diff --git a/build.gradle b/build.gradle index b0f7958ae..e3ccd99e3 100644 --- a/build.gradle +++ b/build.gradle @@ -41,7 +41,7 @@ plugins { } group = 'art.arcane' -version = '4.0.0-1.21.11' +version = '4.0.0-26.1' String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate') .orElse('com.github.VolmitSoftware:VolmLib:master-SNAPSHOT') .get() diff --git a/core/src/main/java/art/arcane/iris/Iris.java b/core/src/main/java/art/arcane/iris/Iris.java index 4d80d9022..f6f69f639 100644 --- a/core/src/main/java/art/arcane/iris/Iris.java +++ b/core/src/main/java/art/arcane/iris/Iris.java @@ -36,15 +36,18 @@ import art.arcane.iris.core.pack.BrokenPackException; import art.arcane.iris.core.pack.PackValidationRegistry; import art.arcane.iris.core.pack.PackValidationResult; import art.arcane.iris.core.pack.PackValidator; +import art.arcane.iris.core.gui.PregeneratorJob; import art.arcane.iris.core.service.StudioSVC; import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.engine.EnginePanic; +import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.object.IrisCompat; import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisWorld; import art.arcane.iris.engine.platform.BukkitChunkGenerator; import art.arcane.iris.core.safeguard.IrisSafeguard; import art.arcane.iris.engine.platform.PlatformChunkGenerator; +import art.arcane.volmlib.integration.ReloadAware; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KMap; import art.arcane.volmlib.util.exceptions.IrisException; @@ -85,12 +88,17 @@ import java.io.*; import java.lang.annotation.Annotation; import java.net.URI; import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Predicate; import java.util.regex.Matcher; import java.util.regex.Pattern; @SuppressWarnings("CanBeFinal") -public class Iris extends VolmitPlugin implements Listener { +public class Iris extends VolmitPlugin implements Listener, ReloadAware { private static final Queue syncJobs = new ShurikenQueue<>(); public static Iris instance; @@ -115,6 +123,7 @@ public class Iris extends VolmitPlugin implements Listener { } private final KList postShutdown = new KList<>(); + private final AtomicBoolean alreadyDrained = new AtomicBoolean(false); private KMap, IrisService> services; public static VolmitSender getSender() { @@ -868,6 +877,9 @@ public class Iris extends VolmitPlugin implements Listener { public void onDisable() { if (IrisSafeguard.isForceShutdown()) return; + if (!alreadyDrained.get()) { + drainWorldGenerators("onDisable", 30L); + } if (services != null) { services.values().forEach(IrisService::onDisable); } @@ -883,6 +895,67 @@ public class Iris extends VolmitPlugin implements Listener { J.attempt(new JarScanner(instance.getJarFile(), "", false)::scanAll); } + @Override + public void onPreUnload(ReloadAware.PreUnloadReason reason) { + if (!alreadyDrained.compareAndSet(false, true)) { + Iris.info("Pre-unload hook skipped; Iris already drained."); + return; + } + Iris.info("BileTools pre-unload hook fired (" + reason + "). Freezing all Iris worlds."); + drainWorldGenerators("pre-unload:" + reason, 45L); + } + + private void drainWorldGenerators(String reason, long timeoutSeconds) { + List irisWorlds = new ArrayList<>(); + for (World world : Bukkit.getWorlds()) { + if (IrisToolbelt.access(world) != null) { + irisWorlds.add(world); + } + } + if (irisWorlds.isEmpty()) { + Iris.info("No Iris worlds to freeze."); + return; + } + + for (World world : irisWorlds) { + IrisToolbelt.beginWorldMaintenance(world, reason, true); + } + + J.attempt(PregeneratorJob::shutdownInstance); + + List> closes = new ArrayList<>(); + for (World world : irisWorlds) { + PlatformChunkGenerator gen = IrisToolbelt.access(world); + if (gen == null) continue; + + Engine engine = gen.getEngine(); + if (engine != null) { + J.attempt(() -> engine.getMantle().saveAllNow()); + } + + try { + closes.add(gen.closeAsync()); + } catch (Throwable t) { + Iris.reportError(t); + } + } + + if (closes.isEmpty()) return; + + try { + CompletableFuture.allOf(closes.toArray(new CompletableFuture[0])) + .get(timeoutSeconds, TimeUnit.SECONDS); + Iris.info("All Iris chunk generators parked. Safe to unload."); + } catch (TimeoutException e) { + Iris.warn("Iris generator drain timed out after " + timeoutSeconds + "s; unload proceeding anyway."); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + Iris.warn("Iris generator drain interrupted; unload proceeding."); + } catch (ExecutionException e) { + Iris.reportError(e.getCause() == null ? e : e.getCause()); + } + } + private void setupPapi() { if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) { new IrisPapiExpansion().register(); diff --git a/core/src/main/java/art/arcane/iris/core/ExternalDataPackPipeline.java b/core/src/main/java/art/arcane/iris/core/ExternalDataPackPipeline.java deleted file mode 100644 index 587a8ca69..000000000 --- a/core/src/main/java/art/arcane/iris/core/ExternalDataPackPipeline.java +++ /dev/null @@ -1,5357 +0,0 @@ -package art.arcane.iris.core; - -import art.arcane.iris.Iris; -import art.arcane.iris.core.loader.IrisData; -import art.arcane.iris.core.nms.INMS; -import art.arcane.iris.core.nms.container.StructurePlacement; -import art.arcane.iris.core.link.Identifier; -import art.arcane.iris.engine.data.cache.AtomicCache; -import art.arcane.iris.engine.object.IrisObject; -import art.arcane.iris.engine.object.IrisDimension; -import art.arcane.iris.engine.object.TileData; -import art.arcane.iris.util.common.data.B; -import art.arcane.iris.util.common.math.Vector3i; -import art.arcane.volmlib.util.collection.KList; -import art.arcane.volmlib.util.collection.KMap; -import art.arcane.volmlib.util.json.JSONArray; -import art.arcane.volmlib.util.json.JSONObject; -import art.arcane.volmlib.util.nbt.io.NBTDeserializer; -import art.arcane.volmlib.util.nbt.io.NamedTag; -import art.arcane.volmlib.util.nbt.tag.ByteArrayTag; -import art.arcane.volmlib.util.nbt.tag.ByteTag; -import art.arcane.volmlib.util.nbt.tag.CompoundTag; -import art.arcane.volmlib.util.nbt.tag.DoubleTag; -import art.arcane.volmlib.util.nbt.tag.FloatTag; -import art.arcane.volmlib.util.nbt.tag.IntTag; -import art.arcane.volmlib.util.nbt.tag.IntArrayTag; -import art.arcane.volmlib.util.nbt.tag.ListTag; -import art.arcane.volmlib.util.nbt.tag.LongTag; -import art.arcane.volmlib.util.nbt.tag.LongArrayTag; -import art.arcane.volmlib.util.nbt.tag.NumberTag; -import art.arcane.volmlib.util.nbt.tag.ShortTag; -import art.arcane.volmlib.util.nbt.tag.StringTag; -import art.arcane.volmlib.util.nbt.tag.Tag; -import org.bukkit.World; -import org.bukkit.block.data.BlockData; -import org.bukkit.Bukkit; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.URI; -import java.net.URL; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.StandardCopyOption; -import java.security.MessageDigest; -import java.time.Instant; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.EnumMap; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutorCompletionService; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; -import java.util.zip.ZipOutputStream; - -public final class ExternalDataPackPipeline { - private static final Pattern STRUCTURE_JSON_ENTRY = Pattern.compile("(?i)^data/([^/]+)/worldgen/structure/(.+)\\.json$"); - private static final Pattern STRUCTURE_SET_JSON_ENTRY = Pattern.compile("(?i)^data/([^/]+)/worldgen/structure_set/(.+)\\.json$"); - private static final Pattern CONFIGURED_FEATURE_JSON_ENTRY = Pattern.compile("(?i)^data/([^/]+)/worldgen/configured_feature/(.+)\\.json$"); - private static final Pattern PLACED_FEATURE_JSON_ENTRY = Pattern.compile("(?i)^data/([^/]+)/worldgen/placed_feature/(.+)\\.json$"); - private static final Pattern TEMPLATE_POOL_JSON_ENTRY = Pattern.compile("(?i)^data/([^/]+)/worldgen/template_pool/(.+)\\.json$"); - private static final Pattern PROCESSOR_LIST_JSON_ENTRY = Pattern.compile("(?i)^data/([^/]+)/worldgen/processor_list/(.+)\\.json$"); - private static final Pattern BIOME_HAS_STRUCTURE_TAG_ENTRY = Pattern.compile("(?i)^data/([^/]+)/tags/worldgen/biome/has_structure/(.+)\\.json$"); - private static final Pattern MODRINTH_VERSION_URL = Pattern.compile("^https?://modrinth\\.com/(?:datapack|mod|plugin|resourcepack)/([^/?#]+)/version/([^/?#]+).*$", Pattern.CASE_INSENSITIVE); - private static final Pattern STRUCTURE_ENTRY = Pattern.compile("(?i)(?:^|.*/)data/([^/]+)/(?:structure|structures)/(.+\\.nbt)$"); - private static final String EXTERNAL_PACK_INDEX = "datapack-imports"; - private static final String PACK_NAME = EXTERNAL_PACK_INDEX; - private static final String EXTERNAL_PACK_INDEX_SUBFOLDER = "datapack-imports"; - private static final String IMPORT_INDEX_FILE_NAME = "datapack-index.json"; - private static final String MANAGED_WORLD_PACK_PREFIX = "iris-external-"; - private static final String MANAGED_PACK_META_DESCRIPTION = "Iris managed external structure datapack assets."; - private static final long MANAGED_ZIP_ENTRY_TIME = 315532800000L; - private static final String IMPORT_PREFIX = "imports"; - private static final String LOCATE_MANIFEST_PATH = "cache/external-datapack-locate-manifest.json"; - private static final String OBJECT_LOCATE_MANIFEST_PATH = "cache/external-datapack-object-locate-manifest.json"; - private static final String SMARTBORE_STRUCTURE_MANIFEST_PATH = "cache/external-datapack-smartbore-manifest.json"; - private static final int CONNECT_TIMEOUT_MS = 4000; - private static final int READ_TIMEOUT_MS = 8000; - private static final int IMPORT_PARALLELISM = Math.max(1, Math.min(8, Runtime.getRuntime().availableProcessors())); - private static final int MAX_IN_FLIGHT = Math.max(2, IMPORT_PARALLELISM * 3); - private static final Map BLOCK_DATA_CACHE = new ConcurrentHashMap<>(); - private static final Map PACK_ENVIRONMENT_CACHE = new ConcurrentHashMap<>(); - private static final Map> RESOLVED_LOCATE_STRUCTURES_BY_ID = new ConcurrentHashMap<>(); - private static final Map> RESOLVED_LOCATE_STRUCTURES_BY_OBJECT_KEY = new ConcurrentHashMap<>(); - private static final Map> RESOLVED_SMARTBORE_STRUCTURES_BY_ID = new ConcurrentHashMap<>(); - private static final AtomicCache> VANILLA_STRUCTURE_PLACEMENTS = new AtomicCache<>(); - private static final BlockData AIR = B.getAir(); - - private ExternalDataPackPipeline() { - } - - public static String sanitizePackNameValue(String value) { - return sanitizePackName(value); - } - - public static String normalizeEnvironmentValue(String value) { - return normalizeEnvironment(value); - } - - public static Set resolveLocateStructuresForId(String id) { - String normalizedId = normalizeLocateId(id); - if (normalizedId.isBlank()) { - return Set.of(); - } - - Set resolved = RESOLVED_LOCATE_STRUCTURES_BY_ID.get(normalizedId); - if (resolved != null && !resolved.isEmpty()) { - return Set.copyOf(resolved); - } - - Map> fromManifest = readLocateManifest(); - Set manifestSet = fromManifest.get(normalizedId); - if (manifestSet == null || manifestSet.isEmpty()) { - return Set.of(); - } - - RESOLVED_LOCATE_STRUCTURES_BY_ID.put(normalizedId, Set.copyOf(manifestSet)); - return Set.copyOf(manifestSet); - } - - public static Set resolveLocateStructuresForObjectKey(String objectKey) { - String normalizedObjectKey = normalizeObjectLoadKey(objectKey); - if (normalizedObjectKey.isBlank()) { - return Set.of(); - } - - Set resolved = RESOLVED_LOCATE_STRUCTURES_BY_OBJECT_KEY.get(normalizedObjectKey); - if (resolved != null && !resolved.isEmpty()) { - return Set.copyOf(resolved); - } - - Map> fromManifest = readObjectLocateManifest(); - Set manifestSet = fromManifest.get(normalizedObjectKey); - if (manifestSet == null || manifestSet.isEmpty()) { - return Set.of(); - } - - RESOLVED_LOCATE_STRUCTURES_BY_OBJECT_KEY.put(normalizedObjectKey, Set.copyOf(manifestSet)); - return Set.copyOf(manifestSet); - } - - public static Map> snapshotLocateStructuresById() { - if (RESOLVED_LOCATE_STRUCTURES_BY_ID.isEmpty()) { - Map> manifest = readLocateManifest(); - if (!manifest.isEmpty()) { - RESOLVED_LOCATE_STRUCTURES_BY_ID.putAll(manifest); - } - } - - ArrayList ids = new ArrayList<>(RESOLVED_LOCATE_STRUCTURES_BY_ID.keySet()); - ids.sort(String::compareTo); - LinkedHashMap> snapshot = new LinkedHashMap<>(); - for (String id : ids) { - if (id == null || id.isBlank()) { - continue; - } - - Set structures = RESOLVED_LOCATE_STRUCTURES_BY_ID.get(id); - if (structures == null || structures.isEmpty()) { - continue; - } - - ArrayList sortedStructures = new ArrayList<>(structures); - sortedStructures.sort(String::compareTo); - snapshot.put(id, Set.copyOf(sortedStructures)); - } - - return Map.copyOf(snapshot); - } - - public static Set snapshotLocateStructureKeys() { - Map> locateById = snapshotLocateStructuresById(); - LinkedHashSet structures = new LinkedHashSet<>(); - for (Set values : locateById.values()) { - if (values == null || values.isEmpty()) { - continue; - } - - for (String value : values) { - String normalized = normalizeLocateStructure(value); - if (!normalized.isBlank()) { - structures.add(normalized); - } - } - } - - return Set.copyOf(structures); - } - - public static Set snapshotSmartBoreStructureKeys() { - if (RESOLVED_SMARTBORE_STRUCTURES_BY_ID.isEmpty()) { - Map> manifest = readSmartBoreManifest(); - if (!manifest.isEmpty()) { - RESOLVED_SMARTBORE_STRUCTURES_BY_ID.putAll(manifest); - } - } - - LinkedHashSet structures = new LinkedHashSet<>(); - for (Set values : RESOLVED_SMARTBORE_STRUCTURES_BY_ID.values()) { - if (values == null || values.isEmpty()) { - continue; - } - - for (String value : values) { - String normalized = normalizeLocateStructure(value); - if (!normalized.isBlank()) { - structures.add(normalized); - } - } - } - - return Set.copyOf(structures); - } - - public static PipelineSummary processDatapacks(List requests, Map> worldDatapackFoldersByPack) { - PipelineSummary summary = new PipelineSummary(); - PACK_ENVIRONMENT_CACHE.clear(); - RESOLVED_LOCATE_STRUCTURES_BY_ID.clear(); - RESOLVED_LOCATE_STRUCTURES_BY_OBJECT_KEY.clear(); - RESOLVED_SMARTBORE_STRUCTURES_BY_ID.clear(); - - Set knownWorldDatapackFolders = new LinkedHashSet<>(); - if (worldDatapackFoldersByPack != null) { - for (Map.Entry> entry : worldDatapackFoldersByPack.entrySet()) { - KList folders = entry.getValue(); - if (folders == null) { - continue; - } - for (File folder : folders) { - if (folder != null) { - knownWorldDatapackFolders.add(folder); - } - } - } - } - collectWorldDatapackFolders(knownWorldDatapackFolders); - summary.legacyDownloadRemovals = removeLegacyGlobalDownloads(); - summary.legacyWorldCopyRemovals = removeLegacyWorldDatapackCopies(knownWorldDatapackFolders); - - List normalizedRequests = normalizeRequests(requests); - summary.requests = normalizedRequests.size(); - if (normalizedRequests.isEmpty()) { - Iris.info("Downloading datapacks [0/0] Downloading/Done!"); - writeLocateManifest(Map.of()); - writeObjectLocateManifest(Map.of()); - writeSmartBoreManifest(Map.of()); - summary.legacyWorldCopyRemovals += pruneManagedWorldDatapacks(knownWorldDatapackFolders, Set.of()); - return summary; - } - - migrateLegacyImportIndex(); - - Map oldIndexByPack = readAllImportIndexes(); - Map oldSources = flattenOldSources(oldIndexByPack); - Map newSourcesByPack = new HashMap<>(); - Map importSummaryByPack = new HashMap<>(); - Set seenSourceKeys = new HashSet<>(); - Set activeManagedWorldDatapackNames = new HashSet<>(); - - Map> priorLocateManifest = readLocateManifest(); - Map> priorObjectLocateManifest = readObjectLocateManifest(); - Map> priorSmartBoreManifest = readSmartBoreManifest(); - - List sourceInputs = new ArrayList<>(); - LinkedHashMap> resolvedLocateStructuresById = new LinkedHashMap<>(); - LinkedHashMap> resolvedLocateStructuresByObjectKey = new LinkedHashMap<>(); - LinkedHashMap> resolvedSmartBoreStructuresById = new LinkedHashMap<>(); - for (int requestIndex = 0; requestIndex < normalizedRequests.size(); requestIndex++) { - DatapackRequest request = normalizedRequests.get(requestIndex); - if (request == null) { - continue; - } - - if (request.replaceVanilla() && !request.hasReplacementTargets()) { - Iris.verbose("Datapack id=" + request.id() + " has replaceVanilla without explicit targets; all minecraft namespace entries will be projected."); - } - - KList targetWorldFolders = resolveTargetWorldFolders(request.targetPack(), worldDatapackFoldersByPack); - if (!targetWorldFolders.isEmpty()) { - Map existingManagedZips = findExistingManagedZipsForRequest(targetWorldFolders, request.targetPack(), request.id()); - if (existingManagedZips.size() == targetWorldFolders.size()) { - adoptExistingManagedRequest( - request, - existingManagedZips, - oldSources, - newSourcesByPack, - seenSourceKeys, - activeManagedWorldDatapackNames, - importSummaryByPack, - priorLocateManifest, - priorObjectLocateManifest, - priorSmartBoreManifest, - resolvedLocateStructuresById, - resolvedLocateStructuresByObjectKey, - resolvedSmartBoreStructuresById - ); - summary.skippedExistingRequests++; - Iris.verbose("External datapack already present, skipping sync/projection: id=" + request.id() - + ", targetPack=" + request.targetPack() - + ", existingDatapacks=" + existingManagedZips.size()); - continue; - } - } - - RequestSyncResult syncResult = syncRequest(request); - if (!syncResult.success()) { - if (request.required()) { - summary.requiredFailures++; - } else { - summary.optionalFailures++; - } - Iris.warn("Downloading datapacks [" + (requestIndex + 1) + "/" + normalizedRequests.size() + "] Failed! id=" + request.id() + " (" + syncResult.error() + ")."); - mergeResolvedLocateStructures(resolvedLocateStructuresById, request.id(), request.resolvedLocateStructures()); - continue; - } - - if (syncResult.downloaded()) { - summary.syncedRequests++; - Iris.info("Downloading datapacks [" + (requestIndex + 1) + "/" + normalizedRequests.size() + "] Downloading/Done!"); - } else if (syncResult.restored()) { - summary.restoredRequests++; - } - mergeResolvedLocateStructures(resolvedLocateStructuresById, request.id(), request.resolvedLocateStructures()); - sourceInputs.add(new RequestedSourceInput(syncResult.source(), request)); - } - - if (sourceInputs.isEmpty() && summary.skippedExistingRequests == 0) { - if (summary.requiredFailures == 0) { - summary.legacyWorldCopyRemovals += pruneManagedWorldDatapacks(knownWorldDatapackFolders, Set.of()); - } - writeLocateManifest(resolvedLocateStructuresById); - writeObjectLocateManifest(resolvedLocateStructuresByObjectKey); - writeSmartBoreManifest(resolvedSmartBoreStructuresById); - RESOLVED_LOCATE_STRUCTURES_BY_ID.putAll(resolvedLocateStructuresById); - RESOLVED_LOCATE_STRUCTURES_BY_OBJECT_KEY.putAll(resolvedLocateStructuresByObjectKey); - RESOLVED_SMARTBORE_STRUCTURES_BY_ID.putAll(resolvedSmartBoreStructuresById); - return summary; - } - - for (int sourceIndex = 0; sourceIndex < sourceInputs.size(); sourceIndex++) { - RequestedSourceInput sourceInput = sourceInputs.get(sourceIndex); - File entry = sourceInput.source(); - DatapackRequest request = sourceInput.request(); - if (entry == null || !entry.exists() || request == null) { - continue; - } - - SourceDescriptor sourceDescriptor = createSourceDescriptor(entry, request.id(), request.targetPack(), request.requiredEnvironment()); - if (sourceDescriptor.requiredEnvironment() != null) { - String packEnvironment = resolvePackEnvironment(sourceDescriptor.targetPack()); - if (packEnvironment == null || !packEnvironment.equals(sourceDescriptor.requiredEnvironment())) { - if (request.required()) { - summary.requiredFailures++; - } else { - summary.optionalFailures++; - } - Iris.warn("Skipped external datapack source " + sourceDescriptor.sourceName() - + " targetPack=" + sourceDescriptor.targetPack() - + " requiredEnvironment=" + sourceDescriptor.requiredEnvironment() - + " packEnvironment=" + (packEnvironment == null ? "unknown" : packEnvironment)); - continue; - } - } - - seenSourceKeys.add(sourceDescriptor.sourceKey()); - File sourceRoot = resolveSourceRoot(sourceDescriptor.targetPack(), sourceDescriptor.objectRootKey()); - JSONObject cachedSource = oldSources.get(sourceDescriptor.sourceKey()); - String cachedTargetPack = cachedSource == null - ? null - : sanitizePackName(cachedSource.optString("targetPack", defaultTargetPack())); - boolean sameTargetPack = cachedTargetPack != null && cachedTargetPack.equals(sourceDescriptor.targetPack()); - String cachedObjectRootKey = cachedSource == null ? "" : normalizeObjectRootKey(cachedSource.optString("objectRootKey", "")); - boolean sameObjectRoot = cachedObjectRootKey.equals(sourceDescriptor.objectRootKey()); - JSONObject activeSource = null; - - JSONArray newSources = newSourcesByPack.computeIfAbsent(sourceDescriptor.targetPack(), k -> new JSONArray()); - ImportSummary packImportSummary = importSummaryByPack.computeIfAbsent(sourceDescriptor.targetPack(), k -> new ImportSummary()); - - if (cachedSource != null - && sourceDescriptor.fingerprint().equals(cachedSource.optString("fingerprint", "")) - && sameTargetPack - && sameObjectRoot - && sourceRoot.exists()) { - newSources.put(cachedSource); - addSourceToSummary(packImportSummary, cachedSource, true); - activeSource = cachedSource; - } else { - if (cachedTargetPack != null && cachedSource != null) { - File previousSourceRoot = resolveSourceRoot(cachedTargetPack, cachedObjectRootKey); - deleteFolder(previousSourceRoot); - String cachedSourceKey = cachedSource.optString("sourceKey", sourceDescriptor.sourceKey()); - File previousLegacySourceRoot = resolveLegacySourceRoot(cachedTargetPack, cachedSourceKey); - deleteFolder(previousLegacySourceRoot); - } - - deleteFolder(sourceRoot); - sourceRoot.mkdirs(); - JSONObject sourceResult = convertSource(entry, sourceDescriptor, sourceRoot, request.id()); - newSources.put(sourceResult); - addSourceToSummary(packImportSummary, sourceResult, false); - activeSource = sourceResult; - int conversionFailed = sourceResult.optInt("failed", 0); - if (conversionFailed > 0) { - int conversionScanned = sourceResult.optInt("nbtScanned", 0); - int conversionSuccess = sourceResult.optInt("converted", 0); - Iris.warn("External datapack object import had " + conversionFailed - + " failed structure conversion(s) for id=" + request.id() - + " source=" + sourceDescriptor.sourceName() - + " (scanned=" + conversionScanned + ", converted=" + conversionSuccess + ")."); - } - } - - KList targetWorldFolders = resolveTargetWorldFolders(request.targetPack(), worldDatapackFoldersByPack); - ProjectionResult projectionResult = projectSourceToWorldDatapacks(entry, sourceDescriptor, request, targetWorldFolders); - summary.worldDatapacksInstalled += projectionResult.installedDatapacks(); - summary.worldAssetsInstalled += projectionResult.installedAssets(); - mergeResolvedLocateStructures(resolvedLocateStructuresById, request.id(), projectionResult.resolvedLocateStructures()); - if (request.supportSmartBore()) { - LinkedHashSet smartBoreTargets = new LinkedHashSet<>(); - smartBoreTargets.addAll(request.resolvedLocateStructures()); - smartBoreTargets.addAll(projectionResult.resolvedLocateStructures()); - mergeResolvedLocateStructures(resolvedSmartBoreStructuresById, request.id(), smartBoreTargets); - } - LinkedHashSet objectLocateTargets = new LinkedHashSet<>(); - objectLocateTargets.addAll(request.resolvedLocateStructures()); - objectLocateTargets.addAll(projectionResult.resolvedLocateStructures()); - mergeResolvedLocateStructuresByObjectKey( - resolvedLocateStructuresByObjectKey, - extractObjectKeys(activeSource), - objectLocateTargets - ); - if (projectionResult.managedName() != null && !projectionResult.managedName().isBlank() && projectionResult.installedDatapacks() > 0) { - activeManagedWorldDatapackNames.add(projectionResult.managedName()); - } - if (projectionResult.success()) { - Iris.verbose("External datapack projection: id=" + request.id() - + ", source=" + sourceDescriptor.sourceName() - + ", targetPack=" + request.targetPack() - + ", managedDatapack=" + projectionResult.managedName() - + ", installedDatapacks=" + projectionResult.installedDatapacks() - + ", installedAssets=" + projectionResult.installedAssets() - + ", syntheticStructureSets=" + projectionResult.syntheticStructureSets() - + ", templateAliasesApplied=" + projectionResult.templateAliasesApplied() - + ", emptyElementConversions=" + projectionResult.emptyElementConversions() - + ", unresolvedTemplateRefs=" + projectionResult.unresolvedTemplateRefs() - + ", success=true"); - } else { - Iris.warn("External datapack projection: id=" + request.id() - + ", source=" + sourceDescriptor.sourceName() - + ", targetPack=" + request.targetPack() - + ", managedDatapack=" + projectionResult.managedName() - + ", installedDatapacks=" + projectionResult.installedDatapacks() - + ", installedAssets=" + projectionResult.installedAssets() - + ", syntheticStructureSets=" + projectionResult.syntheticStructureSets() - + ", templateAliasesApplied=" + projectionResult.templateAliasesApplied() - + ", emptyElementConversions=" + projectionResult.emptyElementConversions() - + ", unresolvedTemplateRefs=" + projectionResult.unresolvedTemplateRefs() - + ", success=false" - + ", reason=" + projectionResult.error()); - } - if (!projectionResult.success()) { - if (request.required()) { - summary.requiredFailures++; - } else { - summary.optionalFailures++; - } - } - } - - pruneRemovedSourceFolders(oldSources, seenSourceKeys); - ImportSummary combinedImportSummary = new ImportSummary(); - for (Map.Entry entry : newSourcesByPack.entrySet()) { - String targetPack = entry.getKey(); - JSONArray packSources = entry.getValue(); - ImportSummary packSummary = importSummaryByPack.getOrDefault(targetPack, new ImportSummary()); - writeIndex(resolveImportIndexFile(targetPack), packSources, packSummary); - mergeImportSummaryInto(combinedImportSummary, packSummary); - } - for (String stalePack : oldIndexByPack.keySet()) { - if (!newSourcesByPack.containsKey(stalePack)) { - File staleIndex = resolveImportIndexFile(stalePack); - if (staleIndex.exists()) { - writeIndex(staleIndex, new JSONArray(), new ImportSummary()); - } - } - } - summary.setImportSummary(combinedImportSummary); - if (summary.requiredFailures == 0) { - summary.legacyWorldCopyRemovals += pruneManagedWorldDatapacks(knownWorldDatapackFolders, activeManagedWorldDatapackNames); - } - - writeLocateManifest(resolvedLocateStructuresById); - writeObjectLocateManifest(resolvedLocateStructuresByObjectKey); - writeSmartBoreManifest(resolvedSmartBoreStructuresById); - RESOLVED_LOCATE_STRUCTURES_BY_ID.putAll(resolvedLocateStructuresById); - RESOLVED_LOCATE_STRUCTURES_BY_OBJECT_KEY.putAll(resolvedLocateStructuresByObjectKey); - RESOLVED_SMARTBORE_STRUCTURES_BY_ID.putAll(resolvedSmartBoreStructuresById); - return summary; - } - - private static File getLocateManifestFile() { - return Iris.instance.getDataFile(LOCATE_MANIFEST_PATH); - } - - private static File getObjectLocateManifestFile() { - return Iris.instance.getDataFile(OBJECT_LOCATE_MANIFEST_PATH); - } - - private static File getSmartBoreManifestFile() { - return Iris.instance.getDataFile(SMARTBORE_STRUCTURE_MANIFEST_PATH); - } - - private static String normalizeLocateId(String id) { - if (id == null) { - return ""; - } - - String normalized = id.trim().toLowerCase(Locale.ROOT); - if (normalized.isBlank()) { - return ""; - } - - normalized = normalized.replace("minecraft:worldgen/structure/", ""); - normalized = normalized.replace("worldgen/structure/", ""); - return normalized; - } - - private static String normalizeLocateStructure(String structure) { - if (structure == null || structure.isBlank()) { - return ""; - } - String normalized = normalizeResourceKey("minecraft", structure, "worldgen/structure/"); - if (normalized == null || normalized.isBlank()) { - return ""; - } - return normalized; - } - - private static String normalizeObjectLoadKey(String objectKey) { - if (objectKey == null) { - return ""; - } - - String normalized = sanitizePath(objectKey); - if (normalized.endsWith(".iob")) { - normalized = normalized.substring(0, normalized.length() - 4); - } - return normalized; - } - - private static String normalizeObjectRootKey(String requestId) { - if (requestId == null) { - return "external-datapack"; - } - - String normalized = sanitizePath(requestId).replace("/", "_"); - if (normalized.isBlank()) { - return "external-datapack"; - } - - return normalized; - } - - private static Set extractObjectKeys(JSONObject source) { - LinkedHashSet objectKeys = new LinkedHashSet<>(); - if (source == null) { - return objectKeys; - } - - JSONArray objects = source.optJSONArray("objects"); - if (objects == null) { - return objectKeys; - } - - for (int i = 0; i < objects.length(); i++) { - JSONObject object = objects.optJSONObject(i); - if (object == null) { - continue; - } - - String objectKey = normalizeObjectLoadKey(object.optString("objectKey", "")); - if (!objectKey.isBlank()) { - objectKeys.add(objectKey); - } - } - - return objectKeys; - } - - private static void mergeResolvedLocateStructures(Map> destination, String id, Set resolvedStructures) { - if (destination == null) { - return; - } - - String normalizedId = normalizeLocateId(id); - if (normalizedId.isBlank() || resolvedStructures == null || resolvedStructures.isEmpty()) { - return; - } - - Set merged = destination.computeIfAbsent(normalizedId, key -> new LinkedHashSet<>()); - for (String structure : resolvedStructures) { - String normalizedStructure = normalizeLocateStructure(structure); - if (!normalizedStructure.isBlank()) { - merged.add(normalizedStructure); - } - } - } - - private static void mergeResolvedLocateStructuresByObjectKey( - Map> destination, - Set objectKeys, - Set resolvedStructures - ) { - if (destination == null || objectKeys == null || objectKeys.isEmpty() || resolvedStructures == null || resolvedStructures.isEmpty()) { - return; - } - - LinkedHashSet normalizedStructures = new LinkedHashSet<>(); - for (String structure : resolvedStructures) { - String normalized = normalizeLocateStructure(structure); - if (!normalized.isBlank()) { - normalizedStructures.add(normalized); - } - } - - if (normalizedStructures.isEmpty()) { - return; - } - - for (String objectKey : objectKeys) { - String normalizedObjectKey = normalizeObjectLoadKey(objectKey); - if (normalizedObjectKey.isBlank()) { - continue; - } - - Set merged = destination.computeIfAbsent(normalizedObjectKey, key -> new LinkedHashSet<>()); - merged.addAll(normalizedStructures); - } - } - - private static void writeLocateManifest(Map> resolvedLocateStructuresById) { - File output = getLocateManifestFile(); - LinkedHashMap> normalized = new LinkedHashMap<>(); - if (resolvedLocateStructuresById != null) { - for (Map.Entry> entry : resolvedLocateStructuresById.entrySet()) { - String normalizedId = normalizeLocateId(entry.getKey()); - if (normalizedId.isBlank()) { - continue; - } - - LinkedHashSet structures = new LinkedHashSet<>(); - Set values = entry.getValue(); - if (values != null) { - for (String structure : values) { - String normalizedStructure = normalizeLocateStructure(structure); - if (!normalizedStructure.isBlank()) { - structures.add(normalizedStructure); - } - } - } - - if (!structures.isEmpty()) { - normalized.put(normalizedId, Set.copyOf(structures)); - } - } - } - - JSONObject root = new JSONObject(); - root.put("generatedAt", Instant.now().toString()); - JSONObject mappings = new JSONObject(); - ArrayList ids = new ArrayList<>(normalized.keySet()); - ids.sort(String::compareTo); - for (String id : ids) { - Set structures = normalized.get(id); - if (structures == null || structures.isEmpty()) { - continue; - } - - ArrayList sortedStructures = new ArrayList<>(structures); - sortedStructures.sort(String::compareTo); - JSONArray values = new JSONArray(); - for (String structure : sortedStructures) { - values.put(structure); - } - mappings.put(id, values); - } - root.put("ids", mappings); - - try { - writeBytesToFile(root.toString(4).getBytes(StandardCharsets.UTF_8), output); - } catch (Throwable e) { - Iris.warn("Failed to write external datapack locate manifest " + output.getPath()); - Iris.reportError(e); - } - } - - private static void writeObjectLocateManifest(Map> resolvedLocateStructuresByObjectKey) { - File output = getObjectLocateManifestFile(); - LinkedHashMap> normalized = new LinkedHashMap<>(); - if (resolvedLocateStructuresByObjectKey != null) { - for (Map.Entry> entry : resolvedLocateStructuresByObjectKey.entrySet()) { - String normalizedObjectKey = normalizeObjectLoadKey(entry.getKey()); - if (normalizedObjectKey.isBlank()) { - continue; - } - - LinkedHashSet structures = new LinkedHashSet<>(); - Set values = entry.getValue(); - if (values != null) { - for (String structure : values) { - String normalizedStructure = normalizeLocateStructure(structure); - if (!normalizedStructure.isBlank()) { - structures.add(normalizedStructure); - } - } - } - - if (!structures.isEmpty()) { - normalized.put(normalizedObjectKey, Set.copyOf(structures)); - } - } - } - - JSONObject root = new JSONObject(); - root.put("generatedAt", Instant.now().toString()); - JSONObject mappings = new JSONObject(); - ArrayList objectKeys = new ArrayList<>(normalized.keySet()); - objectKeys.sort(String::compareTo); - for (String objectKey : objectKeys) { - Set structures = normalized.get(objectKey); - if (structures == null || structures.isEmpty()) { - continue; - } - - ArrayList sortedStructures = new ArrayList<>(structures); - sortedStructures.sort(String::compareTo); - JSONArray values = new JSONArray(); - for (String structure : sortedStructures) { - values.put(structure); - } - mappings.put(objectKey, values); - } - root.put("objects", mappings); - - try { - writeBytesToFile(root.toString(4).getBytes(StandardCharsets.UTF_8), output); - } catch (Throwable e) { - Iris.warn("Failed to write external datapack object locate manifest " + output.getPath()); - Iris.reportError(e); - } - } - - private static void writeSmartBoreManifest(Map> resolvedSmartBoreStructuresById) { - File output = getSmartBoreManifestFile(); - LinkedHashMap> normalized = new LinkedHashMap<>(); - if (resolvedSmartBoreStructuresById != null) { - for (Map.Entry> entry : resolvedSmartBoreStructuresById.entrySet()) { - String normalizedId = normalizeLocateId(entry.getKey()); - if (normalizedId.isBlank()) { - continue; - } - - LinkedHashSet structures = new LinkedHashSet<>(); - Set values = entry.getValue(); - if (values != null) { - for (String structure : values) { - String normalizedStructure = normalizeLocateStructure(structure); - if (!normalizedStructure.isBlank()) { - structures.add(normalizedStructure); - } - } - } - - if (!structures.isEmpty()) { - normalized.put(normalizedId, Set.copyOf(structures)); - } - } - } - - JSONObject root = new JSONObject(); - root.put("generatedAt", Instant.now().toString()); - JSONObject mappings = new JSONObject(); - ArrayList ids = new ArrayList<>(normalized.keySet()); - ids.sort(String::compareTo); - for (String id : ids) { - Set structures = normalized.get(id); - if (structures == null || structures.isEmpty()) { - continue; - } - - ArrayList sortedStructures = new ArrayList<>(structures); - sortedStructures.sort(String::compareTo); - JSONArray values = new JSONArray(); - for (String structure : sortedStructures) { - values.put(structure); - } - mappings.put(id, values); - } - root.put("ids", mappings); - - try { - writeBytesToFile(root.toString(4).getBytes(StandardCharsets.UTF_8), output); - } catch (Throwable e) { - Iris.warn("Failed to write external datapack smartbore manifest " + output.getPath()); - Iris.reportError(e); - } - } - - private static Map> readLocateManifest() { - LinkedHashMap> mapped = new LinkedHashMap<>(); - File input = getLocateManifestFile(); - if (!input.exists() || !input.isFile()) { - return mapped; - } - - try { - JSONObject root = new JSONObject(Files.readString(input.toPath(), StandardCharsets.UTF_8)); - JSONObject ids = root.optJSONObject("ids"); - if (ids == null) { - return mapped; - } - - ArrayList keys = new ArrayList<>(ids.keySet()); - keys.sort(String::compareTo); - for (String key : keys) { - String normalizedId = normalizeLocateId(key); - if (normalizedId.isBlank()) { - continue; - } - - LinkedHashSet structures = new LinkedHashSet<>(); - JSONArray values = ids.optJSONArray(key); - if (values != null) { - for (int i = 0; i < values.length(); i++) { - Object rawValue = values.opt(i); - if (rawValue == null) { - continue; - } - - String normalizedStructure = normalizeLocateStructure(String.valueOf(rawValue)); - if (!normalizedStructure.isBlank()) { - structures.add(normalizedStructure); - } - } - } - - if (!structures.isEmpty()) { - mapped.put(normalizedId, Set.copyOf(structures)); - } - } - } catch (Throwable e) { - Iris.warn("Failed to read external datapack locate manifest " + input.getPath()); - Iris.reportError(e); - } - - return mapped; - } - - private static Map> readObjectLocateManifest() { - LinkedHashMap> mapped = new LinkedHashMap<>(); - File input = getObjectLocateManifestFile(); - if (!input.exists() || !input.isFile()) { - return mapped; - } - - try { - JSONObject root = new JSONObject(Files.readString(input.toPath(), StandardCharsets.UTF_8)); - JSONObject objects = root.optJSONObject("objects"); - if (objects == null) { - return mapped; - } - - ArrayList keys = new ArrayList<>(objects.keySet()); - keys.sort(String::compareTo); - for (String key : keys) { - String normalizedObjectKey = normalizeObjectLoadKey(key); - if (normalizedObjectKey.isBlank()) { - continue; - } - - LinkedHashSet structures = new LinkedHashSet<>(); - JSONArray values = objects.optJSONArray(key); - if (values != null) { - for (int i = 0; i < values.length(); i++) { - Object rawValue = values.opt(i); - if (rawValue == null) { - continue; - } - - String normalizedStructure = normalizeLocateStructure(String.valueOf(rawValue)); - if (!normalizedStructure.isBlank()) { - structures.add(normalizedStructure); - } - } - } - - if (!structures.isEmpty()) { - mapped.put(normalizedObjectKey, Set.copyOf(structures)); - } - } - } catch (Throwable e) { - Iris.warn("Failed to read external datapack object locate manifest " + input.getPath()); - Iris.reportError(e); - } - - return mapped; - } - - private static Map> readSmartBoreManifest() { - LinkedHashMap> mapped = new LinkedHashMap<>(); - File input = getSmartBoreManifestFile(); - if (!input.exists() || !input.isFile()) { - return mapped; - } - - try { - JSONObject root = new JSONObject(Files.readString(input.toPath(), StandardCharsets.UTF_8)); - JSONObject ids = root.optJSONObject("ids"); - if (ids == null) { - return mapped; - } - - ArrayList keys = new ArrayList<>(ids.keySet()); - keys.sort(String::compareTo); - for (String key : keys) { - String normalizedId = normalizeLocateId(key); - if (normalizedId.isBlank()) { - continue; - } - - LinkedHashSet structures = new LinkedHashSet<>(); - JSONArray values = ids.optJSONArray(key); - if (values != null) { - for (int i = 0; i < values.length(); i++) { - Object rawValue = values.opt(i); - if (rawValue == null) { - continue; - } - - String normalizedStructure = normalizeLocateStructure(String.valueOf(rawValue)); - if (!normalizedStructure.isBlank()) { - structures.add(normalizedStructure); - } - } - } - - if (!structures.isEmpty()) { - mapped.put(normalizedId, Set.copyOf(structures)); - } - } - } catch (Throwable e) { - Iris.warn("Failed to read external datapack smartbore manifest " + input.getPath()); - Iris.reportError(e); - } - - return mapped; - } - - private static List normalizeRequests(List requests) { - Map deduplicated = new HashMap<>(); - if (requests == null) { - return new ArrayList<>(); - } - - for (DatapackRequest request : requests) { - if (request == null) { - continue; - } - String dedupeKey = request.getDedupeKey(); - if (dedupeKey.isBlank()) { - continue; - } - - DatapackRequest existing = deduplicated.get(dedupeKey); - if (existing == null) { - deduplicated.put(dedupeKey, request); - } else { - deduplicated.put(dedupeKey, existing.merge(request)); - } - } - - return new ArrayList<>(deduplicated.values()); - } - - private static RequestSyncResult syncRequest(DatapackRequest request) { - if (request == null) { - return RequestSyncResult.failure("request is null"); - } - - String url = request.url(); - if (url == null || url.isBlank()) { - return RequestSyncResult.failure("url is blank"); - } - - File packSourceFolder = Iris.instance.getDataFolder("packs", request.targetPack(), "externaldatapacks"); - File cacheFolder = Iris.instance.getDataFolder("cache", "datapacks"); - packSourceFolder.mkdirs(); - cacheFolder.mkdirs(); - - RequestSyncResult metadataRestore = restoreRequestFromMetadata(packSourceFolder, request); - if (metadataRestore.success()) { - return metadataRestore; - } - - try { - ResolvedRemoteFile remoteFile = resolveRemoteFile(url); - File output = new File(packSourceFolder, remoteFile.outputFileName()); - File cached = new File(cacheFolder, remoteFile.outputFileName()); - if (isUpToDate(output, remoteFile.sha1())) { - writeRequestMetadata(packSourceFolder, request, output.getName(), remoteFile.sha1()); - return RequestSyncResult.restored(output); - } - - if (isUpToDate(cached, remoteFile.sha1())) { - copyFile(cached, output); - if (isUpToDate(output, remoteFile.sha1())) { - writeRequestMetadata(packSourceFolder, request, output.getName(), remoteFile.sha1()); - return RequestSyncResult.restored(output); - } - output.delete(); - } - - downloadToFile(remoteFile.url(), cached); - if (!isUpToDate(cached, remoteFile.sha1())) { - cached.delete(); - return RequestSyncResult.failure("hash mismatch for downloaded datapack"); - } - - copyFile(cached, output); - if (!isUpToDate(output, remoteFile.sha1())) { - output.delete(); - return RequestSyncResult.failure("output write mismatch after download"); - } - - writeRequestMetadata(packSourceFolder, request, output.getName(), remoteFile.sha1()); - return RequestSyncResult.downloaded(output); - } catch (Throwable e) { - String message = e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage(); - return RequestSyncResult.failure(message); - } - } - - private static ResolvedRemoteFile resolveRemoteFile(String url) throws IOException { - Matcher matcher = MODRINTH_VERSION_URL.matcher(url); - if (matcher.matches()) { - ModrinthFile modrinthFile = resolveModrinthFile(url); - return new ResolvedRemoteFile(modrinthFile.url(), modrinthFile.outputFileName(), modrinthFile.sha1()); - } - - String path = URI.create(url).getPath(); - String fileName = path == null || path.isBlank() ? "datapack.zip" : path; - String extension = extension(fileName); - String outputName = "external-" + shortHash(url) + extension; - return new ResolvedRemoteFile(url, outputName, null); - } - - private static void writeRequestMetadata(File packSourceFolder, DatapackRequest request, String fileName, String sha1) { - try { - File metadataFile = getRequestMetadataFile(packSourceFolder, request); - JSONObject metadata = new JSONObject(); - metadata.put("url", request.url()); - metadata.put("file", fileName); - if (sha1 != null && !sha1.isBlank()) { - metadata.put("sha1", sha1); - } - metadata.put("updatedAt", Instant.now().toString()); - Files.writeString(metadataFile.toPath(), metadata.toString(4), StandardCharsets.UTF_8); - } catch (Throwable e) { - Iris.reportError(e); - } - } - - private static RequestSyncResult restoreRequestFromMetadata(File packSourceFolder, DatapackRequest request) { - try { - File metadataFile = getRequestMetadataFile(packSourceFolder, request); - if (!metadataFile.exists() || !metadataFile.isFile()) { - return RequestSyncResult.failure("no cached metadata"); - } - - JSONObject metadata = new JSONObject(Files.readString(metadataFile.toPath(), StandardCharsets.UTF_8)); - String fileName = metadata.optString("file", ""); - if (fileName.isBlank()) { - return RequestSyncResult.failure("cached metadata missing file"); - } - - String sha1 = metadata.optString("sha1", ""); - File candidate = new File(packSourceFolder, fileName); - if (!isUpToDate(candidate, sha1.isBlank() ? null : sha1)) { - return RequestSyncResult.failure("cached datapack failed integrity check"); - } - return RequestSyncResult.restored(candidate); - } catch (Throwable e) { - return RequestSyncResult.failure("failed to restore cached metadata"); - } - } - - private static File getRequestMetadataFile(File packSourceFolder, DatapackRequest request) { - return new File(packSourceFolder, ".iris-request-" + shortHash(request.getDedupeKey()) + ".json"); - } - - private static int removeLegacyGlobalDownloads() { - File legacyFolder = Iris.instance.getDataFolder("datapacks"); - if (!legacyFolder.exists()) { - return 0; - } - - File[] entries = legacyFolder.listFiles(); - int removed = entries == null ? 0 : entries.length; - deleteFolder(legacyFolder); - return removed; - } - - private static int removeLegacyWorldDatapackCopies(Set worldDatapackFolders) { - int removed = 0; - for (File folder : worldDatapackFolders) { - if (folder == null || !folder.exists() || !folder.isDirectory()) { - continue; - } - - File[] entries = folder.listFiles(); - if (entries == null) { - continue; - } - - for (File entry : entries) { - if (entry == null) { - continue; - } - String name = entry.getName().toLowerCase(Locale.ROOT); - if (!name.startsWith("modrinth-")) { - continue; - } - deleteFolder(entry); - if (!entry.exists()) { - removed++; - } - } - } - return removed; - } - - private static int pruneManagedWorldDatapacks(Set worldDatapackFolders, Set activeManagedNames) { - int removed = 0; - for (File folder : worldDatapackFolders) { - if (folder == null || !folder.exists() || !folder.isDirectory()) { - continue; - } - - File[] entries = folder.listFiles(); - if (entries == null) { - continue; - } - - for (File entry : entries) { - if (entry == null) { - continue; - } - String name = entry.getName(); - if (!name.startsWith(MANAGED_WORLD_PACK_PREFIX)) { - continue; - } - if (activeManagedNames.contains(name)) { - continue; - } - deleteFolder(entry); - if (!entry.exists()) { - removed++; - } - } - } - return removed; - } - - private static void adoptExistingManagedRequest( - DatapackRequest request, - Map existingManagedZips, - Map oldSources, - Map newSourcesByPack, - Set seenSourceKeys, - Set activeManagedWorldDatapackNames, - Map importSummaryByPack, - Map> priorLocateManifest, - Map> priorObjectLocateManifest, - Map> priorSmartBoreManifest, - Map> resolvedLocateStructuresById, - Map> resolvedLocateStructuresByObjectKey, - Map> resolvedSmartBoreStructuresById - ) { - if (request == null) { - return; - } - - String normalizedId = normalizeLocateId(request.id()); - String requestObjectRootKey = normalizeObjectRootKey(request.id()); - String requestTargetPack = sanitizePackName(request.targetPack()); - JSONArray newSources = newSourcesByPack.computeIfAbsent(requestTargetPack, k -> new JSONArray()); - ImportSummary importSummary = importSummaryByPack.computeIfAbsent(requestTargetPack, k -> new ImportSummary()); - - LinkedHashSet mergedLocate = new LinkedHashSet<>(); - if (request.resolvedLocateStructures() != null) { - mergedLocate.addAll(request.resolvedLocateStructures()); - } - if (!normalizedId.isBlank() && priorLocateManifest != null) { - Set priorStructures = priorLocateManifest.get(normalizedId); - if (priorStructures != null) { - mergedLocate.addAll(priorStructures); - } - } - mergeResolvedLocateStructures(resolvedLocateStructuresById, request.id(), mergedLocate); - - if (request.supportSmartBore()) { - LinkedHashSet mergedSmartBore = new LinkedHashSet<>(mergedLocate); - if (!normalizedId.isBlank() && priorSmartBoreManifest != null) { - Set priorSmartBore = priorSmartBoreManifest.get(normalizedId); - if (priorSmartBore != null) { - mergedSmartBore.addAll(priorSmartBore); - } - } - mergeResolvedLocateStructures(resolvedSmartBoreStructuresById, request.id(), mergedSmartBore); - } - - if (oldSources != null) { - for (JSONObject source : oldSources.values()) { - if (source == null) { - continue; - } - - String sourceObjectRootKey = normalizeObjectRootKey(source.optString("objectRootKey", "")); - String sourceTargetPack = sanitizePackName(source.optString("targetPack", "")); - if (!requestObjectRootKey.equals(sourceObjectRootKey) || !requestTargetPack.equals(sourceTargetPack)) { - continue; - } - - String sourceKey = source.optString("sourceKey", ""); - if (sourceKey.isEmpty() || !seenSourceKeys.add(sourceKey)) { - continue; - } - - newSources.put(source); - addSourceToSummary(importSummary, source, true); - mergeResolvedLocateStructuresByObjectKey( - resolvedLocateStructuresByObjectKey, - extractObjectKeys(source), - mergedLocate - ); - } - } - - if (priorObjectLocateManifest != null && !priorObjectLocateManifest.isEmpty() && !mergedLocate.isEmpty()) { - LinkedHashSet normalizedMergedLocate = new LinkedHashSet<>(); - for (String structure : mergedLocate) { - String normalizedStructure = normalizeLocateStructure(structure); - if (!normalizedStructure.isBlank()) { - normalizedMergedLocate.add(normalizedStructure); - } - } - if (!normalizedMergedLocate.isEmpty()) { - for (Map.Entry> entry : priorObjectLocateManifest.entrySet()) { - Set priorStructures = entry.getValue(); - if (priorStructures == null || priorStructures.isEmpty()) { - continue; - } - - boolean overlaps = false; - for (String candidate : priorStructures) { - if (normalizedMergedLocate.contains(candidate)) { - overlaps = true; - break; - } - } - if (!overlaps) { - continue; - } - - Set destination = resolvedLocateStructuresByObjectKey.computeIfAbsent(entry.getKey(), key -> new LinkedHashSet<>()); - destination.addAll(priorStructures); - } - } - } - - for (File managedZip : existingManagedZips.values()) { - if (managedZip != null) { - activeManagedWorldDatapackNames.add(managedZip.getName()); - } - } - } - - private static Map findExistingManagedZipsForRequest( - KList worldDatapackFolders, - String targetPack, - String requestId - ) { - LinkedHashMap existing = new LinkedHashMap<>(); - if (worldDatapackFolders == null || worldDatapackFolders.isEmpty()) { - return existing; - } - - String sanitizedPack = sanitizePackName(targetPack); - String sanitizedId = normalizeObjectRootKey(requestId); - if (sanitizedPack.isBlank() || sanitizedId.isBlank()) { - return existing; - } - - String prefix = MANAGED_WORLD_PACK_PREFIX + sanitizedPack + "-" + sanitizedId + "-"; - for (File folder : worldDatapackFolders) { - if (folder == null || !folder.isDirectory()) { - continue; - } - - File[] entries = folder.listFiles(); - if (entries == null) { - continue; - } - - for (File entry : entries) { - if (entry == null || !entry.isFile()) { - continue; - } - - String name = entry.getName(); - if (name.startsWith(prefix) && name.endsWith(".zip") && entry.length() > 0L) { - existing.put(folder, entry); - break; - } - } - } - - return existing; - } - - private static KList resolveTargetWorldFolders(String targetPack, Map> worldDatapackFoldersByPack) { - KList resolved = new KList<>(); - if (worldDatapackFoldersByPack == null || worldDatapackFoldersByPack.isEmpty()) { - return resolved; - } - - String normalizedPack = sanitizePackName(targetPack); - KList direct = worldDatapackFoldersByPack.get(normalizedPack); - if (direct != null) { - for (File file : direct) { - if (file != null && !resolved.contains(file)) { - resolved.add(file); - } - } - return resolved; - } - - KList fallback = worldDatapackFoldersByPack.get(targetPack); - if (fallback != null) { - for (File file : fallback) { - if (file != null && !resolved.contains(file)) { - resolved.add(file); - } - } - } - - return resolved; - } - - private static ProjectionResult projectSourceToWorldDatapacks(File source, SourceDescriptor sourceDescriptor, DatapackRequest request, KList worldDatapackFolders) { - if (source == null || sourceDescriptor == null || request == null) { - return ProjectionResult.failure("", "invalid projection inputs"); - } - - String managedName = buildManagedWorldDatapackName(sourceDescriptor.targetPack(), sourceDescriptor.sourceKey()); - if (worldDatapackFolders == null || worldDatapackFolders.isEmpty()) { - if (request.required()) { - return ProjectionResult.failure(managedName, "no target world datapack folder is available for required external datapack request"); - } - return ProjectionResult.success(managedName, 0, 0, Set.copyOf(request.resolvedLocateStructures()), 0, Set.of(), 0, 0, 0); - } - - String projectionCacheKey = buildProjectionCacheKey(sourceDescriptor.fingerprint(), request); - File projectionCacheDir = Iris.instance.getDataFolder("cache", "projected-datapacks"); - File cachedZip = new File(projectionCacheDir, projectionCacheKey + ".zip"); - File cachedMeta = new File(projectionCacheDir, projectionCacheKey + ".json"); - - if (cachedZip.exists() && cachedZip.length() > 0 && cachedMeta.exists()) { - ProjectionResult cachedResult = restoreCachedProjection(cachedZip, cachedMeta, managedName, worldDatapackFolders); - if (cachedResult != null) { - return cachedResult; - } - } - - ProjectionAssetSummary projectionAssetSummary; - try { - projectionAssetSummary = buildProjectedAssets(source, sourceDescriptor, request); - } catch (Throwable e) { - Iris.warn("Failed to prepare projected external datapack assets from " + sourceDescriptor.sourceName()); - Iris.reportError(e); - return ProjectionResult.failure(managedName, e.getMessage()); - } - - if (projectionAssetSummary.assets().isEmpty()) { - return ProjectionResult.success( - managedName, - 0, - 0, - projectionAssetSummary.resolvedLocateStructures(), - projectionAssetSummary.syntheticStructureSets(), - projectionAssetSummary.projectedStructureKeys(), - projectionAssetSummary.templateAliasesApplied(), - projectionAssetSummary.emptyElementConversions(), - projectionAssetSummary.unresolvedTemplateRefs() - ); - } - - int installedDatapacks = 0; - int installedAssets = 0; - boolean firstWrite = true; - for (File worldDatapackFolder : worldDatapackFolders) { - if (worldDatapackFolder == null) { - continue; - } - - try { - worldDatapackFolder.mkdirs(); - String baseManagedName = managedName.endsWith(".zip") ? managedName.substring(0, managedName.length() - 4) : managedName; - File managedFolder = new File(worldDatapackFolder, baseManagedName); - File managedZip = new File(worldDatapackFolder, managedName); - deleteFolder(managedFolder); - int copiedAssets = writeProjectedAssets(managedZip, projectionAssetSummary.assets()); - if (copiedAssets <= 0) { - if (managedZip.exists() && !managedZip.delete()) { - Iris.verbose("Unable to remove empty managed external datapack zip " + managedZip.getPath() + " (likely locked by the running server)."); - } - continue; - } - installedDatapacks++; - installedAssets += copiedAssets; - - if (firstWrite && managedZip.exists()) { - cacheProjection(managedZip, cachedZip, cachedMeta, projectionAssetSummary); - firstWrite = false; - } - } catch (Throwable e) { - Iris.warn("Failed to project external datapack source " + sourceDescriptor.sourceName() + " into " + worldDatapackFolder.getPath()); - Iris.reportError(e); - return ProjectionResult.failure(managedName, e.getMessage()); - } - } - - return ProjectionResult.success( - managedName, - installedDatapacks, - installedAssets, - projectionAssetSummary.resolvedLocateStructures(), - projectionAssetSummary.syntheticStructureSets(), - projectionAssetSummary.projectedStructureKeys(), - projectionAssetSummary.templateAliasesApplied(), - projectionAssetSummary.emptyElementConversions(), - projectionAssetSummary.unresolvedTemplateRefs() - ); - } - - private static ProjectionAssetSummary buildProjectedAssets(File source, SourceDescriptor sourceDescriptor, DatapackRequest request) throws IOException { - ProjectionSelection projectionSelection = readProjectedEntries(source, request); - - List inputAssets = projectionSelection.assets(); - if (inputAssets.isEmpty()) { - return new ProjectionAssetSummary(List.of(), Set.copyOf(request.resolvedLocateStructures()), 0, Set.of(), 0, 0, 0); - } - - String scopeNamespace = buildScopeNamespace(sourceDescriptor, request); - LinkedHashMap> scopedTagValues = new LinkedHashMap<>(); - LinkedHashSet resolvedLocateStructures = new LinkedHashSet<>(); - resolvedLocateStructures.addAll(request.resolvedLocateStructures()); - LinkedHashSet projectedStructureKeys = new LinkedHashSet<>(); - LinkedHashSet writtenPaths = new LinkedHashSet<>(); - ArrayList outputAssets = new ArrayList<>(); - int projectedCanonicalStructureNbtCount = 0; - - for (ProjectionInputAsset inputAsset : inputAssets) { - ProjectedEntry projectedEntry = inputAsset.entry(); - String outputRelativePath = buildProjectedPath(projectedEntry); - if (outputRelativePath == null || writtenPaths.contains(outputRelativePath)) { - continue; - } - writtenPaths.add(outputRelativePath); - - byte[] outputBytes = inputAsset.bytes(); - if (projectedEntry.type() == ProjectedEntryType.STRUCTURE - || projectedEntry.type() == ProjectedEntryType.STRUCTURE_SET - || projectedEntry.type() == ProjectedEntryType.CONFIGURED_FEATURE - || projectedEntry.type() == ProjectedEntryType.PLACED_FEATURE - || projectedEntry.type() == ProjectedEntryType.TEMPLATE_POOL - || projectedEntry.type() == ProjectedEntryType.PROCESSOR_LIST - || projectedEntry.type() == ProjectedEntryType.BIOME_HAS_STRUCTURE_TAG) { - JSONObject root = new JSONObject(new String(outputBytes, StandardCharsets.UTF_8)); - - if (projectedEntry.type() == ProjectedEntryType.STRUCTURE) { - if (!request.forcedBiomeKeys().isEmpty()) { - String scopeTagKey = scopeNamespace + ":has_structure/" + extractPathFromKey(projectedEntry.key()); - root.put("biomes", "#" + scopeTagKey); - KList values = scopedTagValues.computeIfAbsent(scopeTagKey, key -> new KList<>()); - values.addAll(request.forcedBiomeKeys()); - } - - resolvedLocateStructures.add(projectedEntry.key()); - String normalizedProjectedStructure = normalizeLocateStructure(projectedEntry.key()); - if (!normalizedProjectedStructure.isBlank()) { - projectedStructureKeys.add(normalizedProjectedStructure); - } - } - - outputBytes = root.toString(4).getBytes(StandardCharsets.UTF_8); - } - - outputAssets.add(new ProjectionOutputAsset(outputRelativePath, outputBytes)); - if (projectedEntry.type() == ProjectedEntryType.STRUCTURE_NBT - && outputRelativePath.endsWith(".nbt") - && outputRelativePath.contains("/structure/")) { - projectedCanonicalStructureNbtCount++; - } - } - - for (Map.Entry> scopedEntry : scopedTagValues.entrySet()) { - String tagKey = scopedEntry.getKey(); - String tagPath = "data/" + extractNamespaceFromKey(tagKey) + "/tags/worldgen/biome/" + extractPathFromKey(tagKey) + ".json"; - if (writtenPaths.contains(tagPath)) { - continue; - } - writtenPaths.add(tagPath); - - KList uniqueValues = scopedEntry.getValue().removeDuplicates().sort(); - JSONArray values = new JSONArray(); - for (String value : uniqueValues) { - values.put(value); - } - - JSONObject root = new JSONObject(); - root.put("replace", false); - root.put("values", values); - outputAssets.add(new ProjectionOutputAsset(tagPath, root.toString(4).getBytes(StandardCharsets.UTF_8))); - } - - return new ProjectionAssetSummary( - outputAssets, - Set.copyOf(resolvedLocateStructures), - 0, - Set.copyOf(projectedStructureKeys), - 0, - 0, - 0 - ); - } - - private static ProjectionSelection readProjectedEntries(File source, DatapackRequest request) throws IOException { - if (source.isDirectory()) { - return selectProjectedEntries(readProjectedDirectoryEntries(source), request); - } - if (isArchive(source.getName())) { - return selectProjectedEntries(readProjectedArchiveEntries(source), request); - } - return ProjectionSelection.empty(); - } - - private static List readProjectedDirectoryEntries(File source) throws IOException { - ArrayList assets = new ArrayList<>(); - ArrayDeque queue = new ArrayDeque<>(); - queue.add(source); - while (!queue.isEmpty()) { - File next = queue.removeFirst(); - File[] children = next.listFiles(); - if (children == null) { - continue; - } - - Arrays.sort(children, Comparator.comparing(File::getName, String.CASE_INSENSITIVE_ORDER)); - for (File child : children) { - if (child == null || child.getName().startsWith(".")) { - continue; - } - if (child.isDirectory()) { - queue.add(child); - continue; - } - - String relative = source.toPath().relativize(child.toPath()).toString().replace('\\', '/'); - String normalizedRelative = normalizeRelativePath(relative); - if (normalizedRelative == null) { - continue; - } - - ProjectedEntry projectedEntry = parseProjectedEntry(normalizedRelative); - if (projectedEntry == null) { - continue; - } - - byte[] bytes = Files.readAllBytes(child.toPath()); - assets.add(new ProjectionInputAsset(normalizedRelative, projectedEntry, bytes)); - } - } - return assets; - } - - private static List readProjectedArchiveEntries(File source) throws IOException { - ArrayList assets = new ArrayList<>(); - try (ZipFile zipFile = new ZipFile(source)) { - List entries = zipFile.stream() - .filter(entry -> !entry.isDirectory()) - .sorted(Comparator.comparing(ZipEntry::getName, String.CASE_INSENSITIVE_ORDER)) - .toList(); - - for (ZipEntry zipEntry : entries) { - String normalizedRelative = normalizeRelativePath(zipEntry.getName()); - if (normalizedRelative == null) { - continue; - } - - ProjectedEntry projectedEntry = parseProjectedEntry(normalizedRelative); - if (projectedEntry == null) { - continue; - } - - try (InputStream inputStream = zipFile.getInputStream(zipEntry)) { - byte[] bytes = inputStream.readAllBytes(); - assets.add(new ProjectionInputAsset(normalizedRelative, projectedEntry, bytes)); - } - } - } - return assets; - } - - private static ProjectionSelection selectProjectedEntries(List inputAssets, DatapackRequest request) { - if (inputAssets == null || inputAssets.isEmpty() || request == null) { - return ProjectionSelection.empty(); - } - - ArrayList selected = new ArrayList<>(); - for (ProjectionInputAsset asset : inputAssets) { - if (asset == null) { - continue; - } - - if (shouldProjectEntry(asset.entry(), request)) { - selected.add(asset); - } - } - - return new ProjectionSelection(selected, Set.of(), Set.of(), Set.of()); - } - - private static ProjectionSelection selectReplaceVanillaEntries(List inputAssets, DatapackRequest request) { - EnumMap> allAssets = new EnumMap<>(ProjectedEntryType.class); - EnumMap> closure = new EnumMap<>(ProjectedEntryType.class); - for (ProjectedEntryType type : ProjectedEntryType.values()) { - allAssets.put(type, new LinkedHashMap<>()); - closure.put(type, new LinkedHashSet<>()); - } - - for (ProjectionInputAsset asset : inputAssets) { - if (asset == null || asset.entry() == null) { - continue; - } - - allAssets.get(asset.entry().type()).put(asset.entry().key(), asset); - } - - LinkedHashSet missingSeededTargets = new LinkedHashSet<>(); - LinkedHashSet directResolvedTargets = new LinkedHashSet<>(); - LinkedHashSet aliasResolvedTargets = new LinkedHashSet<>(); - LinkedHashMap> resolvedStructureAliases = new LinkedHashMap<>(); - LinkedHashMap resolvedStructureSetAliases = new LinkedHashMap<>(); - ArrayDeque queue = new ArrayDeque<>(); - enqueueSeedTargetsWithMultiAliases( - request.structures(), - ProjectedEntryType.STRUCTURE, - allAssets, - request.structureAliases(), - resolvedStructureAliases, - directResolvedTargets, - aliasResolvedTargets, - missingSeededTargets, - queue - ); - enqueueSeedTargetsWithSingleAliases( - request.structureSets(), - ProjectedEntryType.STRUCTURE_SET, - allAssets, - request.structureSetAliases(), - resolvedStructureSetAliases, - directResolvedTargets, - aliasResolvedTargets, - missingSeededTargets, - queue - ); - enqueueSeedTargets(request.configuredFeatures(), ProjectedEntryType.CONFIGURED_FEATURE, allAssets, missingSeededTargets, queue); - enqueueSeedTargets(request.placedFeatures(), ProjectedEntryType.PLACED_FEATURE, allAssets, missingSeededTargets, queue); - enqueueSeedTargets(request.templatePools(), ProjectedEntryType.TEMPLATE_POOL, allAssets, missingSeededTargets, queue); - enqueueSeedTargets(request.processorLists(), ProjectedEntryType.PROCESSOR_LIST, allAssets, missingSeededTargets, queue); - enqueueSeedTargets(request.biomeHasStructureTags(), ProjectedEntryType.BIOME_HAS_STRUCTURE_TAG, allAssets, missingSeededTargets, queue); - - while (!queue.isEmpty()) { - ProjectedDependency current = queue.removeFirst(); - if (current == null || current.key() == null || current.key().isBlank()) { - continue; - } - - LinkedHashSet visited = closure.get(current.type()); - if (visited == null || !visited.add(current.key())) { - continue; - } - - ProjectionInputAsset currentAsset = allAssets.get(current.type()).get(current.key()); - if (currentAsset == null) { - continue; - } - - if (current.type() == ProjectedEntryType.STRUCTURE_NBT) { - continue; - } - - if (!isJsonProjectedEntryType(current.type())) { - continue; - } - - try { - JSONObject root = new JSONObject(new String(currentAsset.bytes(), StandardCharsets.UTF_8)); - LinkedHashSet dependencies = new LinkedHashSet<>(); - collectProjectedDependencies(root, current.type(), dependencies); - for (ProjectedDependency dependency : dependencies) { - if (dependency == null || dependency.key() == null || dependency.key().isBlank()) { - continue; - } - if (!allAssets.get(dependency.type()).containsKey(dependency.key())) { - continue; - } - queue.addLast(dependency); - } - } catch (Throwable ignored) { - } - } - - Set externalAliasNamespaces = collectExternalAliasNamespaces(resolvedStructureAliases, resolvedStructureSetAliases); - if (!externalAliasNamespaces.isEmpty()) { - includeNamespaceAssetsInClosure(externalAliasNamespaces, allAssets, closure, ProjectedEntryType.CONFIGURED_FEATURE); - includeNamespaceAssetsInClosure(externalAliasNamespaces, allAssets, closure, ProjectedEntryType.PLACED_FEATURE); - includeNamespaceAssetsInClosure(externalAliasNamespaces, allAssets, closure, ProjectedEntryType.TEMPLATE_POOL); - includeNamespaceAssetsInClosure(externalAliasNamespaces, allAssets, closure, ProjectedEntryType.PROCESSOR_LIST); - includeNamespaceAssetsInClosure(externalAliasNamespaces, allAssets, closure, ProjectedEntryType.BIOME_HAS_STRUCTURE_TAG); - includeNamespaceAssetsInClosure(externalAliasNamespaces, allAssets, closure, ProjectedEntryType.STRUCTURE_NBT); - } - - ArrayList selected = new ArrayList<>(); - for (ProjectionInputAsset asset : inputAssets) { - if (asset == null || asset.entry() == null) { - continue; - } - - LinkedHashSet selectedKeys = closure.get(asset.entry().type()); - if (selectedKeys != null && selectedKeys.contains(asset.entry().key())) { - selected.add(asset); - } - } - - Map sourceToTargetStructureAliases = invertStructureAliasMap(resolvedStructureAliases); - Map sourceStructureWeights = collectAliasedStructureWeights( - resolvedStructureSetAliases, - allAssets.get(ProjectedEntryType.STRUCTURE_SET) - ); - LinkedHashSet usedTemplatePoolKeys = new LinkedHashSet<>(allAssets.get(ProjectedEntryType.TEMPLATE_POOL).keySet()); - for (Map.Entry> aliasEntry : resolvedStructureAliases.entrySet()) { - String targetKey = aliasEntry.getKey(); - List sourceKeys = aliasEntry.getValue(); - if (sourceKeys == null || sourceKeys.isEmpty()) { - missingSeededTargets.add(formatSeededTarget(ProjectedEntryType.STRUCTURE, targetKey)); - continue; - } - - ProjectionInputAsset sourceAsset = allAssets.get(ProjectedEntryType.STRUCTURE).get(sourceKeys.get(0)); - if (sourceAsset == null) { - missingSeededTargets.add(formatSeededTarget(ProjectedEntryType.STRUCTURE, targetKey)); - continue; - } - - MergedStartPoolResult mergedStartPoolResult = synthesizeMergedStartPoolAsset( - targetKey, - sourceKeys, - allAssets.get(ProjectedEntryType.STRUCTURE), - allAssets.get(ProjectedEntryType.TEMPLATE_POOL), - sourceStructureWeights, - usedTemplatePoolKeys - ); - if (mergedStartPoolResult.asset() != null) { - selected.add(mergedStartPoolResult.asset()); - closure.get(ProjectedEntryType.TEMPLATE_POOL).add(mergedStartPoolResult.key()); - } - - ProjectionInputAsset syntheticAsset = synthesizeAliasedStructureAsset(targetKey, sourceAsset, mergedStartPoolResult.key()); - if (syntheticAsset == null) { - missingSeededTargets.add(formatSeededTarget(ProjectedEntryType.STRUCTURE, targetKey)); - continue; - } - - selected.add(syntheticAsset); - closure.get(ProjectedEntryType.STRUCTURE).add(targetKey); - } - - for (Map.Entry aliasEntry : resolvedStructureSetAliases.entrySet()) { - String targetKey = aliasEntry.getKey(); - String sourceKey = aliasEntry.getValue(); - ProjectionInputAsset sourceAsset = allAssets.get(ProjectedEntryType.STRUCTURE_SET).get(sourceKey); - AliasedStructureSetSynthesisResult syntheticResult = synthesizeAliasedStructureSetAsset( - targetKey, - sourceAsset, - sourceToTargetStructureAliases, - request.structures() - ); - if (syntheticResult == null) { - missingSeededTargets.add(formatSeededTarget(ProjectedEntryType.STRUCTURE_SET, targetKey)); - continue; - } - if (!syntheticResult.unmappedStructureKeys().isEmpty()) { - String missing = formatSeededTarget(ProjectedEntryType.STRUCTURE_SET, targetKey) - + " (unmapped structures: " - + summarizeMissingSeededTargets(syntheticResult.unmappedStructureKeys()) - + ")"; - missingSeededTargets.add(missing); - continue; - } - if (syntheticResult.asset() == null) { - missingSeededTargets.add(formatSeededTarget(ProjectedEntryType.STRUCTURE_SET, targetKey)); - continue; - } - - selected.add(syntheticResult.asset()); - closure.get(ProjectedEntryType.STRUCTURE_SET).add(targetKey); - } - - for (String structureTarget : request.structures()) { - if (!closure.get(ProjectedEntryType.STRUCTURE).contains(structureTarget)) { - missingSeededTargets.add(formatSeededTarget(ProjectedEntryType.STRUCTURE, structureTarget)); - } - } - - for (String structureSetTarget : request.structureSets()) { - if (!closure.get(ProjectedEntryType.STRUCTURE_SET).contains(structureSetTarget)) { - missingSeededTargets.add(formatSeededTarget(ProjectedEntryType.STRUCTURE_SET, structureSetTarget)); - } - } - - return new ProjectionSelection( - selected, - Set.copyOf(missingSeededTargets), - Set.copyOf(directResolvedTargets), - Set.copyOf(aliasResolvedTargets) - ); - } - - private static Set collectExternalAliasNamespaces( - Map> resolvedStructureAliases, - Map resolvedStructureSetAliases - ) { - LinkedHashSet namespaces = new LinkedHashSet<>(); - if (resolvedStructureAliases != null && !resolvedStructureAliases.isEmpty()) { - for (List sourceList : resolvedStructureAliases.values()) { - if (sourceList == null || sourceList.isEmpty()) { - continue; - } - for (String sourceKey : sourceList) { - String namespace = extractNamespaceFromKey(sourceKey); - if (namespace == null || namespace.isBlank() || "minecraft".equals(namespace)) { - continue; - } - namespaces.add(namespace); - } - } - } - - if (resolvedStructureSetAliases != null && !resolvedStructureSetAliases.isEmpty()) { - for (String sourceKey : resolvedStructureSetAliases.values()) { - String namespace = extractNamespaceFromKey(sourceKey); - if (namespace == null || namespace.isBlank() || "minecraft".equals(namespace)) { - continue; - } - namespaces.add(namespace); - } - } - - return namespaces; - } - - private static void includeNamespaceAssetsInClosure( - Set namespaces, - Map> allAssets, - Map> closure, - ProjectedEntryType type - ) { - if (namespaces == null || namespaces.isEmpty() || allAssets == null || closure == null || type == null) { - return; - } - - LinkedHashMap typedAssets = allAssets.get(type); - LinkedHashSet typedClosure = closure.get(type); - if (typedAssets == null || typedAssets.isEmpty() || typedClosure == null) { - return; - } - - for (String key : typedAssets.keySet()) { - String namespace = extractNamespaceFromKey(key); - if (namespace == null || namespace.isBlank()) { - continue; - } - if (namespaces.contains(namespace)) { - typedClosure.add(key); - } - } - } - - private static void enqueueSeedTargets( - Set keys, - ProjectedEntryType type, - Map> availableAssets, - Set missingSeededTargets, - ArrayDeque queue - ) { - if (keys == null || keys.isEmpty()) { - return; - } - - LinkedHashMap typedAssets = availableAssets.get(type); - for (String key : keys) { - if (key == null || key.isBlank()) { - continue; - } - - if (typedAssets == null || !typedAssets.containsKey(key)) { - missingSeededTargets.add(formatSeededTarget(type, key)); - continue; - } - - queue.addLast(new ProjectedDependency(type, key)); - } - } - - private static void enqueueSeedTargetsWithMultiAliases( - Set keys, - ProjectedEntryType type, - Map> availableAssets, - Map> aliases, - Map> resolvedAliases, - Set directResolvedTargets, - Set aliasResolvedTargets, - Set missingSeededTargets, - ArrayDeque queue - ) { - if (keys == null || keys.isEmpty()) { - return; - } - - LinkedHashMap typedAssets = availableAssets.get(type); - for (String key : keys) { - if (key == null || key.isBlank()) { - continue; - } - - if (typedAssets != null && typedAssets.containsKey(key)) { - queue.addLast(new ProjectedDependency(type, key)); - directResolvedTargets.add(formatSeededTarget(type, key)); - continue; - } - - List aliasSources = aliases == null ? null : aliases.get(key); - if (aliasSources == null || aliasSources.isEmpty()) { - missingSeededTargets.add(formatSeededTarget(type, key)); - continue; - } - - ArrayList resolvedSources = new ArrayList<>(); - for (String aliasSource : aliasSources) { - if (aliasSource == null || aliasSource.isBlank()) { - continue; - } - if (typedAssets == null || !typedAssets.containsKey(aliasSource)) { - continue; - } - if (!resolvedSources.contains(aliasSource)) { - resolvedSources.add(aliasSource); - queue.addLast(new ProjectedDependency(type, aliasSource)); - } - } - - if (resolvedSources.isEmpty()) { - missingSeededTargets.add(formatSeededTarget(type, key)); - continue; - } - - resolvedAliases.put(key, List.copyOf(resolvedSources)); - aliasResolvedTargets.add(formatSeededTarget(type, key)); - } - } - - private static void enqueueSeedTargetsWithSingleAliases( - Set keys, - ProjectedEntryType type, - Map> availableAssets, - Map aliases, - Map resolvedAliases, - Set directResolvedTargets, - Set aliasResolvedTargets, - Set missingSeededTargets, - ArrayDeque queue - ) { - if (keys == null || keys.isEmpty()) { - return; - } - - LinkedHashMap typedAssets = availableAssets.get(type); - for (String key : keys) { - if (key == null || key.isBlank()) { - continue; - } - - if (typedAssets != null && typedAssets.containsKey(key)) { - queue.addLast(new ProjectedDependency(type, key)); - directResolvedTargets.add(formatSeededTarget(type, key)); - continue; - } - - String aliasedSource = aliases == null ? null : aliases.get(key); - if (aliasedSource == null || aliasedSource.isBlank()) { - missingSeededTargets.add(formatSeededTarget(type, key)); - continue; - } - - if (typedAssets == null || !typedAssets.containsKey(aliasedSource)) { - missingSeededTargets.add(formatSeededTarget(type, key)); - continue; - } - - queue.addLast(new ProjectedDependency(type, aliasedSource)); - resolvedAliases.put(key, aliasedSource); - aliasResolvedTargets.add(formatSeededTarget(type, key)); - } - } - - private static String formatSeededTarget(ProjectedEntryType type, String key) { - return type.name().toLowerCase(Locale.ROOT) + ":" + key; - } - - private static Map invertStructureAliasMap(Map> targetToSourceAlias) { - LinkedHashMap sourceToTarget = new LinkedHashMap<>(); - if (targetToSourceAlias == null || targetToSourceAlias.isEmpty()) { - return sourceToTarget; - } - - for (Map.Entry> entry : targetToSourceAlias.entrySet()) { - String target = entry.getKey(); - List sources = entry.getValue(); - if (target == null || target.isBlank() || sources == null || sources.isEmpty()) { - continue; - } - - for (String source : sources) { - if (source == null || source.isBlank()) { - continue; - } - sourceToTarget.putIfAbsent(source, target); - } - } - - return sourceToTarget; - } - - private static Map collectAliasedStructureWeights( - Map resolvedStructureSetAliases, - Map structureSetAssets - ) { - LinkedHashMap weights = new LinkedHashMap<>(); - if (resolvedStructureSetAliases == null || resolvedStructureSetAliases.isEmpty() || structureSetAssets == null || structureSetAssets.isEmpty()) { - return weights; - } - - for (String sourceStructureSetKey : resolvedStructureSetAliases.values()) { - if (sourceStructureSetKey == null || sourceStructureSetKey.isBlank()) { - continue; - } - - ProjectionInputAsset structureSetAsset = structureSetAssets.get(sourceStructureSetKey); - if (structureSetAsset == null) { - continue; - } - - try { - JSONObject root = new JSONObject(new String(structureSetAsset.bytes(), StandardCharsets.UTF_8)); - JSONArray structures = root.optJSONArray("structures"); - if (structures == null) { - continue; - } - for (int index = 0; index < structures.length(); index++) { - JSONObject structure = structures.optJSONObject(index); - if (structure == null) { - continue; - } - String structureKey = normalizeResourceKey("minecraft", structure.optString("structure", ""), "worldgen/structure/"); - if (structureKey == null || structureKey.isBlank()) { - continue; - } - int weight = Math.max(1, structure.optInt("weight", 1)); - Integer existing = weights.get(structureKey); - if (existing == null) { - weights.put(structureKey, weight); - } else { - long summed = (long) existing + weight; - if (summed > Integer.MAX_VALUE) { - summed = Integer.MAX_VALUE; - } - weights.put(structureKey, (int) summed); - } - } - } catch (Throwable ignored) { - } - } - - return weights; - } - - private static MergedStartPoolResult synthesizeMergedStartPoolAsset( - String targetKey, - List sourceStructureKeys, - Map structureAssets, - Map templatePoolAssets, - Map sourceStructureWeights, - Set usedTemplatePoolKeys - ) { - if (targetKey == null - || targetKey.isBlank() - || sourceStructureKeys == null - || sourceStructureKeys.isEmpty() - || structureAssets == null - || structureAssets.isEmpty() - || templatePoolAssets == null - || templatePoolAssets.isEmpty()) { - return MergedStartPoolResult.empty(); - } - - LinkedHashSet sourceStartPools = new LinkedHashSet<>(); - LinkedHashMap startPoolWeights = new LinkedHashMap<>(); - String fallback = ""; - ArrayList weightedElements = new ArrayList<>(); - long maxRawWeight = 1L; - for (String sourceStructureKey : sourceStructureKeys) { - if (sourceStructureKey == null || sourceStructureKey.isBlank()) { - continue; - } - - ProjectionInputAsset sourceStructureAsset = structureAssets.get(sourceStructureKey); - if (sourceStructureAsset == null) { - continue; - } - - String startPoolKey; - try { - JSONObject sourceStructureRoot = new JSONObject(new String(sourceStructureAsset.bytes(), StandardCharsets.UTF_8)); - startPoolKey = normalizeResourceKey("minecraft", sourceStructureRoot.optString("start_pool", ""), "worldgen/template_pool/"); - } catch (Throwable ignored) { - continue; - } - if (startPoolKey == null || startPoolKey.isBlank()) { - continue; - } - - ProjectionInputAsset templatePoolAsset = templatePoolAssets.get(startPoolKey); - if (templatePoolAsset == null) { - continue; - } - - JSONObject templatePoolRoot; - try { - templatePoolRoot = new JSONObject(new String(templatePoolAsset.bytes(), StandardCharsets.UTF_8)); - } catch (Throwable ignored) { - continue; - } - - JSONArray elements = templatePoolRoot.optJSONArray("elements"); - if (elements == null || elements.length() == 0) { - continue; - } - - if (fallback.isBlank()) { - fallback = templatePoolRoot.optString("fallback", ""); - } - - int sourceWeight = sourceStructureWeights == null - ? 1 - : Math.max(1, sourceStructureWeights.getOrDefault(sourceStructureKey, 1)); - sourceStartPools.add(startPoolKey); - startPoolWeights.put(startPoolKey, sourceWeight); - for (int index = 0; index < elements.length(); index++) { - JSONObject element = elements.optJSONObject(index); - if (element == null) { - continue; - } - JSONObject copied = new JSONObject(element.toString()); - int entryWeight = Math.max(1, copied.optInt("weight", 1)); - long rawWeight = (long) entryWeight * sourceWeight; - if (rawWeight < 1L) { - rawWeight = 1L; - } - if (rawWeight > Integer.MAX_VALUE) { - rawWeight = Integer.MAX_VALUE; - } - if (rawWeight > maxRawWeight) { - maxRawWeight = rawWeight; - } - weightedElements.add(new WeightedTemplatePoolElement(copied, (int) rawWeight)); - } - } - - if (weightedElements.isEmpty() || sourceStartPools.isEmpty()) { - return MergedStartPoolResult.empty(); - } - - JSONArray mergedElements = new JSONArray(); - double scale = maxRawWeight > 150L ? 150.0D / (double) maxRawWeight : 1.0D; - for (WeightedTemplatePoolElement weightedElement : weightedElements) { - JSONObject copied = weightedElement.element(); - int scaledWeight = (int) Math.round((double) weightedElement.weight() * scale); - if (scaledWeight < 1) { - scaledWeight = 1; - } else if (scaledWeight > 150) { - scaledWeight = 150; - } - copied.put("weight", scaledWeight); - mergedElements.put(copied); - } - - String normalizedFallback = normalizeResourceKey("minecraft", fallback, "worldgen/template_pool/"); - if (normalizedFallback == null || normalizedFallback.isBlank()) { - normalizedFallback = "minecraft:empty"; - } - - String basePath = sanitizePath(extractPathFromKey(targetKey)).replace('/', '_'); - if (basePath.isBlank()) { - basePath = "structure"; - } - StringBuilder seedBuilder = new StringBuilder(targetKey); - ArrayList sourceStartPoolList = new ArrayList<>(sourceStartPools); - sourceStartPoolList.sort(String::compareTo); - for (String sourceStartPool : sourceStartPoolList) { - int sourceWeight = startPoolWeights.getOrDefault(sourceStartPool, 1); - seedBuilder.append("|").append(sourceStartPool).append(":").append(sourceWeight); - } - - String namespace = extractNamespaceFromKey(targetKey); - if (namespace.isBlank()) { - namespace = "minecraft"; - } - String baseKey = namespace + ":iris_external/merged_start_pool/" + basePath + "-" + shortHash(seedBuilder.toString()); - String finalKey = baseKey; - int uniqueIndex = 2; - while (usedTemplatePoolKeys != null && usedTemplatePoolKeys.contains(finalKey)) { - finalKey = baseKey + "-" + uniqueIndex; - uniqueIndex++; - } - - JSONObject root = new JSONObject(); - root.put("fallback", normalizedFallback); - root.put("elements", mergedElements); - ProjectedEntry entry = new ProjectedEntry(ProjectedEntryType.TEMPLATE_POOL, extractNamespaceFromKey(finalKey), finalKey); - String relativePath = buildProjectedPath(entry); - if (relativePath == null || relativePath.isBlank()) { - return MergedStartPoolResult.empty(); - } - - if (usedTemplatePoolKeys != null) { - usedTemplatePoolKeys.add(finalKey); - } - - ProjectionInputAsset asset = new ProjectionInputAsset(relativePath, entry, root.toString(4).getBytes(StandardCharsets.UTF_8)); - return new MergedStartPoolResult(finalKey, asset); - } - - private static ProjectionInputAsset synthesizeAliasedStructureAsset(String targetKey, ProjectionInputAsset sourceAsset, String mergedStartPoolKey) { - if (targetKey == null || targetKey.isBlank() || sourceAsset == null) { - return null; - } - - ProjectedEntry entry = new ProjectedEntry(ProjectedEntryType.STRUCTURE, extractNamespaceFromKey(targetKey), targetKey); - String relativePath = buildProjectedPath(entry); - if (relativePath == null || relativePath.isBlank()) { - return null; - } - - byte[] outputBytes; - try { - JSONObject root = new JSONObject(new String(sourceAsset.bytes(), StandardCharsets.UTF_8)); - if (mergedStartPoolKey != null && !mergedStartPoolKey.isBlank()) { - root.put("start_pool", mergedStartPoolKey); - } - String mineshaftBiomeTag = resolveMineshaftBiomeTag(targetKey); - if (!mineshaftBiomeTag.isBlank()) { - root.put("biomes", "#" + mineshaftBiomeTag); - } - outputBytes = root.toString(4).getBytes(StandardCharsets.UTF_8); - } catch (Throwable ignored) { - return null; - } - - return new ProjectionInputAsset(relativePath, entry, outputBytes); - } - - private static String resolveMineshaftBiomeTag(String targetKey) { - String normalizedTargetKey = normalizeLocateStructure(targetKey); - if ("minecraft:mineshaft".equals(normalizedTargetKey)) { - return "minecraft:has_structure/mineshaft"; - } - if ("minecraft:mineshaft_mesa".equals(normalizedTargetKey)) { - return "minecraft:has_structure/mineshaft_mesa"; - } - return ""; - } - - private static AliasedStructureSetSynthesisResult synthesizeAliasedStructureSetAsset( - String targetKey, - ProjectionInputAsset sourceAsset, - Map sourceToTargetStructureAliases, - Set allowedTargetStructures - ) { - if (targetKey == null || targetKey.isBlank() || sourceAsset == null) { - return null; - } - - ProjectedEntry entry = new ProjectedEntry(ProjectedEntryType.STRUCTURE_SET, extractNamespaceFromKey(targetKey), targetKey); - String relativePath = buildProjectedPath(entry); - if (relativePath == null || relativePath.isBlank()) { - return null; - } - - JSONObject root; - try { - root = new JSONObject(new String(sourceAsset.bytes(), StandardCharsets.UTF_8)); - } catch (Throwable ignored) { - return null; - } - - StructureSetRewriteResult rewriteResult = rewriteStructureSetAliasStructures(root, sourceToTargetStructureAliases, allowedTargetStructures); - if (rewriteResult == null) { - return new AliasedStructureSetSynthesisResult(null, Set.of()); - } - - if (!rewriteResult.unmappedStructureKeys().isEmpty()) { - return new AliasedStructureSetSynthesisResult(null, rewriteResult.unmappedStructureKeys()); - } - root.put("structures", rewriteResult.rewrittenStructures()); - byte[] outputBytes = root.toString(4).getBytes(StandardCharsets.UTF_8); - - return new AliasedStructureSetSynthesisResult(new ProjectionInputAsset(relativePath, entry, outputBytes), Set.of()); - } - - private static StructureSetRewriteResult rewriteStructureSetAliasStructures( - JSONObject root, - Map sourceToTargetStructureAliases, - Set allowedTargetStructures - ) { - if (root == null) { - return null; - } - - JSONArray structures = root.optJSONArray("structures"); - if (structures == null) { - return new StructureSetRewriteResult(new JSONArray(), Set.of()); - } - - Map effectiveAliases = sourceToTargetStructureAliases == null ? Map.of() : sourceToTargetStructureAliases; - LinkedHashMap collapsedWeights = new LinkedHashMap<>(); - LinkedHashSet unmappedStructureKeys = new LinkedHashSet<>(); - for (int i = 0; i < structures.length(); i++) { - JSONObject structure = structures.optJSONObject(i); - if (structure == null) { - continue; - } - - String structureKey = structure.optString("structure", ""); - if (structureKey.isBlank()) { - continue; - } - - String normalizedStructureKey = normalizeResourceKey("minecraft", structureKey, "worldgen/structure/"); - if (normalizedStructureKey == null || normalizedStructureKey.isBlank()) { - continue; - } - - String targetKey = effectiveAliases.get(normalizedStructureKey); - if (targetKey == null || targetKey.isBlank()) { - if (allowedTargetStructures != null && allowedTargetStructures.contains(normalizedStructureKey)) { - targetKey = normalizedStructureKey; - } else { - unmappedStructureKeys.add(normalizedStructureKey); - continue; - } - } - - int weight = Math.max(1, structure.optInt("weight", 1)); - Integer existingWeight = collapsedWeights.get(targetKey); - if (existingWeight == null) { - collapsedWeights.put(targetKey, weight); - } else { - long sum = (long) existingWeight + weight; - if (sum > Integer.MAX_VALUE) { - sum = Integer.MAX_VALUE; - } - collapsedWeights.put(targetKey, (int) sum); - } - } - - JSONArray rewritten = new JSONArray(); - for (Map.Entry entry : collapsedWeights.entrySet()) { - JSONObject structure = new JSONObject(); - structure.put("structure", entry.getKey()); - structure.put("weight", entry.getValue()); - rewritten.put(structure); - } - - return new StructureSetRewriteResult(rewritten, Set.copyOf(unmappedStructureKeys)); - } - - private static boolean isJsonProjectedEntryType(ProjectedEntryType type) { - return type == ProjectedEntryType.STRUCTURE - || type == ProjectedEntryType.STRUCTURE_SET - || type == ProjectedEntryType.CONFIGURED_FEATURE - || type == ProjectedEntryType.PLACED_FEATURE - || type == ProjectedEntryType.TEMPLATE_POOL - || type == ProjectedEntryType.PROCESSOR_LIST - || type == ProjectedEntryType.BIOME_HAS_STRUCTURE_TAG; - } - - private static void collectProjectedDependencies(Object node, ProjectedEntryType ownerType, Set dependencies) { - if (node == null) { - return; - } - - if (node instanceof JSONObject object) { - for (String key : object.keySet()) { - collectProjectedDependencies(object.get(key), ownerType, dependencies); - } - return; - } - - if (node instanceof JSONArray array) { - for (int index = 0; index < array.length(); index++) { - collectProjectedDependencies(array.get(index), ownerType, dependencies); - } - return; - } - - if (!(node instanceof String rawValue)) { - return; - } - - String value = rawValue.trim(); - if (value.isBlank()) { - return; - } - - addDependency(ownerType, dependencies, value, ProjectedEntryType.STRUCTURE, "worldgen/structure/"); - addDependency(ownerType, dependencies, value, ProjectedEntryType.STRUCTURE_SET, "worldgen/structure_set/"); - addDependency(ownerType, dependencies, value, ProjectedEntryType.CONFIGURED_FEATURE, "worldgen/configured_feature/"); - addDependency(ownerType, dependencies, value, ProjectedEntryType.PLACED_FEATURE, "worldgen/placed_feature/"); - addDependency(ownerType, dependencies, value, ProjectedEntryType.TEMPLATE_POOL, "worldgen/template_pool/"); - addDependency(ownerType, dependencies, value, ProjectedEntryType.PROCESSOR_LIST, "worldgen/processor_list/"); - addDependency(ownerType, dependencies, value, ProjectedEntryType.BIOME_HAS_STRUCTURE_TAG, - "tags/worldgen/biome/has_structure/", - "worldgen/biome/has_structure/", - "has_structure/"); - addDependency(ownerType, dependencies, value, ProjectedEntryType.STRUCTURE_NBT, "structure/", "structures/"); - } - - private static void addDependency( - ProjectedEntryType ownerType, - Set dependencies, - String value, - ProjectedEntryType dependencyType, - String... prefixes - ) { - String normalized = normalizeResourceKey("minecraft", value, prefixes); - if (normalized == null || normalized.isBlank()) { - return; - } - - if (ownerType == dependencyType && ownerType != ProjectedEntryType.TEMPLATE_POOL) { - return; - } - - dependencies.add(new ProjectedDependency(dependencyType, normalized)); - } - - private static String summarizeMissingSeededTargets(Set missingSeededTargets) { - if (missingSeededTargets == null || missingSeededTargets.isEmpty()) { - return ""; - } - - ArrayList sorted = new ArrayList<>(missingSeededTargets); - sorted.sort(String::compareTo); - if (sorted.size() <= 8) { - return String.join(", ", sorted); - } - - ArrayList limited = new ArrayList<>(sorted.subList(0, 8)); - return String.join(", ", limited) + " +" + (sorted.size() - limited.size()) + " more"; - } - - private static int writeProjectedAssets(File managedZipFile, List assets) throws IOException { - if (assets == null || assets.isEmpty()) { - return 0; - } - - File parent = managedZipFile.getParentFile(); - if (parent != null) { - parent.mkdirs(); - } - - File temp = parent == null - ? new File(managedZipFile.getPath() + ".tmp-" + System.nanoTime()) - : new File(parent, managedZipFile.getName() + ".tmp-" + System.nanoTime()); - int copied; - try { - copied = buildDeterministicManagedZip(temp, assets); - } catch (IOException e) { - deleteQuietly(temp); - throw e; - } - - if (copied <= 0) { - deleteQuietly(temp); - return 0; - } - - try { - if (managedZipFile.exists() - && managedZipFile.length() == temp.length() - && Files.mismatch(temp.toPath(), managedZipFile.toPath()) == -1L) { - deleteQuietly(temp); - return copied; - } - } catch (IOException compareFailure) { - Iris.verbose("Managed external datapack zip equality check failed (" + compareFailure.getMessage() + "); attempting replacement."); - } - - try { - Files.move(temp.toPath(), managedZipFile.toPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); - return copied; - } catch (IOException atomicFailure) { - try { - Files.move(temp.toPath(), managedZipFile.toPath(), StandardCopyOption.REPLACE_EXISTING); - return copied; - } catch (IOException replaceFailure) { - deleteQuietly(temp); - if (managedZipFile.exists()) { - Iris.warn("Managed external datapack " + managedZipFile.getName() - + " is locked by the running server and cannot be replaced in place." - + " The previously installed version remains active; restart the server to apply updated assets."); - return copied; - } - throw replaceFailure; - } - } - } - - private static int buildDeterministicManagedZip(File temp, List assets) throws IOException { - int copied = 0; - try (ZipOutputStream zipOutputStream = new ZipOutputStream(Files.newOutputStream(temp.toPath()))) { - ZipEntry packMetaEntry = new ZipEntry("pack.mcmeta"); - packMetaEntry.setTime(MANAGED_ZIP_ENTRY_TIME); - zipOutputStream.putNextEntry(packMetaEntry); - zipOutputStream.write(buildManagedPackMetaBytes()); - zipOutputStream.closeEntry(); - - for (ProjectionOutputAsset asset : assets) { - if (asset == null || asset.relativePath() == null || asset.bytes() == null) { - continue; - } - - String relativePath = normalizeRelativePath(asset.relativePath()); - if (relativePath == null || relativePath.isBlank()) { - continue; - } - - ZipEntry zipEntry = new ZipEntry(relativePath); - zipEntry.setTime(MANAGED_ZIP_ENTRY_TIME); - zipOutputStream.putNextEntry(zipEntry); - zipOutputStream.write(asset.bytes()); - zipOutputStream.closeEntry(); - copied++; - } - } - return copied; - } - - private static void deleteQuietly(File file) { - if (file == null || !file.exists()) { - return; - } - if (!file.delete()) { - file.deleteOnExit(); - } - } - - private static void writeBytesToFile(byte[] data, File output) throws IOException { - File parent = output.getParentFile(); - if (parent != null) { - parent.mkdirs(); - } - - File temp = parent == null - ? new File(output.getPath() + ".tmp-" + System.nanoTime()) - : new File(parent, output.getName() + ".tmp-" + System.nanoTime()); - Files.write(temp.toPath(), data); - try { - Files.move(temp.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); - } catch (IOException e) { - Files.move(temp.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING); - } - } - - private static Integer getPatchedStartHeightAbsolute(ProjectedEntry projectedEntry, DatapackRequest request) { - if (projectedEntry == null || request == null || projectedEntry.type() != ProjectedEntryType.STRUCTURE) { - return null; - } - return request.structureStartHeights().get(projectedEntry.key()); - } - - private static void rewriteJsonValues(Object root, Map replacements) { - if (root instanceof JSONObject object) { - for (String key : object.keySet()) { - Object value = object.get(key); - Object rewritten = rewriteJsonValue(value, replacements); - if (rewritten != value) { - object.put(key, rewritten); - } - } - return; - } - if (root instanceof JSONArray array) { - for (int i = 0; i < array.length(); i++) { - Object value = array.get(i); - Object rewritten = rewriteJsonValue(value, replacements); - if (rewritten != value) { - array.put(i, rewritten); - } - } - } - } - - private static Object rewriteJsonValue(Object value, Map replacements) { - if (value instanceof JSONObject object) { - rewriteJsonValues(object, replacements); - return object; - } - if (value instanceof JSONArray array) { - rewriteJsonValues(array, replacements); - return array; - } - if (value instanceof String stringValue) { - String replacement = replacements.get(stringValue); - if (replacement != null) { - return replacement; - } - } - return value; - } - - private static TemplateAliasRewriteResult applyTemplateAliasesToTemplatePool(JSONObject root, Map templateAliases) { - if (root == null || templateAliases == null || templateAliases.isEmpty()) { - return TemplateAliasRewriteResult.empty(); - } - - JSONArray elements = root.optJSONArray("elements"); - if (elements == null || elements.length() <= 0) { - return TemplateAliasRewriteResult.empty(); - } - - LinkedHashSet referenced = new LinkedHashSet<>(); - LinkedHashSet rewritten = new LinkedHashSet<>(); - LinkedHashSet unresolved = new LinkedHashSet<>(); - int applied = 0; - int emptyConversions = 0; - - for (int i = 0; i < elements.length(); i++) { - JSONObject poolElement = elements.optJSONObject(i); - if (poolElement == null) { - continue; - } - - JSONObject elementData = poolElement.optJSONObject("element"); - if (elementData == null) { - continue; - } - - String location = elementData.optString("location", ""); - if (location.isBlank()) { - continue; - } - - String normalizedLocation = normalizeResourceKey("minecraft", location, "structure/", "structures/"); - if (normalizedLocation == null || normalizedLocation.isBlank()) { - continue; - } - - String aliasTarget = templateAliases.get(normalizedLocation); - if (aliasTarget == null || aliasTarget.isBlank()) { - continue; - } - - referenced.add(normalizedLocation); - try { - if ("minecraft:empty".equalsIgnoreCase(aliasTarget)) { - JSONObject emptyElement = new JSONObject(); - emptyElement.put("element_type", "minecraft:empty_pool_element"); - poolElement.put("element", emptyElement); - emptyConversions++; - } else { - elementData.put("location", aliasTarget); - poolElement.put("element", elementData); - } - applied++; - rewritten.add(normalizedLocation); - } catch (Throwable ignored) { - unresolved.add(normalizedLocation); - } - } - - for (String reference : referenced) { - if (!rewritten.contains(reference)) { - unresolved.add(reference); - } - } - - return new TemplateAliasRewriteResult(applied, emptyConversions, unresolved); - } - - private static Set readStructureSetReferences(JSONObject root) { - LinkedHashSet references = new LinkedHashSet<>(); - if (root == null) { - return references; - } - - JSONArray structures = root.optJSONArray("structures"); - if (structures == null) { - return references; - } - - for (int i = 0; i < structures.length(); i++) { - JSONObject structure = structures.optJSONObject(i); - if (structure == null) { - continue; - } - String structureKey = structure.optString("structure", ""); - if (structureKey.isBlank()) { - continue; - } - String normalizedStructure = normalizeResourceKey("minecraft", structureKey, "worldgen/structure/"); - if (normalizedStructure == null || normalizedStructure.isBlank()) { - continue; - } - references.add(normalizedStructure); - } - return references; - } - - private static SyntheticStructureSetResult synthesizeMissingStructureSets( - Set remappedStructureKeys, - Set structureSetReferences, - Map remappedKeys, - String scopeNamespace, - Set writtenPaths - ) { - if (remappedStructureKeys == null || remappedStructureKeys.isEmpty()) { - return SyntheticStructureSetResult.empty(); - } - - LinkedHashMap reverseRemap = new LinkedHashMap<>(); - for (Map.Entry entry : remappedKeys.entrySet()) { - reverseRemap.put(entry.getValue(), entry.getKey()); - } - - KMap vanillaPlacements = VANILLA_STRUCTURE_PLACEMENTS.aquire(() -> INMS.get().collectStructures()); - if (vanillaPlacements == null || vanillaPlacements.isEmpty()) { - return SyntheticStructureSetResult.empty(); - } - - ArrayList assets = new ArrayList<>(); - int synthesized = 0; - for (String remappedStructure : remappedStructureKeys) { - if (structureSetReferences.contains(remappedStructure)) { - continue; - } - - String originalStructure = reverseRemap.get(remappedStructure); - if (originalStructure == null || originalStructure.isBlank()) { - continue; - } - - SyntheticPlacement syntheticPlacement = findSyntheticPlacement(vanillaPlacements, originalStructure); - if (syntheticPlacement == null) { - Iris.warn("Unable to synthesize structure set for remapped structure " + remappedStructure + " (no vanilla placement found)."); - continue; - } - - JSONObject structureSetRoot = buildSyntheticStructureSetJson(syntheticPlacement, remappedStructure); - if (structureSetRoot == null) { - Iris.warn("Unable to synthesize structure set for remapped structure " + remappedStructure + " (unsupported placement type)."); - continue; - } - - String structurePath = sanitizePath(extractPathFromKey(remappedStructure)); - if (structurePath.isBlank()) { - structurePath = "structure"; - } - String syntheticSetKey = scopeNamespace + ":generated/" + structurePath.replace('/', '_'); - String syntheticPath = buildProjectedPath(new ProjectedEntry(ProjectedEntryType.STRUCTURE_SET, scopeNamespace, syntheticSetKey)); - if (syntheticPath == null) { - continue; - } - if (writtenPaths.contains(syntheticPath)) { - syntheticSetKey = syntheticSetKey + "-" + shortHash(remappedStructure + "|" + syntheticPlacement.structureSetKey()); - syntheticPath = buildProjectedPath(new ProjectedEntry(ProjectedEntryType.STRUCTURE_SET, scopeNamespace, syntheticSetKey)); - } - if (syntheticPath == null || writtenPaths.contains(syntheticPath)) { - continue; - } - - writtenPaths.add(syntheticPath); - structureSetReferences.add(remappedStructure); - assets.add(new ProjectionOutputAsset(syntheticPath, structureSetRoot.toString(4).getBytes(StandardCharsets.UTF_8))); - synthesized++; - } - - return new SyntheticStructureSetResult(assets, synthesized); - } - - private static SyntheticPlacement findSyntheticPlacement(KMap vanillaPlacements, String structureKey) { - if (vanillaPlacements == null || vanillaPlacements.isEmpty() || structureKey == null || structureKey.isBlank()) { - return null; - } - - for (Map.Entry entry : vanillaPlacements.entrySet()) { - StructurePlacement placement = entry.getValue(); - if (placement == null || placement.structures() == null || placement.structures().isEmpty()) { - continue; - } - - for (StructurePlacement.Structure structure : placement.structures()) { - if (structure == null || structure.key() == null || structure.key().isBlank()) { - continue; - } - if (!structureKey.equalsIgnoreCase(structure.key())) { - continue; - } - int weight = structure.weight() > 0 ? structure.weight() : 1; - return new SyntheticPlacement(entry.getKey(), placement, weight); - } - } - - return null; - } - - private static JSONObject buildSyntheticStructureSetJson(SyntheticPlacement placement, String structureKey) { - if (placement == null || placement.placement() == null || structureKey == null || structureKey.isBlank()) { - return null; - } - - JSONObject root = new JSONObject(); - JSONArray structures = new JSONArray(); - JSONObject structure = new JSONObject(); - structure.put("structure", structureKey); - structure.put("weight", placement.weight()); - structures.put(structure); - root.put("structures", structures); - - StructurePlacement structurePlacement = placement.placement(); - JSONObject placementJson = new JSONObject(); - placementJson.put("salt", structurePlacement.salt()); - if (structurePlacement instanceof StructurePlacement.RandomSpread randomSpread) { - placementJson.put("type", "minecraft:random_spread"); - placementJson.put("spacing", randomSpread.spacing()); - placementJson.put("separation", randomSpread.separation()); - if (randomSpread.spreadType() != null) { - placementJson.put("spread_type", randomSpread.spreadType().name().toLowerCase(Locale.ROOT)); - } - float frequency = structurePlacement.frequency(); - if (frequency > 0F && frequency < 0.999999F) { - placementJson.put("frequency", frequency); - placementJson.put("frequency_reduction_method", "default"); - } - } else if (structurePlacement instanceof StructurePlacement.ConcentricRings concentricRings) { - placementJson.put("type", "minecraft:concentric_rings"); - placementJson.put("distance", concentricRings.distance()); - placementJson.put("spread", concentricRings.spread()); - placementJson.put("count", concentricRings.count()); - } else { - return null; - } - - root.put("placement", placementJson); - return root; - } - - private static String buildScopeNamespace(SourceDescriptor sourceDescriptor, DatapackRequest request) { - String base = shortHash(sourceDescriptor.sourceKey() + "|" + request.id() + "|" + request.scopeKey()); - return "iris_external_" + base; - } - - private static String buildProjectedPath(ProjectedEntry entry) { - if (entry == null || entry.key() == null || entry.key().isBlank()) { - return null; - } - - String namespace = extractNamespaceFromKey(entry.key()); - String path = extractPathFromKey(entry.key()); - if (namespace.isBlank() || path.isBlank()) { - return null; - } - - return switch (entry.type()) { - case STRUCTURE -> "data/" + namespace + "/worldgen/structure/" + path + ".json"; - case STRUCTURE_SET -> "data/" + namespace + "/worldgen/structure_set/" + path + ".json"; - case CONFIGURED_FEATURE -> "data/" + namespace + "/worldgen/configured_feature/" + path + ".json"; - case PLACED_FEATURE -> "data/" + namespace + "/worldgen/placed_feature/" + path + ".json"; - case TEMPLATE_POOL -> "data/" + namespace + "/worldgen/template_pool/" + path + ".json"; - case PROCESSOR_LIST -> "data/" + namespace + "/worldgen/processor_list/" + path + ".json"; - case BIOME_HAS_STRUCTURE_TAG -> "data/" + namespace + "/tags/worldgen/biome/has_structure/" + path + ".json"; - case STRUCTURE_NBT -> "data/" + namespace + "/structure/" + path + ".nbt"; - }; - } - - private static String extractNamespaceFromKey(String key) { - if (key == null || key.isBlank()) { - return ""; - } - int colon = key.indexOf(':'); - if (colon <= 0) { - return ""; - } - return sanitizePath(key.substring(0, colon)); - } - - private static String extractPathFromKey(String key) { - if (key == null || key.isBlank()) { - return ""; - } - int colon = key.indexOf(':'); - if (colon < 0 || colon + 1 >= key.length()) { - return sanitizePath(key); - } - return sanitizePath(key.substring(colon + 1)); - } - - private static void writeInputStreamToFile(InputStream inputStream, File output) throws IOException { - File parent = output.getParentFile(); - if (parent != null) { - parent.mkdirs(); - } - - File temp = parent == null - ? new File(output.getPath() + ".tmp-" + System.nanoTime()) - : new File(parent, output.getName() + ".tmp-" + System.nanoTime()); - Files.copy(inputStream, temp.toPath(), StandardCopyOption.REPLACE_EXISTING); - try { - Files.move(temp.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); - } catch (IOException e) { - Files.move(temp.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING); - } - } - - private static byte[] buildManagedPackMetaBytes() { - int packFormat = INMS.get().getDataVersion().getPackFormat(); - JSONObject root = new JSONObject(); - JSONObject pack = new JSONObject(); - pack.put("description", MANAGED_PACK_META_DESCRIPTION); - pack.put("pack_format", packFormat); - root.put("pack", pack); - return root.toString(4).getBytes(StandardCharsets.UTF_8); - } - - private static boolean shouldProjectEntry(ProjectedEntry entry, DatapackRequest request) { - if (entry == null) { - return false; - } - - if (!"minecraft".equals(entry.namespace())) { - return true; - } - - return request.replaceVanilla(); - } - - private static ProjectedEntry parseProjectedEntry(String relativePath) { - String normalized = relativePath.replace('\\', '/'); - Matcher matcher = STRUCTURE_JSON_ENTRY.matcher(normalized); - if (matcher.matches()) { - String key = normalizeResourceKey(matcher.group(1), matcher.group(2), "worldgen/structure/"); - return key == null ? null : new ProjectedEntry(ProjectedEntryType.STRUCTURE, normalizeNamespace(matcher.group(1)), key); - } - - matcher = STRUCTURE_SET_JSON_ENTRY.matcher(normalized); - if (matcher.matches()) { - String key = normalizeResourceKey(matcher.group(1), matcher.group(2), "worldgen/structure_set/"); - return key == null ? null : new ProjectedEntry(ProjectedEntryType.STRUCTURE_SET, normalizeNamespace(matcher.group(1)), key); - } - - matcher = CONFIGURED_FEATURE_JSON_ENTRY.matcher(normalized); - if (matcher.matches()) { - String key = normalizeResourceKey(matcher.group(1), matcher.group(2), "worldgen/configured_feature/"); - return key == null ? null : new ProjectedEntry(ProjectedEntryType.CONFIGURED_FEATURE, normalizeNamespace(matcher.group(1)), key); - } - - matcher = PLACED_FEATURE_JSON_ENTRY.matcher(normalized); - if (matcher.matches()) { - String key = normalizeResourceKey(matcher.group(1), matcher.group(2), "worldgen/placed_feature/"); - return key == null ? null : new ProjectedEntry(ProjectedEntryType.PLACED_FEATURE, normalizeNamespace(matcher.group(1)), key); - } - - matcher = TEMPLATE_POOL_JSON_ENTRY.matcher(normalized); - if (matcher.matches()) { - String key = normalizeResourceKey(matcher.group(1), matcher.group(2), "worldgen/template_pool/"); - return key == null ? null : new ProjectedEntry(ProjectedEntryType.TEMPLATE_POOL, normalizeNamespace(matcher.group(1)), key); - } - - matcher = PROCESSOR_LIST_JSON_ENTRY.matcher(normalized); - if (matcher.matches()) { - String key = normalizeResourceKey(matcher.group(1), matcher.group(2), "worldgen/processor_list/"); - return key == null ? null : new ProjectedEntry(ProjectedEntryType.PROCESSOR_LIST, normalizeNamespace(matcher.group(1)), key); - } - - matcher = BIOME_HAS_STRUCTURE_TAG_ENTRY.matcher(normalized); - if (matcher.matches()) { - String key = normalizeResourceKey(matcher.group(1), matcher.group(2), "tags/worldgen/biome/has_structure/", "worldgen/biome/has_structure/", "has_structure/"); - return key == null ? null : new ProjectedEntry(ProjectedEntryType.BIOME_HAS_STRUCTURE_TAG, normalizeNamespace(matcher.group(1)), key); - } - - EntryPath entryPath = resolveEntryPath(normalized); - if (entryPath == null) { - return null; - } - String key = normalizeResourceKey(entryPath.namespace, stripExtension(entryPath.structurePath)); - return key == null ? null : new ProjectedEntry(ProjectedEntryType.STRUCTURE_NBT, normalizeNamespace(entryPath.namespace), key); - } - - private static String normalizeRelativePath(String value) { - if (value == null || value.isBlank()) { - return null; - } - String normalized = value.replace('\\', '/').replaceAll("/+", "/"); - normalized = normalized.replaceAll("^/+", "").replaceAll("/+$", ""); - if (normalized.isBlank() || normalized.contains("..")) { - return null; - } - return normalized; - } - - private static String buildManagedWorldDatapackName(String targetPack, String sourceKey) { - String pack = sanitizePackName(targetPack); - String source = sanitizePath(sourceKey).replace("/", "_"); - if (pack.isBlank()) { - pack = "pack"; - } - if (source.isBlank()) { - source = "source"; - } - return MANAGED_WORLD_PACK_PREFIX + pack + "-" + source + ".zip"; - } - - private static String normalizeNamespace(String namespace) { - String cleaned = sanitizePath(namespace); - return cleaned.isBlank() ? "minecraft" : cleaned; - } - - private static String normalizeResourceKey(String namespace, String value, String... prefixes) { - String normalizedNamespace = normalizeNamespace(namespace); - if (value == null || value.isBlank()) { - return null; - } - - String cleaned = value.trim().replace('\\', '/'); - if (cleaned.startsWith("#")) { - cleaned = cleaned.substring(1); - } - if (cleaned.isBlank()) { - return null; - } - - String keyNamespace = normalizedNamespace; - String path = cleaned; - int colon = cleaned.indexOf(':'); - if (colon > 0 && colon < cleaned.length() - 1) { - keyNamespace = normalizeNamespace(cleaned.substring(0, colon)); - path = cleaned.substring(colon + 1); - } - - path = sanitizePath(path); - if (path.isBlank()) { - return null; - } - - if (prefixes != null) { - for (String prefix : prefixes) { - String cleanedPrefix = sanitizePath(prefix); - if (!cleanedPrefix.isBlank() && path.startsWith(cleanedPrefix)) { - path = path.substring(cleanedPrefix.length()); - } - } - } - - path = path.replaceAll("^/+", "").replaceAll("/+$", ""); - if (path.endsWith(".json")) { - path = stripExtension(path); - } - if (path.endsWith(".nbt")) { - path = stripExtension(path); - } - if (path.isBlank()) { - return null; - } - - return keyNamespace + ":" + path; - } - - private static String normalizeBiomeKey(String value) { - if (value == null || value.isBlank()) { - return null; - } - - String cleaned = value.trim().replace('\\', '/'); - if (cleaned.startsWith("#")) { - cleaned = cleaned.substring(1); - } - if (cleaned.isBlank()) { - return null; - } - - return normalizeResourceKey("minecraft", cleaned, "worldgen/biome/"); - } - - private static void collectWorldDatapackFolders(Set folders) { - try { - File container = Bukkit.getWorldContainer(); - if (container == null || !container.exists() || !container.isDirectory()) { - return; - } - - File rootDatapacks = new File(container, "datapacks"); - if (rootDatapacks.exists() && rootDatapacks.isDirectory()) { - folders.add(rootDatapacks); - } - - File[] children = container.listFiles(File::isDirectory); - if (children == null || children.length == 0) { - return; - } - - for (File child : children) { - File datapacks = new File(child, "datapacks"); - if (datapacks.exists() && datapacks.isDirectory()) { - folders.add(datapacks); - } - } - } catch (Throwable e) { - Iris.reportError(e); - } - } - - private static String defaultTargetPack() { - String configured = sanitizePackName(IrisSettings.get().getGenerator().getDefaultWorldType()); - if (!configured.isEmpty()) { - return configured; - } - return PACK_NAME; - } - - private static String sanitizePackName(String value) { - String cleaned = sanitizePath(value).replace("/", "_"); - if (cleaned.contains("..")) { - cleaned = cleaned.replace("..", "_"); - } - return cleaned; - } - - private static String normalizeEnvironment(String value) { - if (value == null) { - return null; - } - - String normalized = value.trim().toUpperCase(Locale.ROOT).replace('-', '_'); - if (normalized.isEmpty()) { - return null; - } - - return switch (normalized) { - case "NORMAL", "OVERWORLD" -> "OVERWORLD"; - case "NETHER", "THE_NETHER" -> "NETHER"; - case "END", "THE_END" -> "THE_END"; - default -> null; - }; - } - - private static boolean looksLikeDatapackDirectory(File directory) { - if (directory == null || !directory.isDirectory()) { - return false; - } - - File packMeta = new File(directory, "pack.mcmeta"); - if (packMeta.exists() && packMeta.isFile()) { - return true; - } - - File dataFolder = new File(directory, "data"); - return dataFolder.exists() && dataFolder.isDirectory(); - } - - private static String resolvePackEnvironment(String targetPack) { - String pack = sanitizePackName(targetPack); - if (pack.isEmpty()) { - return null; - } - - return PACK_ENVIRONMENT_CACHE.computeIfAbsent(pack, ExternalDataPackPipeline::resolvePackEnvironmentInternal); - } - - private static String resolvePackEnvironmentInternal(String targetPack) { - try { - File packFolder = Iris.instance.getDataFolder("packs", targetPack); - if (!packFolder.exists() || !packFolder.isDirectory()) { - return null; - } - - IrisData data = IrisData.get(packFolder); - IrisDimension dimension = data.getDimensionLoader().load(targetPack, false); - if (dimension == null) { - String[] keys = data.getDimensionLoader().getPossibleKeys(); - if (keys.length > 0) { - dimension = data.getDimensionLoader().load(keys[0], false); - } - } - if (dimension == null) { - return null; - } - - World.Environment environment = dimension.getEnvironment(); - if (environment == null) { - return "OVERWORLD"; - } - - return normalizeEnvironment(environment.name()); - } catch (Throwable e) { - Iris.reportError(e); - return null; - } - } - - private static File resolveSourceRoot(String targetPack, String objectRootKey) { - String pack = sanitizePackName(targetPack); - if (pack.isEmpty()) { - pack = defaultTargetPack(); - } - String normalizedObjectRootKey = normalizeObjectRootKey(objectRootKey); - if (normalizedObjectRootKey.isEmpty()) { - normalizedObjectRootKey = "external-datapack"; - } - return new File(Iris.instance.getDataFolder("packs", pack), "objects/" + normalizedObjectRootKey); - } - - private static File resolveLegacySourceRoot(String targetPack, String sourceKey) { - String pack = sanitizePackName(targetPack); - if (pack.isEmpty()) { - pack = defaultTargetPack(); - } - return new File(Iris.instance.getDataFolder("packs", pack), "objects/" + IMPORT_PREFIX + "/" + sourceKey); - } - - private static SourceDescriptor createSourceDescriptor(File entry, String requestId, String targetPack, String requiredEnvironment) { - String base = entry.getName(); - String sanitized = sanitizePath(stripExtension(base)); - if (sanitized.isEmpty()) { - sanitized = "source"; - } - String objectRootKey = normalizeObjectRootKey(requestId); - String sourceHash = shortHash(entry.getAbsolutePath() + "|" + objectRootKey); - String sourceKey = objectRootKey + "-" + sanitized + "-" + sourceHash; - String fingerprint = entry.isFile() - ? "file:" + entry.length() + ":" + entry.lastModified() - : "dir:" + directoryFingerprint(entry); - String pack = sanitizePackName(targetPack); - if (pack.isEmpty()) { - pack = defaultTargetPack(); - } - return new SourceDescriptor(sourceKey, base, fingerprint, pack, normalizeEnvironment(requiredEnvironment), objectRootKey); - } - - private static String directoryFingerprint(File directory) { - long files = 0L; - long size = 0L; - long latest = 0L; - ArrayDeque queue = new ArrayDeque<>(); - queue.add(directory); - while (!queue.isEmpty()) { - File next = queue.removeFirst(); - File[] children = next.listFiles(); - if (children == null) { - continue; - } - for (File child : children) { - if (child == null || child.getName().startsWith(".")) { - continue; - } - if (child.isDirectory()) { - queue.add(child); - continue; - } - files++; - size += child.length(); - latest = Math.max(latest, child.lastModified()); - } - } - return files + ":" + size + ":" + latest; - } - - private static JSONObject convertSource(File entry, SourceDescriptor sourceDescriptor, File sourceRoot, String requestId) { - SourceConversion conversion = new SourceConversion( - sourceDescriptor.sourceKey(), - sourceDescriptor.sourceName(), - sourceDescriptor.targetPack(), - sourceDescriptor.requiredEnvironment(), - sourceDescriptor.objectRootKey(), - requestId - ); - if (entry.isDirectory()) { - convertDirectory(entry, conversion, sourceRoot); - } else { - convertArchive(entry, conversion, sourceRoot); - } - return conversion.toJson(sourceDescriptor.fingerprint()); - } - - private static void convertDirectory(File source, SourceConversion conversion, File sourceRoot) { - ExecutorService executorService = Executors.newFixedThreadPool(IMPORT_PARALLELISM); - ExecutorCompletionService completionService = new ExecutorCompletionService<>(executorService); - int inFlight = 0; - ArrayDeque queue = new ArrayDeque<>(); - queue.add(source); - try { - while (!queue.isEmpty()) { - File next = queue.removeFirst(); - File[] children = next.listFiles(); - if (children == null) { - continue; - } - - Arrays.sort(children, Comparator.comparing(File::getName, String.CASE_INSENSITIVE_ORDER)); - for (File child : children) { - if (child == null || child.getName().startsWith(".")) { - continue; - } - if (child.isDirectory()) { - queue.add(child); - continue; - } - if (!child.getName().toLowerCase(Locale.ROOT).endsWith(".nbt")) { - continue; - } - conversion.nbtScanned++; - String relative = source.toPath().relativize(child.toPath()).toString().replace('\\', '/'); - EntryPath entryPath = resolveEntryPath(relative); - if (entryPath == null) { - conversion.skipped++; - continue; - } - - String objectKey = conversion.reserveObjectKey(entryPath.namespace, entryPath.structurePath); - if (objectKey == null) { - conversion.failed++; - continue; - } - - try { - byte[] bytes = Files.readAllBytes(child.toPath()); - completionService.submit(() -> convertNbt(bytes, entryPath, objectKey, sourceRoot)); - inFlight++; - } catch (Throwable e) { - conversion.failed++; - Iris.warn("Failed to convert datapack structure " + relative + " from " + source.getName()); - Iris.reportError(e); - } - - while (inFlight >= MAX_IN_FLIGHT) { - applyResult(conversion, takeResult(completionService)); - inFlight--; - } - } - } - } finally { - while (inFlight > 0) { - applyResult(conversion, takeResult(completionService)); - inFlight--; - } - executorService.shutdown(); - } - } - - private static void convertArchive(File source, SourceConversion conversion, File sourceRoot) { - ExecutorService executorService = Executors.newFixedThreadPool(IMPORT_PARALLELISM); - ExecutorCompletionService completionService = new ExecutorCompletionService<>(executorService); - int inFlight = 0; - try (ZipFile zipFile = new ZipFile(source)) { - List entries = zipFile.stream() - .filter(entry -> !entry.isDirectory()) - .filter(entry -> entry.getName().toLowerCase(Locale.ROOT).endsWith(".nbt")) - .sorted(Comparator.comparing(ZipEntry::getName, String.CASE_INSENSITIVE_ORDER)) - .toList(); - - for (ZipEntry zipEntry : entries) { - conversion.nbtScanned++; - EntryPath entryPath = resolveEntryPath(zipEntry.getName()); - if (entryPath == null) { - conversion.skipped++; - continue; - } - - String objectKey = conversion.reserveObjectKey(entryPath.namespace, entryPath.structurePath); - if (objectKey == null) { - conversion.failed++; - continue; - } - - try (InputStream inputStream = zipFile.getInputStream(zipEntry); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { - inputStream.transferTo(outputStream); - byte[] bytes = outputStream.toByteArray(); - completionService.submit(() -> convertNbt(bytes, entryPath, objectKey, sourceRoot)); - inFlight++; - } catch (Throwable e) { - conversion.failed++; - Iris.warn("Failed to convert datapack structure " + zipEntry.getName() + " from " + source.getName()); - Iris.reportError(e); - } - - while (inFlight >= MAX_IN_FLIGHT) { - applyResult(conversion, takeResult(completionService)); - inFlight--; - } - } - } catch (Throwable e) { - conversion.failed++; - Iris.warn("Failed to read datapack archive " + source.getName()); - Iris.reportError(e); - } finally { - while (inFlight > 0) { - applyResult(conversion, takeResult(completionService)); - inFlight--; - } - executorService.shutdown(); - } - } - - private static ConversionResult takeResult(ExecutorCompletionService completionService) { - try { - Future future = completionService.take(); - return future.get(); - } catch (Throwable e) { - Iris.reportError(e); - return ConversionResult.failed(); - } - } - - private static void applyResult(SourceConversion conversion, ConversionResult result) { - if (result.skipped) { - conversion.skipped++; - return; - } - - if (!result.success) { - conversion.failed++; - return; - } - - conversion.converted++; - conversion.blockEntities += result.blockEntities; - if (result.entitiesIgnored) { - conversion.entitiesIgnored++; - } - if (result.record != null) { - conversion.objects.put(result.record); - } - } - - private static ConversionResult convertNbt(byte[] bytes, EntryPath entryPath, String objectKey, File sourceRoot) throws IOException { - NamedTag namedTag = readNamedTag(bytes); - Tag rootTag = namedTag.getTag(); - if (!(rootTag instanceof CompoundTag compoundTag)) { - return ConversionResult.failed(); - } - - if (isEmptyStructure(compoundTag)) { - return ConversionResult.skipped(); - } - - IrisObject object = toObject(compoundTag); - if (object == null) { - return ConversionResult.failed(); - } - - String relative = objectKey; - int slash = relative.indexOf('/'); - if (slash <= 0 || slash + 1 >= relative.length()) { - return ConversionResult.failed(); - } - relative = relative.substring(slash + 1); - File output = new File(sourceRoot, relative + ".iob"); - File parent = output.getParentFile(); - if (parent != null) { - parent.mkdirs(); - } - - object.write(output); - - ListTag entities = compoundTag.getListTag("entities"); - boolean hasEntities = entities != null && !entities.getValue().isEmpty(); - - JSONObject record = new JSONObject(); - record.put("sourcePath", entryPath.originalPath); - record.put("structureId", entryPath.namespace.toLowerCase(Locale.ROOT) + ":" + stripExtension(entryPath.structurePath)); - record.put("objectKey", objectKey); - record.put("entitiesIgnored", hasEntities); - return ConversionResult.success(record, object.getStates().size(), hasEntities); - } - - private static boolean isEmptyStructure(CompoundTag root) { - ListTag sizeList = root.getListTag("size"); - if (sizeList == null || sizeList.size() < 3) { - return false; - } - - Integer width = tagToInt(sizeList.get(0)); - Integer height = tagToInt(sizeList.get(1)); - Integer depth = tagToInt(sizeList.get(2)); - if (width == null || height == null || depth == null) { - return false; - } - - if (width != 0 || height != 0 || depth != 0) { - return false; - } - - ListTag blocksTag = root.getListTag("blocks"); - if (blocksTag != null && blocksTag.size() > 0) { - return false; - } - - ListTag paletteTag = root.getListTag("palette"); - return paletteTag == null || paletteTag.size() == 0; - } - - private static IrisObject toObject(CompoundTag root) { - ListTag sizeList = root.getListTag("size"); - if (sizeList == null || sizeList.size() < 3) { - return null; - } - - Integer width = tagToInt(sizeList.get(0)); - Integer height = tagToInt(sizeList.get(1)); - Integer depth = tagToInt(sizeList.get(2)); - if (width == null || height == null || depth == null || width <= 0 || height <= 0 || depth <= 0) { - return null; - } - - ListTag paletteTag = root.getListTag("palette"); - ListTag blocksTag = root.getListTag("blocks"); - if (paletteTag == null || paletteTag.size() == 0 || blocksTag == null) { - return null; - } - - List palette = buildPalette(paletteTag); - IrisObject object = new IrisObject(width, height, depth); - - for (Object blockRaw : blocksTag.getValue()) { - if (!(blockRaw instanceof CompoundTag blockTag)) { - continue; - } - - Integer stateIndex = tagToInt(blockTag.get("state")); - if (stateIndex == null || stateIndex < 0 || stateIndex >= palette.size()) { - continue; - } - - int[] pos = readPos(blockTag.get("pos")); - if (pos == null) { - continue; - } - - int x = pos[0]; - int y = pos[1]; - int z = pos[2]; - if (x < 0 || y < 0 || z < 0 || x >= width || y >= height || z >= depth) { - continue; - } - - BlockData blockData = palette.get(stateIndex); - if (blockData == null) { - blockData = AIR; - } - - if (!B.isAir(blockData)) { - object.setUnsigned(x, y, z, blockData); - } - - CompoundTag tileNbt = blockTag.getCompoundTag("nbt"); - if (tileNbt != null && tileNbt.size() > 0) { - KMap tileData = convertCompound(tileNbt); - if (!tileData.isEmpty()) { - TileData state = new TileData(blockData.getMaterial(), tileData); - object.getStates().put(new Vector3i(x, y, z), state); - } - } - } - - return object; - } - - private static List buildPalette(ListTag paletteTag) { - List palette = new ArrayList<>(paletteTag.size()); - for (Object paletteRaw : paletteTag.getValue()) { - BlockData blockData = AIR; - if (paletteRaw instanceof CompoundTag paletteEntry) { - String name = paletteEntry.getString("Name"); - String blockState = buildBlockState(name, paletteEntry.getCompoundTag("Properties")); - BlockData resolved = resolveBlockData(blockState, name); - blockData = resolved == null ? AIR : resolved; - } - palette.add(blockData); - } - return palette; - } - - private static BlockData resolveBlockData(String blockState, String fallbackName) { - String stateKey = blockState == null ? "" : blockState.toLowerCase(Locale.ROOT); - if (!stateKey.isEmpty()) { - BlockData cached = BLOCK_DATA_CACHE.get(stateKey); - if (cached != null) { - return cached; - } - } - - BlockData resolved = blockState == null || blockState.isBlank() ? null : B.getOrNull(blockState, false); - if (resolved == null && fallbackName != null && !fallbackName.isBlank()) { - String fallbackKey = fallbackName.toLowerCase(Locale.ROOT); - BlockData fallbackCached = BLOCK_DATA_CACHE.get(fallbackKey); - if (fallbackCached != null) { - return fallbackCached; - } - resolved = B.getOrNull(fallbackName, false); - if (resolved != null) { - BLOCK_DATA_CACHE.putIfAbsent(fallbackKey, resolved); - } - } - - if (resolved == null) { - resolved = AIR; - } - - if (!stateKey.isEmpty()) { - BLOCK_DATA_CACHE.putIfAbsent(stateKey, resolved); - } - - return resolved; - } - - private static String buildBlockState(String name, CompoundTag properties) { - String base = name == null ? "minecraft:air" : name; - if (properties == null || properties.size() == 0) { - return base; - } - - List keys = new ArrayList<>(properties.keySet()); - keys.sort(String::compareTo); - StringBuilder builder = new StringBuilder(base).append("["); - for (int i = 0; i < keys.size(); i++) { - String key = keys.get(i); - Tag valueTag = properties.get(key); - if (i > 0) { - builder.append(","); - } - builder.append(key).append("=").append(tagToPropertyValue(valueTag)); - } - builder.append("]"); - return builder.toString(); - } - - private static String tagToPropertyValue(Tag valueTag) { - if (valueTag instanceof StringTag stringTag) { - return stringTag.getValue(); - } - if (valueTag == null) { - return "null"; - } - String value = valueTag.valueToString(); - if (value.length() > 1 && value.charAt(0) == '"' && value.charAt(value.length() - 1) == '"') { - return value.substring(1, value.length() - 1); - } - return value; - } - - private static int[] readPos(Tag posTag) { - if (!(posTag instanceof ListTag listTag) || listTag.size() < 3) { - return null; - } - Integer x = tagToInt(listTag.get(0)); - Integer y = tagToInt(listTag.get(1)); - Integer z = tagToInt(listTag.get(2)); - if (x == null || y == null || z == null) { - return null; - } - return new int[]{x, y, z}; - } - - private static Integer tagToInt(Tag tag) { - if (tag instanceof NumberTag numberTag) { - return numberTag.asInt(); - } - return null; - } - - private static NamedTag readNamedTag(byte[] bytes) throws IOException { - IOException primary = null; - try { - return new NBTDeserializer(false).fromStream(new ByteArrayInputStream(bytes)); - } catch (IOException e) { - primary = e; - } - - try { - return new NBTDeserializer(true).fromStream(new ByteArrayInputStream(bytes)); - } catch (IOException e) { - if (primary != null) { - e.addSuppressed(primary); - } - throw e; - } - } - - private static KMap convertCompound(CompoundTag tag) { - KMap map = new KMap<>(); - for (Map.Entry> entry : tag) { - String key = entry.getKey(); - Object value = convertTag(entry.getValue()); - if (value != null) { - map.put(key, value); - } - } - return map; - } - - private static Object convertTag(Tag tag) { - if (tag == null) { - return null; - } - - if (tag instanceof CompoundTag compoundTag) { - return convertCompound(compoundTag); - } - - if (tag instanceof ListTag listTag) { - KList list = new KList<>(); - for (Object child : listTag.getValue()) { - if (!(child instanceof Tag childTag)) { - continue; - } - Object converted = convertTag(childTag); - if (converted != null) { - list.add(converted); - } - } - return list; - } - - if (tag instanceof ByteArrayTag byteArrayTag) { - KList list = new KList<>(); - for (byte value : byteArrayTag.getValue()) { - list.add(value); - } - return list; - } - - if (tag instanceof IntArrayTag intArrayTag) { - KList list = new KList<>(); - for (int value : intArrayTag.getValue()) { - list.add(value); - } - return list; - } - - if (tag instanceof LongArrayTag longArrayTag) { - KList list = new KList<>(); - for (long value : longArrayTag.getValue()) { - list.add(value); - } - return list; - } - - if (tag instanceof NumberTag numberTag) { - if (tag instanceof ByteTag) { - return numberTag.asByte(); - } - if (tag instanceof ShortTag) { - return numberTag.asShort(); - } - if (tag instanceof IntTag) { - return numberTag.asInt(); - } - if (tag instanceof LongTag) { - return numberTag.asLong(); - } - if (tag instanceof FloatTag) { - return numberTag.asFloat(); - } - if (tag instanceof DoubleTag) { - return numberTag.asDouble(); - } - return numberTag.asDouble(); - } - - if (tag instanceof StringTag stringTag) { - return stringTag.getValue(); - } - - return null; - } - - private static String createUniqueKey(String base, Set used) { - if (used.add(base)) { - return base; - } - int index = 2; - while (true) { - String candidate = base + "-" + index; - if (used.add(candidate)) { - return candidate; - } - index++; - } - } - - private static EntryPath resolveEntryPath(String path) { - if (path == null) { - return null; - } - String normalized = path.replace('\\', '/'); - Matcher matcher = STRUCTURE_ENTRY.matcher(normalized); - if (!matcher.matches()) { - return null; - } - String namespace = matcher.group(1); - String structurePath = matcher.group(2); - if (namespace == null || structurePath == null || namespace.isBlank() || structurePath.isBlank()) { - return null; - } - return new EntryPath(normalized, namespace, structurePath); - } - - private static boolean isArchive(String name) { - String lower = name.toLowerCase(Locale.ROOT); - return lower.endsWith(".zip") || lower.endsWith(".jar"); - } - - private static String sanitizePath(String value) { - if (value == null) { - return ""; - } - String cleaned = value.toLowerCase(Locale.ROOT) - .replace('\\', '/') - .replaceAll("[^a-z0-9_\\-./]", "_") - .replaceAll("/+", "/") - .replaceAll("^/+", "") - .replaceAll("/+$", ""); - if (cleaned.contains("..")) { - cleaned = cleaned.replace("..", "_"); - } - return cleaned; - } - - private static String stripExtension(String name) { - if (name == null) { - return ""; - } - int dot = name.lastIndexOf('.'); - if (dot <= 0) { - return name; - } - return name.substring(0, dot); - } - - private static JSONObject readExistingIndex(File indexFile) { - if (indexFile == null || !indexFile.exists()) { - return new JSONObject(); - } - try { - return new JSONObject(Files.readString(indexFile.toPath(), StandardCharsets.UTF_8)); - } catch (Throwable e) { - Iris.warn("Failed to read datapack index, rebuilding."); - Iris.reportError(e); - return new JSONObject(); - } - } - - private static File resolveImportIndexFile(String targetPack) { - String sanitized = sanitizePackName(targetPack); - if (sanitized.isBlank()) { - sanitized = defaultTargetPack(); - } - return new File(Iris.instance.getDataFolder("packs", sanitized, EXTERNAL_PACK_INDEX_SUBFOLDER), IMPORT_INDEX_FILE_NAME); - } - - private static Map readAllImportIndexes() { - Map byPack = new HashMap<>(); - File packsRoot = Iris.instance.getDataFolder("packs"); - File[] packDirs = packsRoot.listFiles(); - if (packDirs == null) { - return byPack; - } - for (File packDir : packDirs) { - if (packDir == null || !packDir.isDirectory()) { - continue; - } - File importsFolder = new File(packDir, EXTERNAL_PACK_INDEX_SUBFOLDER); - if (!importsFolder.isDirectory()) { - continue; - } - File indexFile = new File(importsFolder, IMPORT_INDEX_FILE_NAME); - if (!indexFile.exists()) { - continue; - } - JSONObject index = readExistingIndex(indexFile); - if (index != null) { - byPack.put(packDir.getName(), index); - } - } - return byPack; - } - - private static Map flattenOldSources(Map oldIndexByPack) { - Map combined = new HashMap<>(); - if (oldIndexByPack == null || oldIndexByPack.isEmpty()) { - return combined; - } - for (JSONObject packIndex : oldIndexByPack.values()) { - Map packSources = mapExistingSources(packIndex); - for (Map.Entry entry : packSources.entrySet()) { - combined.putIfAbsent(entry.getKey(), entry.getValue()); - } - } - return combined; - } - - private static void migrateLegacyImportIndex() { - File legacyFolder = Iris.instance.getDataFolder("packs", EXTERNAL_PACK_INDEX); - if (!legacyFolder.exists()) { - return; - } - try { - File legacyIndex = new File(legacyFolder, IMPORT_INDEX_FILE_NAME); - if (!legacyIndex.exists()) { - deleteFolder(legacyFolder); - return; - } - - JSONObject legacy = readExistingIndex(legacyIndex); - JSONArray sources = legacy.optJSONArray("sources"); - if (sources == null || sources.length() == 0) { - deleteFolder(legacyFolder); - Iris.info("Removed empty legacy datapack-imports folder at packs/" + EXTERNAL_PACK_INDEX + "/"); - return; - } - - Map sourcesByPack = new HashMap<>(); - Map summariesByPack = new HashMap<>(); - int migratedEntries = 0; - for (int i = 0; i < sources.length(); i++) { - JSONObject source = sources.optJSONObject(i); - if (source == null) { - continue; - } - String targetPack = sanitizePackName(source.optString("targetPack", "")); - if (targetPack.isBlank() || EXTERNAL_PACK_INDEX.equals(targetPack)) { - targetPack = defaultTargetPack(); - if (EXTERNAL_PACK_INDEX.equals(targetPack)) { - targetPack = "overworld"; - } - } - sourcesByPack.computeIfAbsent(targetPack, k -> new JSONArray()).put(source); - ImportSummary packSummary = summariesByPack.computeIfAbsent(targetPack, k -> new ImportSummary()); - addSourceToSummary(packSummary, source, true); - migratedEntries++; - } - - for (Map.Entry entry : sourcesByPack.entrySet()) { - File indexFile = resolveImportIndexFile(entry.getKey()); - ImportSummary packSummary = summariesByPack.getOrDefault(entry.getKey(), new ImportSummary()); - writeIndex(indexFile, entry.getValue(), packSummary); - } - - deleteFolder(legacyFolder); - Iris.info("Migrated datapack-imports index from packs/" + EXTERNAL_PACK_INDEX - + "/ into per-pack folders (" + migratedEntries + " entries across " - + sourcesByPack.size() + " pack(s))."); - } catch (Throwable e) { - Iris.warn("Failed to migrate legacy datapack-imports index; leaving legacy folder in place."); - Iris.reportError(e); - } - } - - private static void mergeImportSummaryInto(ImportSummary target, ImportSummary source) { - if (target == null || source == null) { - return; - } - target.sources += source.sources; - target.cachedSources += source.cachedSources; - target.nbtScanned += source.nbtScanned; - target.converted += source.converted; - target.failed += source.failed; - target.skipped += source.skipped; - target.entitiesIgnored += source.entitiesIgnored; - target.blockEntities += source.blockEntities; - } - - private static Map mapExistingSources(JSONObject index) { - Map mapped = new HashMap<>(); - if (index == null) { - return mapped; - } - JSONArray sources = index.optJSONArray("sources"); - if (sources == null) { - return mapped; - } - for (int i = 0; i < sources.length(); i++) { - JSONObject source = sources.optJSONObject(i); - if (source == null) { - continue; - } - String sourceKey = source.optString("sourceKey", ""); - if (!sourceKey.isEmpty()) { - mapped.put(sourceKey, source); - } - } - return mapped; - } - - private static void addSourceToSummary(ImportSummary summary, JSONObject source, boolean cached) { - if (summary == null || source == null) { - return; - } - summary.sources++; - summary.nbtScanned += source.optInt("nbtScanned", 0); - summary.converted += source.optInt("converted", 0); - summary.failed += source.optInt("failed", 0); - summary.skipped += source.optInt("skipped", 0); - summary.entitiesIgnored += source.optInt("entitiesIgnored", 0); - summary.blockEntities += source.optInt("blockEntities", 0); - if (cached) { - summary.cachedSources++; - } - } - - private static void pruneRemovedSourceFolders(Map oldSources, Set activeSourceKeys) { - if (oldSources == null || oldSources.isEmpty()) { - return; - } - - for (Map.Entry entry : oldSources.entrySet()) { - String sourceKey = entry.getKey(); - if (sourceKey == null || sourceKey.isEmpty() || activeSourceKeys.contains(sourceKey)) { - continue; - } - - JSONObject source = entry.getValue(); - String targetPack = defaultTargetPack(); - if (source != null) { - String configuredPack = sanitizePackName(source.optString("targetPack", "")); - if (!configuredPack.isEmpty()) { - targetPack = configuredPack; - } - } - - String objectRootKey = source == null ? "" : normalizeObjectRootKey(source.optString("objectRootKey", "")); - if (!objectRootKey.isBlank()) { - deleteFolder(resolveSourceRoot(targetPack, objectRootKey)); - } - deleteFolder(resolveLegacySourceRoot(targetPack, sourceKey)); - } - } - - private static void writeIndex(File indexFile, JSONArray sources, ImportSummary summary) { - JSONObject totals = new JSONObject(); - totals.put("sources", summary.sources); - totals.put("cachedSources", summary.cachedSources); - totals.put("nbtScanned", summary.nbtScanned); - totals.put("converted", summary.converted); - totals.put("failed", summary.failed); - totals.put("skipped", summary.skipped); - totals.put("entitiesIgnored", summary.entitiesIgnored); - totals.put("blockEntities", summary.blockEntities); - - JSONObject root = new JSONObject(); - root.put("generatedAt", Instant.now().toString()); - root.put("sources", sources); - root.put("totals", totals); - - try { - File parent = indexFile.getParentFile(); - if (parent != null) { - parent.mkdirs(); - } - Files.writeString(indexFile.toPath(), root.toString(4), StandardCharsets.UTF_8); - } catch (Throwable e) { - Iris.warn("Failed to write datapack index " + indexFile.getPath()); - Iris.reportError(e); - } - } - - private static void deleteFolder(File folder) { - if (folder == null || !folder.exists()) { - return; - } - try { - Files.walk(folder.toPath()) - .sorted(Comparator.reverseOrder()) - .map(java.nio.file.Path::toFile) - .forEach(File::delete); - } catch (Throwable e) { - Iris.reportError(e); - } - } - - private static ModrinthFile resolveModrinthFile(String pageUrl) throws IOException { - Matcher matcher = MODRINTH_VERSION_URL.matcher(pageUrl); - if (!matcher.matches()) { - throw new IOException("Unsupported Modrinth URL format: " + pageUrl); - } - - String slug = matcher.group(1); - String version = matcher.group(2); - if (slug == null || version == null) { - throw new IOException("Invalid Modrinth URL: " + pageUrl); - } - - String api = "https://api.modrinth.com/v2/project/" - + URLEncoder.encode(slug, StandardCharsets.UTF_8) - + "/version/" - + URLEncoder.encode(version, StandardCharsets.UTF_8); - JSONObject json = getJson(api); - JSONArray loaders = json.optJSONArray("loaders"); - if (loaders == null || !containsIgnoreCase(loaders, "datapack")) { - throw new IOException("Modrinth version is not a datapack: " + pageUrl); - } - - JSONArray files = json.optJSONArray("files"); - if (files == null || files.length() == 0) { - throw new IOException("No downloadable files in Modrinth version: " + pageUrl); - } - - JSONObject selected = null; - for (int i = 0; i < files.length(); i++) { - JSONObject file = files.optJSONObject(i); - if (file != null && file.optBoolean("primary", false)) { - selected = file; - break; - } - } - - if (selected == null) { - selected = files.optJSONObject(0); - } - if (selected == null) { - throw new IOException("Unable to select datapack file for " + pageUrl); - } - - String fileUrl = selected.optString("url", ""); - String fileName = selected.optString("filename", ""); - if (fileUrl.isEmpty() || fileName.isEmpty()) { - throw new IOException("Invalid file payload for " + pageUrl); - } - - String versionId = json.optString("id", version); - JSONObject hashes = selected.optJSONObject("hashes"); - String sha1 = hashes == null ? null : hashes.optString("sha1", null); - String extension = extension(fileName); - String safeSlug = sanitizePath(slug).replace("/", "_"); - if (safeSlug.isEmpty()) { - safeSlug = "modrinth"; - } - return new ModrinthFile(pageUrl, fileUrl, safeSlug, versionId, extension, sha1); - } - - private static JSONObject getJson(String url) throws IOException { - HttpURLConnection connection = (HttpURLConnection) URI.create(url).toURL().openConnection(); - connection.setConnectTimeout(CONNECT_TIMEOUT_MS); - connection.setReadTimeout(READ_TIMEOUT_MS); - connection.setRequestProperty("User-Agent", "Iris/" + Iris.instance.getDescription().getVersion()); - connection.setRequestProperty("Accept", "application/json"); - int response = connection.getResponseCode(); - if (response < 200 || response >= 300) { - InputStream error = connection.getErrorStream(); - String message = error == null ? "" : new String(error.readAllBytes(), StandardCharsets.UTF_8); - throw new IOException("HTTP " + response + " for " + url + (message.isEmpty() ? "" : " - " + message)); - } - try (InputStream inputStream = connection.getInputStream()) { - String body = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); - return new JSONObject(body); - } - } - - private static void downloadToFile(String url, File output) throws IOException { - File parent = output.getParentFile(); - if (parent != null) { - parent.mkdirs(); - } - File temp = parent == null - ? new File(output.getPath() + ".tmp-" + System.nanoTime()) - : new File(parent, output.getName() + ".tmp-" + System.nanoTime()); - HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); - connection.setConnectTimeout(CONNECT_TIMEOUT_MS); - connection.setReadTimeout(30000); - connection.setRequestProperty("User-Agent", "Iris/" + Iris.instance.getDescription().getVersion()); - connection.setRequestProperty("Accept", "*/*"); - int response = connection.getResponseCode(); - if (response < 200 || response >= 300) { - throw new IOException("HTTP " + response + " for " + url); - } - - try (InputStream inputStream = connection.getInputStream()) { - Files.copy(inputStream, temp.toPath(), StandardCopyOption.REPLACE_EXISTING); - } - - try { - Files.move(temp.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); - } catch (IOException e) { - Files.move(temp.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING); - } - } - - private static void copyFile(File from, File to) throws IOException { - if (from == null || to == null) { - throw new IOException("Invalid copy source/target"); - } - - File parent = to.getParentFile(); - if (parent != null) { - parent.mkdirs(); - } - - File temp = parent == null - ? new File(to.getPath() + ".tmp-" + System.nanoTime()) - : new File(parent, to.getName() + ".tmp-" + System.nanoTime()); - Files.copy(from.toPath(), temp.toPath(), StandardCopyOption.REPLACE_EXISTING); - try { - Files.move(temp.toPath(), to.toPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); - } catch (IOException e) { - Files.move(temp.toPath(), to.toPath(), StandardCopyOption.REPLACE_EXISTING); - } - } - - private static boolean containsIgnoreCase(JSONArray array, String value) { - if (array == null || value == null) { - return false; - } - for (int i = 0; i < array.length(); i++) { - Object raw = array.opt(i); - if (raw == null) { - continue; - } - if (value.equalsIgnoreCase(String.valueOf(raw))) { - return true; - } - } - return false; - } - - private static String extension(String fileName) { - int dot = fileName.lastIndexOf('.'); - if (dot < 0 || dot == fileName.length() - 1) { - return ".zip"; - } - return fileName.substring(dot).toLowerCase(Locale.ROOT); - } - - private static boolean isUpToDate(File output, String expectedSha1) { - if (output == null || !output.exists() || !output.isFile()) { - return false; - } - if (expectedSha1 == null || expectedSha1.isBlank()) { - return true; - } - try { - return expectedSha1.equalsIgnoreCase(sha1Hex(output)); - } catch (Throwable e) { - return false; - } - } - - private static String sha1Hex(File file) throws Exception { - MessageDigest digest = MessageDigest.getInstance("SHA-1"); - try (InputStream inputStream = Files.newInputStream(file.toPath())) { - byte[] buffer = new byte[16384]; - int read; - while ((read = inputStream.read(buffer)) != -1) { - digest.update(buffer, 0, read); - } - } - byte[] bytes = digest.digest(); - StringBuilder builder = new StringBuilder(bytes.length * 2); - for (byte b : bytes) { - builder.append(String.format("%02x", b)); - } - return builder.toString(); - } - - private static String buildProjectionCacheKey(String sourceFingerprint, DatapackRequest request) { - StringBuilder keyBuilder = new StringBuilder(); - keyBuilder.append(sourceFingerprint); - keyBuilder.append('|').append(request.getDedupeKey()); - keyBuilder.append('|').append(request.alongsideMode()); - keyBuilder.append('|').append(request.templateAliases()); - keyBuilder.append('|').append(request.structureStartHeights()); - keyBuilder.append('|').append(request.structureSetAliases()); - keyBuilder.append('|').append(request.structureAliases()); - keyBuilder.append('|').append(request.structures()); - keyBuilder.append('|').append(request.structureSets()); - keyBuilder.append('|').append(request.templatePools()); - keyBuilder.append('|').append(request.processorLists()); - keyBuilder.append('|').append(request.configuredFeatures()); - keyBuilder.append('|').append(request.placedFeatures()); - keyBuilder.append('|').append(request.biomeHasStructureTags()); - try { - MessageDigest digest = MessageDigest.getInstance("SHA-1"); - byte[] bytes = digest.digest(keyBuilder.toString().getBytes(StandardCharsets.UTF_8)); - StringBuilder hex = new StringBuilder(bytes.length * 2); - for (byte b : bytes) { - hex.append(String.format("%02x", b)); - } - return hex.toString(); - } catch (Throwable e) { - return shortHash(keyBuilder.toString()); - } - } - - private static ProjectionResult restoreCachedProjection(File cachedZip, File cachedMeta, String managedName, KList worldDatapackFolders) { - try { - JSONObject meta = new JSONObject(Files.readString(cachedMeta.toPath(), StandardCharsets.UTF_8)); - int installedDatapacks = 0; - int installedAssets = 0; - for (File worldDatapackFolder : worldDatapackFolders) { - if (worldDatapackFolder == null) { - continue; - } - worldDatapackFolder.mkdirs(); - String baseManagedName = managedName.endsWith(".zip") ? managedName.substring(0, managedName.length() - 4) : managedName; - deleteFolder(new File(worldDatapackFolder, baseManagedName)); - File managedZip = new File(worldDatapackFolder, managedName); - if (managedZip.exists() - && managedZip.length() == cachedZip.length() - && managedZip.length() > 0 - && Files.mismatch(managedZip.toPath(), cachedZip.toPath()) == -1L) { - installedDatapacks++; - installedAssets += meta.optInt("installedAssets", 0); - continue; - } - try { - Files.copy(cachedZip.toPath(), managedZip.toPath(), StandardCopyOption.REPLACE_EXISTING); - } catch (IOException copyFailure) { - if (managedZip.exists() && managedZip.length() > 0) { - Iris.warn("Managed external datapack " + managedZip.getName() - + " is locked by the running server and cannot be replaced in place." - + " The previously installed version remains active; restart the server to apply updated assets."); - installedDatapacks++; - installedAssets += meta.optInt("installedAssets", 0); - continue; - } - throw copyFailure; - } - if (managedZip.exists() && managedZip.length() > 0) { - installedDatapacks++; - installedAssets += meta.optInt("installedAssets", 0); - } - } - Set resolvedLocateStructures = readJsonStringSet(meta, "resolvedLocateStructures"); - Set projectedStructureKeys = readJsonStringSet(meta, "projectedStructureKeys"); - return ProjectionResult.success( - managedName, - installedDatapacks, - installedAssets, - resolvedLocateStructures, - meta.optInt("syntheticStructureSets", 0), - projectedStructureKeys, - meta.optInt("templateAliasesApplied", 0), - meta.optInt("emptyElementConversions", 0), - meta.optInt("unresolvedTemplateRefs", 0) - ); - } catch (Throwable e) { - Iris.verbose("Projection cache miss: " + e.getMessage()); - return null; - } - } - - private static void cacheProjection(File sourceZip, File cachedZip, File cachedMeta, ProjectionAssetSummary summary) { - try { - File parent = cachedZip.getParentFile(); - if (parent != null) { - parent.mkdirs(); - } - Files.copy(sourceZip.toPath(), cachedZip.toPath(), StandardCopyOption.REPLACE_EXISTING); - JSONObject meta = new JSONObject(); - meta.put("installedAssets", summary.assets().size()); - meta.put("syntheticStructureSets", summary.syntheticStructureSets()); - meta.put("templateAliasesApplied", summary.templateAliasesApplied()); - meta.put("emptyElementConversions", summary.emptyElementConversions()); - meta.put("unresolvedTemplateRefs", summary.unresolvedTemplateRefs()); - meta.put("resolvedLocateStructures", new JSONArray(summary.resolvedLocateStructures())); - meta.put("projectedStructureKeys", new JSONArray(summary.projectedStructureKeys())); - Files.writeString(cachedMeta.toPath(), meta.toString(2), StandardCharsets.UTF_8); - } catch (Throwable e) { - Iris.verbose("Failed to cache projection: " + e.getMessage()); - } - } - - private static Set readJsonStringSet(JSONObject json, String key) { - JSONArray array = json.optJSONArray(key); - if (array == null) { - return Set.of(); - } - LinkedHashSet result = new LinkedHashSet<>(); - for (int i = 0; i < array.length(); i++) { - String value = array.optString(i, ""); - if (!value.isBlank()) { - result.add(value); - } - } - return Set.copyOf(result); - } - - private static String shortHash(String value) { - try { - MessageDigest digest = MessageDigest.getInstance("SHA-1"); - byte[] bytes = digest.digest(value.getBytes(StandardCharsets.UTF_8)); - StringBuilder builder = new StringBuilder(); - for (int i = 0; i < 4 && i < bytes.length; i++) { - builder.append(String.format("%02x", bytes[i])); - } - return builder.toString(); - } catch (Throwable e) { - return "00000000"; - } - } - - public record DatapackRequest( - String id, - String url, - String targetPack, - String requiredEnvironment, - boolean required, - boolean replaceVanilla, - boolean supportSmartBore, - Set structures, - Set structureSets, - Map> structureAliases, - Map structureSetAliases, - Map templateAliases, - Set configuredFeatures, - Set placedFeatures, - Set templatePools, - Set processorLists, - Set biomeHasStructureTags, - Map structureStartHeights, - Set forcedBiomeKeys, - String scopeKey, - boolean alongsideMode, - Set resolvedLocateStructures - ) { - public DatapackRequest( - String id, - String url, - String targetPack, - String requiredEnvironment, - boolean required, - boolean replaceVanilla, - Set forcedBiomeKeys, - String scopeKey - ) { - this( - normalizeRequestId(id, url), - url == null ? "" : url.trim(), - normalizeRequestPack(targetPack), - normalizeEnvironment(requiredEnvironment), - required, - replaceVanilla, - replaceVanilla, - Set.of(), - Set.of(), - Map.of(), - Map.of(), - Map.of(), - Set.of(), - Set.of(), - Set.of(), - Set.of(), - Set.of(), - Map.of(), - normalizeBiomeKeys(forcedBiomeKeys), - normalizeScopeKey(scopeKey), - !replaceVanilla, - Set.of() - ); - } - - public DatapackRequest { - id = normalizeRequestId(id, url); - url = url == null ? "" : url.trim(); - targetPack = normalizeRequestPack(targetPack); - requiredEnvironment = normalizeEnvironment(requiredEnvironment); - structures = immutableSet(structures); - structureSets = immutableSet(structureSets); - structureAliases = immutableStructureAliasMap(structureAliases); - structureSetAliases = immutableAliasMap(structureSetAliases); - templateAliases = immutableAliasMap(templateAliases); - configuredFeatures = immutableSet(configuredFeatures); - placedFeatures = immutableSet(placedFeatures); - templatePools = immutableSet(templatePools); - processorLists = immutableSet(processorLists); - biomeHasStructureTags = immutableSet(biomeHasStructureTags); - structureStartHeights = immutableMap(structureStartHeights); - forcedBiomeKeys = immutableBiomeSet(forcedBiomeKeys); - scopeKey = normalizeScopeKey(scopeKey); - alongsideMode = alongsideMode || !replaceVanilla; - resolvedLocateStructures = immutableLocateSet(resolvedLocateStructures, structures); - } - - public String getDedupeKey() { - return targetPack + "|" + url + "|" + scopeKey + "|" + replaceVanilla + "|" + required + "|" + setFingerprint(forcedBiomeKeys); - } - - public boolean hasReplacementTargets() { - return !structures.isEmpty() - || !structureSets.isEmpty() - || !configuredFeatures.isEmpty() - || !placedFeatures.isEmpty() - || !templatePools.isEmpty() - || !processorLists.isEmpty() - || !biomeHasStructureTags.isEmpty(); - } - - public DatapackRequest merge(DatapackRequest other) { - if (other == null) { - return this; - } - String environment = requiredEnvironment; - if ((environment == null || environment.isBlank()) && other.requiredEnvironment != null && !other.requiredEnvironment.isBlank()) { - environment = other.requiredEnvironment; - } - return new DatapackRequest( - id, - url, - targetPack, - environment, - required || other.required, - replaceVanilla || other.replaceVanilla, - supportSmartBore || other.supportSmartBore, - union(structures, other.structures), - union(structureSets, other.structureSets), - unionStructureAliases(structureAliases, other.structureAliases), - unionAliases(structureSetAliases, other.structureSetAliases), - unionAliases(templateAliases, other.templateAliases), - union(configuredFeatures, other.configuredFeatures), - union(placedFeatures, other.placedFeatures), - union(templatePools, other.templatePools), - union(processorLists, other.processorLists), - union(biomeHasStructureTags, other.biomeHasStructureTags), - unionStructureStartHeights(structureStartHeights, other.structureStartHeights), - union(forcedBiomeKeys, other.forcedBiomeKeys), - normalizeScopeKey(scopeKey), - alongsideMode || other.alongsideMode, - union(resolvedLocateStructures, other.resolvedLocateStructures) - ); - } - - private static String normalizeRequestId(String id, String url) { - String cleaned = id == null ? "" : id.trim(); - if (!cleaned.isBlank()) { - return cleaned; - } - return url == null ? "" : url.trim(); - } - - private static String normalizeRequestPack(String targetPack) { - String sanitized = sanitizePackName(targetPack); - if (!sanitized.isBlank()) { - return sanitized; - } - return defaultTargetPack(); - } - - private static Set normalizeBiomeKeys(Set values) { - LinkedHashSet normalized = new LinkedHashSet<>(); - if (values == null) { - return normalized; - } - - for (String value : values) { - String normalizedBiome = normalizeBiomeKey(value); - if (normalizedBiome != null && !normalizedBiome.isBlank()) { - normalized.add(normalizedBiome); - } - } - - return normalized; - } - - private static Set normalizeLocateStructures(Set values, KList structureValues) { - LinkedHashSet normalized = new LinkedHashSet<>(); - if (values != null) { - for (String value : values) { - String normalizedStructure = normalizeLocateStructure(value); - if (!normalizedStructure.isBlank()) { - normalized.add(normalizedStructure); - } - } - } - - if (structureValues != null) { - for (String structure : structureValues) { - String normalizedStructure = normalizeLocateStructure(structure); - if (!normalizedStructure.isBlank()) { - normalized.add(normalizedStructure); - } - } - } - - return normalized; - } - - private static Set normalizeTargets(KList values, String... prefixes) { - LinkedHashSet normalized = new LinkedHashSet<>(); - if (values == null) { - return normalized; - } - for (String value : values) { - String normalizedKey = normalizeResourceKey("minecraft", value, prefixes); - if (normalizedKey != null && !normalizedKey.isBlank()) { - normalized.add(normalizedKey); - } - } - return normalized; - } - - private static Set immutableSet(Set values) { - LinkedHashSet copy = new LinkedHashSet<>(); - if (values != null) { - copy.addAll(values); - } - return Set.copyOf(copy); - } - - private static Map> immutableStructureAliasMap(Map> values) { - LinkedHashMap> copy = new LinkedHashMap<>(); - if (values != null) { - for (Map.Entry> entry : values.entrySet()) { - String target = entry.getKey(); - List sources = entry.getValue(); - if (target == null || target.isBlank() || sources == null || sources.isEmpty()) { - continue; - } - - ArrayList filteredSources = new ArrayList<>(); - for (String source : sources) { - if (source == null || source.isBlank() || filteredSources.contains(source)) { - continue; - } - filteredSources.add(source); - } - - if (!filteredSources.isEmpty()) { - copy.put(target, List.copyOf(filteredSources)); - } - } - } - return Map.copyOf(copy); - } - - private static Map immutableAliasMap(Map values) { - LinkedHashMap copy = new LinkedHashMap<>(); - if (values != null) { - for (Map.Entry entry : values.entrySet()) { - String target = entry.getKey(); - String source = entry.getValue(); - if (target == null || target.isBlank() || source == null || source.isBlank()) { - continue; - } - copy.put(target, source); - } - } - return Map.copyOf(copy); - } - - private static Set immutableBiomeSet(Set values) { - LinkedHashSet copy = new LinkedHashSet<>(); - if (values != null) { - for (String value : values) { - String normalized = normalizeBiomeKey(value); - if (normalized != null && !normalized.isBlank()) { - copy.add(normalized); - } - } - } - return Set.copyOf(copy); - } - - private static Set immutableLocateSet(Set values, Set structureTargets) { - LinkedHashSet copy = new LinkedHashSet<>(); - if (values != null) { - for (String value : values) { - String normalized = normalizeLocateStructure(value); - if (!normalized.isBlank()) { - copy.add(normalized); - } - } - } - if (structureTargets != null) { - for (String structureTarget : structureTargets) { - String normalized = normalizeLocateStructure(structureTarget); - if (!normalized.isBlank()) { - copy.add(normalized); - } - } - } - return Set.copyOf(copy); - } - - private static Map immutableMap(Map values) { - LinkedHashMap copy = new LinkedHashMap<>(); - if (values != null) { - copy.putAll(values); - } - return Map.copyOf(copy); - } - - private static Set union(Set first, Set second) { - LinkedHashSet merged = new LinkedHashSet<>(); - if (first != null) { - merged.addAll(first); - } - if (second != null) { - merged.addAll(second); - } - return merged; - } - - private static Map> unionStructureAliases(Map> first, Map> second) { - LinkedHashMap> merged = new LinkedHashMap<>(); - mergeStructureAliasesInto(merged, first); - mergeStructureAliasesInto(merged, second); - LinkedHashMap> normalized = new LinkedHashMap<>(); - for (Map.Entry> entry : merged.entrySet()) { - ArrayList sources = entry.getValue(); - if (sources == null || sources.isEmpty()) { - continue; - } - normalized.put(entry.getKey(), List.copyOf(sources)); - } - return normalized; - } - - private static void mergeStructureAliasesInto(Map> target, Map> source) { - if (target == null || source == null || source.isEmpty()) { - return; - } - - for (Map.Entry> entry : source.entrySet()) { - String key = entry.getKey(); - List values = entry.getValue(); - if (key == null || key.isBlank() || values == null || values.isEmpty()) { - continue; - } - - ArrayList existing = target.computeIfAbsent(key, ignored -> new ArrayList<>()); - for (String value : values) { - if (value == null || value.isBlank() || existing.contains(value)) { - continue; - } - existing.add(value); - } - } - } - - private static Map unionAliases(Map first, Map second) { - LinkedHashMap merged = new LinkedHashMap<>(); - if (first != null) { - merged.putAll(first); - } - if (second != null) { - merged.putAll(second); - } - return merged; - } - - private static Map unionStructureStartHeights(Map first, Map second) { - LinkedHashMap merged = new LinkedHashMap<>(); - if (first != null) { - merged.putAll(first); - } - if (second != null) { - merged.putAll(second); - } - return merged; - } - - private static String normalizeScopeKey(String value) { - String normalized = sanitizePath(value).replace("/", "_"); - if (normalized.isBlank()) { - return "dimension-root"; - } - return normalized; - } - - private static String setFingerprint(Set values) { - if (values == null || values.isEmpty()) { - return "none"; - } - ArrayList sorted = new ArrayList<>(values); - sorted.sort(String::compareTo); - return shortHash(String.join(",", sorted)); - } - } - - public static final class PipelineSummary { - private int requests; - private int syncedRequests; - private int restoredRequests; - private int skippedExistingRequests; - private int optionalFailures; - private int requiredFailures; - private int importedSources; - private int cachedSources; - private int scannedStructures; - private int convertedStructures; - private int failedConversions; - private int skippedConversions; - private int entitiesIgnored; - private int blockEntities; - private int worldDatapacksInstalled; - private int worldAssetsInstalled; - private int legacyDownloadRemovals; - private int legacyWorldCopyRemovals; - - private void setImportSummary(ImportSummary importSummary) { - if (importSummary == null) { - return; - } - this.importedSources = importSummary.getSources(); - this.cachedSources = importSummary.getCachedSources(); - this.scannedStructures = importSummary.getNbtScanned(); - this.convertedStructures = importSummary.getConverted(); - this.failedConversions = importSummary.getFailed(); - this.skippedConversions = importSummary.getSkipped(); - this.entitiesIgnored = importSummary.getEntitiesIgnored(); - this.blockEntities = importSummary.getBlockEntities(); - } - - public int getRequests() { - return requests; - } - - public int getSyncedRequests() { - return syncedRequests; - } - - public int getRestoredRequests() { - return restoredRequests; - } - - public int getSkippedExistingRequests() { - return skippedExistingRequests; - } - - public int getOptionalFailures() { - return optionalFailures; - } - - public int getRequiredFailures() { - return requiredFailures; - } - - public int getImportedSources() { - return importedSources; - } - - public int getCachedSources() { - return cachedSources; - } - - public int getScannedStructures() { - return scannedStructures; - } - - public int getConvertedStructures() { - return convertedStructures; - } - - public int getFailedConversions() { - return failedConversions; - } - - public int getSkippedConversions() { - return skippedConversions; - } - - public int getEntitiesIgnored() { - return entitiesIgnored; - } - - public int getBlockEntities() { - return blockEntities; - } - - public int getWorldDatapacksInstalled() { - return worldDatapacksInstalled; - } - - public int getWorldAssetsInstalled() { - return worldAssetsInstalled; - } - - public int getLegacyDownloadRemovals() { - return legacyDownloadRemovals; - } - - public int getLegacyWorldCopyRemovals() { - return legacyWorldCopyRemovals; - } - } - - private record RequestedSourceInput(File source, DatapackRequest request) { - } - - private record ResolvedRemoteFile(String url, String outputFileName, String sha1) { - } - - private record RequestSyncResult(boolean success, boolean downloaded, boolean restored, File source, String error) { - private static RequestSyncResult downloaded(File source) { - return new RequestSyncResult(true, true, false, source, ""); - } - - private static RequestSyncResult restored(File source) { - return new RequestSyncResult(true, false, true, source, ""); - } - - private static RequestSyncResult failure(String error) { - return new RequestSyncResult(false, false, false, null, error == null ? "unknown error" : error); - } - } - - private record ProjectedEntry(ProjectedEntryType type, String namespace, String key) { - } - - private enum ProjectedEntryType { - STRUCTURE, - STRUCTURE_SET, - CONFIGURED_FEATURE, - PLACED_FEATURE, - TEMPLATE_POOL, - PROCESSOR_LIST, - STRUCTURE_NBT, - BIOME_HAS_STRUCTURE_TAG - } - - private record ProjectedDependency(ProjectedEntryType type, String key) { - } - - private record ProjectionSelection( - List assets, - Set missingSeededTargets, - Set directResolvedTargets, - Set aliasResolvedTargets - ) { - private ProjectionSelection { - assets = assets == null ? List.of() : List.copyOf(assets); - missingSeededTargets = missingSeededTargets == null ? Set.of() : Set.copyOf(missingSeededTargets); - directResolvedTargets = directResolvedTargets == null ? Set.of() : Set.copyOf(directResolvedTargets); - aliasResolvedTargets = aliasResolvedTargets == null ? Set.of() : Set.copyOf(aliasResolvedTargets); - } - - private static ProjectionSelection empty() { - return new ProjectionSelection(List.of(), Set.of(), Set.of(), Set.of()); - } - } - - private record MergedStartPoolResult(String key, ProjectionInputAsset asset) { - private static MergedStartPoolResult empty() { - return new MergedStartPoolResult("", null); - } - } - - private record WeightedTemplatePoolElement(JSONObject element, int weight) { - } - - private record AliasedStructureSetSynthesisResult(ProjectionInputAsset asset, Set unmappedStructureKeys) { - private AliasedStructureSetSynthesisResult { - unmappedStructureKeys = unmappedStructureKeys == null ? Set.of() : Set.copyOf(unmappedStructureKeys); - } - } - - private record StructureSetRewriteResult(JSONArray rewrittenStructures, Set unmappedStructureKeys) { - private StructureSetRewriteResult { - rewrittenStructures = rewrittenStructures == null ? new JSONArray() : rewrittenStructures; - unmappedStructureKeys = unmappedStructureKeys == null ? Set.of() : Set.copyOf(unmappedStructureKeys); - } - } - - private record ProjectionResult( - boolean success, - int installedDatapacks, - int installedAssets, - Set resolvedLocateStructures, - int syntheticStructureSets, - Set projectedStructureKeys, - int templateAliasesApplied, - int emptyElementConversions, - int unresolvedTemplateRefs, - String managedName, - String error - ) { - private ProjectionResult { - LinkedHashSet normalized = new LinkedHashSet<>(); - if (resolvedLocateStructures != null) { - for (String structure : resolvedLocateStructures) { - String normalizedStructure = normalizeLocateStructure(structure); - if (!normalizedStructure.isBlank()) { - normalized.add(normalizedStructure); - } - } - } - resolvedLocateStructures = Set.copyOf(normalized); - LinkedHashSet normalizedProjected = new LinkedHashSet<>(); - if (projectedStructureKeys != null) { - for (String structure : projectedStructureKeys) { - String normalizedStructure = normalizeLocateStructure(structure); - if (!normalizedStructure.isBlank()) { - normalizedProjected.add(normalizedStructure); - } - } - } - projectedStructureKeys = Set.copyOf(normalizedProjected); - syntheticStructureSets = Math.max(0, syntheticStructureSets); - templateAliasesApplied = Math.max(0, templateAliasesApplied); - emptyElementConversions = Math.max(0, emptyElementConversions); - unresolvedTemplateRefs = Math.max(0, unresolvedTemplateRefs); - if (error == null) { - error = ""; - } - } - - private static ProjectionResult success( - String managedName, - int installedDatapacks, - int installedAssets, - Set resolvedLocateStructures, - int syntheticStructureSets, - Set projectedStructureKeys, - int templateAliasesApplied, - int emptyElementConversions, - int unresolvedTemplateRefs - ) { - return new ProjectionResult( - true, - installedDatapacks, - installedAssets, - resolvedLocateStructures, - syntheticStructureSets, - projectedStructureKeys, - templateAliasesApplied, - emptyElementConversions, - unresolvedTemplateRefs, - managedName, - "" - ); - } - - private static ProjectionResult failure(String managedName, String error) { - String message = error == null || error.isBlank() ? "projection failed" : error; - return new ProjectionResult(false, 0, 0, Set.of(), 0, Set.of(), 0, 0, 0, managedName, message); - } - } - - private record ProjectionInputAsset(String relativePath, ProjectedEntry entry, byte[] bytes) { - private ProjectionInputAsset { - bytes = bytes == null ? new byte[0] : Arrays.copyOf(bytes, bytes.length); - } - } - - private record ProjectionOutputAsset(String relativePath, byte[] bytes) { - private ProjectionOutputAsset { - bytes = bytes == null ? new byte[0] : Arrays.copyOf(bytes, bytes.length); - } - } - - private record TemplateAliasRewriteResult( - int appliedCount, - int emptyConversions, - Set unresolvedReferences - ) { - private TemplateAliasRewriteResult { - appliedCount = Math.max(0, appliedCount); - emptyConversions = Math.max(0, emptyConversions); - unresolvedReferences = unresolvedReferences == null ? Set.of() : Set.copyOf(unresolvedReferences); - } - - private static TemplateAliasRewriteResult empty() { - return new TemplateAliasRewriteResult(0, 0, Set.of()); - } - } - - private record ProjectionAssetSummary( - List assets, - Set resolvedLocateStructures, - int syntheticStructureSets, - Set projectedStructureKeys, - int templateAliasesApplied, - int emptyElementConversions, - int unresolvedTemplateRefs - ) { - private ProjectionAssetSummary { - assets = assets == null ? List.of() : List.copyOf(assets); - LinkedHashSet normalized = new LinkedHashSet<>(); - if (resolvedLocateStructures != null) { - for (String structure : resolvedLocateStructures) { - String normalizedStructure = normalizeLocateStructure(structure); - if (!normalizedStructure.isBlank()) { - normalized.add(normalizedStructure); - } - } - } - resolvedLocateStructures = Set.copyOf(normalized); - LinkedHashSet normalizedProjected = new LinkedHashSet<>(); - if (projectedStructureKeys != null) { - for (String structure : projectedStructureKeys) { - String normalizedStructure = normalizeLocateStructure(structure); - if (!normalizedStructure.isBlank()) { - normalizedProjected.add(normalizedStructure); - } - } - } - projectedStructureKeys = Set.copyOf(normalizedProjected); - syntheticStructureSets = Math.max(0, syntheticStructureSets); - templateAliasesApplied = Math.max(0, templateAliasesApplied); - emptyElementConversions = Math.max(0, emptyElementConversions); - unresolvedTemplateRefs = Math.max(0, unresolvedTemplateRefs); - } - } - - private record SyntheticPlacement(Identifier structureSetKey, StructurePlacement placement, int weight) { - } - - private record SyntheticStructureSetResult(List assets, int count) { - private SyntheticStructureSetResult { - assets = assets == null ? List.of() : List.copyOf(assets); - count = Math.max(0, count); - } - - private static SyntheticStructureSetResult empty() { - return new SyntheticStructureSetResult(List.of(), 0); - } - } - - private record EntryPath(String originalPath, String namespace, String structurePath) { - } - - private record SourceDescriptor( - String sourceKey, - String sourceName, - String fingerprint, - String targetPack, - String requiredEnvironment, - String objectRootKey - ) { - } - - private record ModrinthFile(String pageUrl, String url, String slug, String versionId, String extension, String sha1) { - private String outputFileName() { - String version = sanitizePath(versionId).replace("/", "_"); - if (version.isEmpty()) { - version = "version"; - } - return "modrinth-" + slug + "-" + version + extension; - } - } - - private static final class SourceConversion { - private final String sourceKey; - private final String sourceName; - private final String targetPack; - private final String requiredEnvironment; - private final String objectRootKey; - private final Set usedKeys; - private final JSONArray objects; - private int nbtScanned; - private int converted; - private int failed; - private int skipped; - private int entitiesIgnored; - private int blockEntities; - - private SourceConversion( - String sourceKey, - String sourceName, - String targetPack, - String requiredEnvironment, - String objectRootKey, - String requestId - ) { - this.sourceKey = sourceKey; - this.sourceName = sourceName; - this.targetPack = targetPack; - this.requiredEnvironment = requiredEnvironment; - String effectiveRequestId = requestId == null ? "" : requestId; - this.objectRootKey = normalizeObjectRootKey(effectiveRequestId.isBlank() ? objectRootKey : effectiveRequestId); - this.usedKeys = new HashSet<>(); - this.objects = new JSONArray(); - this.nbtScanned = 0; - this.converted = 0; - this.failed = 0; - this.skipped = 0; - this.entitiesIgnored = 0; - this.blockEntities = 0; - } - - private String reserveObjectKey(String namespace, String structurePath) { - String namespacePath = sanitizePath(namespace); - String structureValue = sanitizePath(stripExtension(structurePath)); - if (namespacePath.isEmpty() || structureValue.isEmpty()) { - return null; - } - String baseKey = objectRootKey + "/" + structureValue; - if (usedKeys.add(baseKey)) { - return baseKey; - } - String namespacedKey = objectRootKey + "/" + namespacePath + "/" + structureValue; - return createUniqueKey(namespacedKey, usedKeys); - } - - private JSONObject toJson(String fingerprint) { - JSONObject source = new JSONObject(); - source.put("sourceKey", sourceKey); - source.put("sourceName", sourceName); - source.put("targetPack", targetPack); - source.put("objectRootKey", objectRootKey); - if (requiredEnvironment != null) { - source.put("requiredEnvironment", requiredEnvironment); - } - source.put("fingerprint", fingerprint); - source.put("nbtScanned", nbtScanned); - source.put("converted", converted); - source.put("failed", failed); - source.put("skipped", skipped); - source.put("entitiesIgnored", entitiesIgnored); - source.put("blockEntities", blockEntities); - source.put("objects", objects); - return source; - } - } - - public static final class ImportSummary { - private int sources; - private int cachedSources; - private int nbtScanned; - private int converted; - private int failed; - private int skipped; - private int entitiesIgnored; - private int blockEntities; - - public int getSources() { - return sources; - } - - public int getCachedSources() { - return cachedSources; - } - - public int getNbtScanned() { - return nbtScanned; - } - - public int getConverted() { - return converted; - } - - public int getFailed() { - return failed; - } - - public int getSkipped() { - return skipped; - } - - public int getEntitiesIgnored() { - return entitiesIgnored; - } - - public int getBlockEntities() { - return blockEntities; - } - } - - private static final class ConversionResult { - private final boolean success; - private final boolean skipped; - private final JSONObject record; - private final int blockEntities; - private final boolean entitiesIgnored; - - private ConversionResult(boolean success, boolean skipped, JSONObject record, int blockEntities, boolean entitiesIgnored) { - this.success = success; - this.skipped = skipped; - this.record = record; - this.blockEntities = blockEntities; - this.entitiesIgnored = entitiesIgnored; - } - - private static ConversionResult success(JSONObject record, int blockEntities, boolean entitiesIgnored) { - return new ConversionResult(true, false, record, blockEntities, entitiesIgnored); - } - - private static ConversionResult failed() { - return new ConversionResult(false, false, null, 0, false); - } - - private static ConversionResult skipped() { - return new ConversionResult(false, true, null, 0, false); - } - } -} 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 601ebcf91..c5b26d645 100644 --- a/core/src/main/java/art/arcane/iris/core/IrisSettings.java +++ b/core/src/main/java/art/arcane/iris/core/IrisSettings.java @@ -220,24 +220,6 @@ public class IrisSettings { public boolean useConsoleCustomColors = true; public boolean useCustomColorsIngame = true; public boolean adjustVanillaHeight = false; - public boolean importExternalDatapacks = true; - public boolean autoGenerateIntrinsicStructures = true; - public boolean intrinsicStructureFoundations = true; - public int intrinsicFoundationMaxDepth = 96; - public java.util.List intrinsicStructureAllowlist = new java.util.ArrayList<>(java.util.List.of( - "minecraft:village_plains", - "minecraft:village_desert", - "minecraft:village_savanna", - "minecraft:village_snowy", - "minecraft:village_taiga", - "minecraft:pillager_outpost", - "minecraft:desert_pyramid", - "minecraft:jungle_temple", - "minecraft:swamp_hut", - "minecraft:igloo", - "minecraft:mansion", - "minecraft:ruined_portal*" - )); 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 f95f8a94e..28225b763 100644 --- a/core/src/main/java/art/arcane/iris/core/ServerConfigurator.java +++ b/core/src/main/java/art/arcane/iris/core/ServerConfigurator.java @@ -20,13 +20,13 @@ package art.arcane.iris.core; import art.arcane.iris.Iris; import art.arcane.iris.core.loader.IrisData; -import art.arcane.iris.core.loader.ResourceLoader; import art.arcane.iris.core.nms.INMS; import art.arcane.iris.core.nms.datapack.DataVersion; import art.arcane.iris.core.nms.datapack.IDataFixer; -import art.arcane.iris.engine.object.*; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisBiomeCustom; +import art.arcane.iris.engine.object.IrisDimension; import art.arcane.volmlib.util.collection.KList; -import art.arcane.volmlib.util.collection.KMap; import art.arcane.volmlib.util.collection.KSet; import art.arcane.iris.util.common.format.C; import art.arcane.iris.util.common.misc.ServerProperties; @@ -34,8 +34,6 @@ import art.arcane.iris.util.common.plugin.VolmitSender; import art.arcane.iris.util.common.scheduling.J; import lombok.NonNull; import org.bukkit.Bukkit; -import org.bukkit.NamespacedKey; -import org.bukkit.block.Biome; import org.bukkit.configuration.InvalidConfigurationException; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; @@ -44,15 +42,8 @@ import org.jetbrains.annotations.Nullable; import java.io.File; import java.io.IOException; -import java.util.ArrayDeque; -import java.util.ArrayList; import java.util.Arrays; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.Locale; -import java.util.Map; import java.util.Objects; -import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicIntegerArray; import java.util.stream.Stream; @@ -105,6 +96,7 @@ public class ServerConfigurator { f.save(spigotConfig); } } + private static void increasePaperWatchdog() throws IOException, InvalidConfigurationException { File spigotConfig = new File("config/paper-global.yml"); FileConfiguration f = new YamlConfiguration(); @@ -138,64 +130,28 @@ public class ServerConfigurator { } public static boolean installDataPacks(boolean fullInstall) { - return installDataPacks(fullInstall, true); - } - - public static boolean installDataPacks(boolean fullInstall, boolean includeExternal) { - return installDataPacks(fullInstall, includeExternal, null); - } - - public static boolean installDataPacks( - boolean fullInstall, - boolean includeExternal, - KMap> extraWorldDatapackFoldersByPack - ) { IDataFixer fixer = DataVersion.getDefault(); if (fixer == null) { DataVersion fallback = DataVersion.getLatest(); Iris.warn("Primary datapack fixer was null, forcing latest fixer: " + fallback.getVersion()); fixer = fallback.get(); } - return installDataPacks(fixer, fullInstall, includeExternal, extraWorldDatapackFoldersByPack); + return installDataPacks(fixer, fullInstall); } public static boolean installDataPacks(IDataFixer fixer, boolean fullInstall) { - return installDataPacks(fixer, fullInstall, true); - } - - public static boolean installDataPacks(IDataFixer fixer, boolean fullInstall, boolean includeExternal) { - return installDataPacks(fixer, fullInstall, includeExternal, null); - } - - public static boolean installDataPacks( - IDataFixer fixer, - boolean fullInstall, - boolean includeExternal, - KMap> extraWorldDatapackFoldersByPack - ) { if (fixer == null) { Iris.error("Unable to install datapacks, fixer is null!"); return false; } - if (fullInstall || includeExternal) { + if (fullInstall) { Iris.info("Checking Data Packs..."); } else { Iris.verbose("Checking Data Packs..."); } DimensionHeight height = new DimensionHeight(fixer); - KList baseFolders = getDatapacksFolder(); - KList folders = collectInstallDatapackFolders(baseFolders, extraWorldDatapackFoldersByPack); - if (fullInstall) { - if (anyDimensionHasVanillaStructures()) { - VanillaDatapackDumper.dumpIfNeeded(baseFolders); - } else { - VanillaDatapackDumper.removeIfPresent(baseFolders); - } - } - if (includeExternal) { - installExternalDataPacks(baseFolders, extraWorldDatapackFoldersByPack); - } - KMap> biomes = new KMap<>(); + KList folders = getDatapacksFolder(); + java.util.concurrent.ConcurrentMap> biomes = new java.util.concurrent.ConcurrentHashMap<>(); try (Stream stream = allPacks()) { stream.flatMap(height::merge) @@ -207,7 +163,7 @@ public class ServerConfigurator { }); } IrisDimension.writeShared(folders, height); - if (fullInstall || includeExternal) { + if (fullInstall) { Iris.info("Data Packs Setup!"); } else { Iris.verbose("Data Packs Setup!"); @@ -216,83 +172,6 @@ public class ServerConfigurator { return fullInstall && verifyDataPacksPost(IrisSettings.get().getAutoConfiguration().isAutoRestartOnCustomBiomeInstall()); } - static KList collectInstallDatapackFolders( - KList baseFolders, - KMap> extraWorldDatapackFoldersByPack - ) { - KList folders = new KList<>(); - if (baseFolders != null) { - for (File folder : baseFolders) { - if (folder != null && !folders.contains(folder)) { - folders.add(folder); - } - } - } - if (extraWorldDatapackFoldersByPack == null || extraWorldDatapackFoldersByPack.isEmpty()) { - return folders; - } - for (KList extraFolders : extraWorldDatapackFoldersByPack.values()) { - if (extraFolders == null || extraFolders.isEmpty()) { - continue; - } - for (File folder : extraFolders) { - if (folder != null && !folders.contains(folder)) { - folders.add(folder); - } - } - } - return folders; - } - - private static void installExternalDataPacks( - KList folders, - KMap> extraWorldDatapackFoldersByPack - ) { - if (!IrisSettings.get().getGeneral().isImportExternalDatapacks()) { - return; - } - - KList requests = collectExternalDatapackRequests(); - KMap> worldDatapackFoldersByPack = collectWorldDatapackFoldersByPack(folders, extraWorldDatapackFoldersByPack); - ExternalDataPackPipeline.PipelineSummary summary = ExternalDataPackPipeline.processDatapacks(requests, worldDatapackFoldersByPack); - if (summary.getLegacyDownloadRemovals() > 0) { - Iris.verbose("Removed " + summary.getLegacyDownloadRemovals() + " legacy global datapack downloads."); - } - if (summary.getLegacyWorldCopyRemovals() > 0) { - Iris.verbose("Removed " + summary.getLegacyWorldCopyRemovals() + " legacy managed world datapack copies."); - } - if (summary.getSkippedExistingRequests() > 0) { - Iris.verbose("Reused " + summary.getSkippedExistingRequests() + " already-installed external datapack(s) (no download/projection)."); - } - int loadedDatapackCount = Math.max(0, summary.getRequests() - summary.getOptionalFailures() - summary.getRequiredFailures()); - Iris.info("Loaded Datapacks into Iris: " + loadedDatapackCount + "!"); - if (summary.getRequiredFailures() > 0) { - throw new IllegalStateException("Required external datapack setup failed for " + summary.getRequiredFailures() + " request(s)."); - } - } - - private static boolean anyDimensionHasVanillaStructures() { - try (Stream stream = allPacks()) { - return stream.anyMatch(data -> { - ResourceLoader loader = data.getDimensionLoader(); - if (loader == null) { - return false; - } - String[] keys = loader.getPossibleKeys(); - if (keys == null || keys.length == 0) { - return false; - } - for (String key : keys) { - IrisDimension dim = loader.load(key); - if (dim != null && dim.isVanillaStructures()) { - return true; - } - } - return false; - }); - } - } - private static boolean shouldDeferInstallUntilWorldsReady() { String forcedMainWorld = IrisSettings.get().getGeneral().forceMainWorld; if (forcedMainWorld != null && !forcedMainWorld.isBlank()) { @@ -302,722 +181,6 @@ public class ServerConfigurator { return Bukkit.getServer().getWorlds().isEmpty(); } - private static KList collectExternalDatapackRequests() { - KMap deduplicated = new KMap<>(); - try (Stream stream = allPacks()) { - stream.forEach(data -> collectExternalDatapackRequestsForPack(data, deduplicated)); - } - - return new KList<>(deduplicated.v()); - } - - private static void collectExternalDatapackRequestsForPack(IrisData data, KMap deduplicated) { - ResourceLoader loader = data.getDimensionLoader(); - if (loader == null) { - Iris.warn("Skipping external datapack request discovery for pack " + data.getDataFolder().getName() + " because dimension loader is unavailable."); - return; - } - - String[] possibleKeys = loader.getPossibleKeys(); - if (possibleKeys == null || possibleKeys.length == 0) { - File dimensionsFolder = new File(data.getDataFolder(), "dimensions"); - File[] dimensionFiles = dimensionsFolder.listFiles((dir, name) -> name != null && name.toLowerCase().endsWith(".json")); - int dimensionFileCount = dimensionFiles == null ? 0 : dimensionFiles.length; - Iris.warn("Pack " + data.getDataFolder().getName() + " has no loadable dimension keys. Dimension folder json files=" + dimensionFileCount + ". External datapacks in this pack cannot be discovered."); - return; - } - - KList dimensions = loader.loadAll(possibleKeys); - int scannedDimensions = 0; - int dimensionsWithExternalEntries = 0; - int enabledEntries = 0; - int disabledEntries = 0; - int skippedBlankUrl = 0; - int scopedRequests = 0; - int unscopedRequests = 0; - int dedupeMerges = 0; - for (IrisDimension dimension : dimensions) { - if (dimension == null) { - continue; - } - - scannedDimensions++; - KList externalDatapacks = dimension.getExternalDatapacks(); - if (externalDatapacks == null || externalDatapacks.isEmpty()) { - continue; - } - - dimensionsWithExternalEntries++; - String targetPack = sanitizePackName(dimension.getLoadKey()); - if (targetPack.isBlank()) { - targetPack = sanitizePackName(data.getDataFolder().getName()); - } - - String environment = ExternalDataPackPipeline.normalizeEnvironmentValue(dimension.getEnvironment() == null ? null : dimension.getEnvironment().name()); - LinkedHashMap definitionsById = new LinkedHashMap<>(); - for (IrisExternalDatapack externalDatapack : externalDatapacks) { - if (externalDatapack == null) { - disabledEntries++; - continue; - } - - if (!externalDatapack.isEnabled()) { - disabledEntries++; - continue; - } - - String url = externalDatapack.getUrl() == null ? "" : externalDatapack.getUrl().trim(); - if (url.isBlank()) { - skippedBlankUrl++; - continue; - } - - enabledEntries++; - String requestId = normalizeExternalDatapackId(externalDatapack.getId(), url); - IrisExternalDatapack existingDefinition = definitionsById.put(requestId, externalDatapack); - if (existingDefinition != null) { - Iris.warn("Duplicate external datapack id '" + requestId + "' in dimension " + dimension.getLoadKey() + ". Latest entry wins."); - } - } - - if (definitionsById.isEmpty()) { - continue; - } - - KMap> scopedGroups = resolveScopedBindingGroups(data, dimension, definitionsById); - for (Map.Entry entry : definitionsById.entrySet()) { - String requestId = entry.getKey(); - IrisExternalDatapack definition = entry.getValue(); - String url = definition.getUrl() == null ? "" : definition.getUrl().trim(); - if (url.isBlank()) { - continue; - } - - KList groups = scopedGroups.get(requestId); - if (groups == null || groups.isEmpty()) { - String scopeKey = buildRootScopeKey(dimension.getLoadKey(), requestId); - ExternalDataPackPipeline.DatapackRequest request = new ExternalDataPackPipeline.DatapackRequest( - requestId, - url, - targetPack, - environment, - definition.isRequired(), - definition.isReplace(), - Set.of(), - scopeKey - ); - dedupeMerges += mergeDeduplicatedRequest(deduplicated, request); - unscopedRequests++; - Iris.verbose("External datapack scope resolved: id=" + requestId - + ", targetPack=" + targetPack - + ", dimension=" + dimension.getLoadKey() - + ", scope=dimension-root" - + ", forcedBiomes=0" - + ", replace=" + definition.isReplace() - + ", required=" + definition.isRequired()); - continue; - } - - for (ScopedBindingGroup group : groups) { - ExternalDataPackPipeline.DatapackRequest request = new ExternalDataPackPipeline.DatapackRequest( - requestId, - url, - targetPack, - environment, - group.required(), - group.replaceVanilla(), - group.forcedBiomeKeys(), - group.scopeKey() - ); - dedupeMerges += mergeDeduplicatedRequest(deduplicated, request); - scopedRequests++; - Iris.verbose("External datapack scope resolved: id=" + requestId - + ", targetPack=" + targetPack - + ", dimension=" + dimension.getLoadKey() - + ", scope=" + group.source() - + ", forcedBiomes=" + group.forcedBiomeKeys().size() - + ", replace=" + group.replaceVanilla() - + ", required=" + group.required()); - } - } - } - - if (scannedDimensions == 0) { - Iris.warn("Pack " + data.getDataFolder().getName() + " did not resolve any dimensions during external datapack discovery."); - return; - } - - if (dimensionsWithExternalEntries > 0 || enabledEntries > 0 || disabledEntries > 0 || skippedBlankUrl > 0) { - Iris.verbose("External datapack discovery for pack " + data.getDataFolder().getName() - + ": dimensions=" + scannedDimensions - + ", withEntries=" + dimensionsWithExternalEntries - + ", enabled=" + enabledEntries - + ", disabled=" + disabledEntries - + ", skippedBlankUrl=" + skippedBlankUrl - + ", scopedRequests=" + scopedRequests - + ", unscopedRequests=" + unscopedRequests - + ", dedupeMerges=" + dedupeMerges); - } - } - - private static KMap> resolveScopedBindingGroups( - IrisData data, - IrisDimension dimension, - Map definitionsById - ) { - KMap> groupedRequestsById = new KMap<>(); - if (definitionsById == null || definitionsById.isEmpty()) { - return groupedRequestsById; - } - - ResourceLoader regionLoader = data.getRegionLoader(); - ResourceLoader biomeLoader = data.getBiomeLoader(); - if (regionLoader == null || biomeLoader == null) { - return groupedRequestsById; - } - - String biomeNamespace = resolveBiomeNamespace(dimension); - LinkedHashMap biomeCache = new LinkedHashMap<>(); - LinkedHashMap regions = new LinkedHashMap<>(); - KList dimensionRegions = dimension.getRegions(); - if (dimensionRegions != null) { - for (String regionKey : dimensionRegions) { - String normalizedRegion = normalizeResourceReference(regionKey); - if (normalizedRegion.isBlank()) { - continue; - } - - IrisRegion region = regionLoader.load(normalizedRegion, false); - if (region != null) { - regions.put(normalizedRegion, region); - } - } - } - - LinkedHashMap> candidatesById = new LinkedHashMap<>(); - LinkedHashSet discoveryBiomeKeys = new LinkedHashSet<>(); - for (IrisRegion region : regions.values()) { - Set expandedRegionBiomes = collectRegionBiomeKeys(region, true, biomeLoader, biomeCache); - discoveryBiomeKeys.addAll(expandedRegionBiomes); - - KList bindings = region.getExternalDatapacks(); - if (bindings == null || bindings.isEmpty()) { - continue; - } - - for (IrisExternalDatapackBinding binding : bindings) { - if (binding == null || !binding.isEnabled()) { - continue; - } - - String id = normalizeExternalDatapackId(binding.getId(), ""); - if (id.isBlank()) { - continue; - } - - IrisExternalDatapack definition = definitionsById.get(id); - if (definition == null) { - Iris.warn("Ignoring region external datapack binding id '" + id + "' in " + region.getLoadKey() + " because no matching dimension externalDatapacks entry exists."); - continue; - } - - boolean replaceVanilla = binding.getReplaceOverride() == null - ? definition.isReplace() - : binding.getReplaceOverride(); - boolean required = binding.getRequiredOverride() == null - ? definition.isRequired() - : binding.getRequiredOverride(); - Set regionBiomeKeys = collectRegionBiomeKeys(region, binding.isIncludeChildren(), biomeLoader, biomeCache); - Set runtimeBiomeKeys = resolveRuntimeBiomeKeys(regionBiomeKeys, biomeNamespace, biomeLoader, biomeCache); - if (runtimeBiomeKeys.isEmpty()) { - continue; - } - - KList candidates = candidatesById.computeIfAbsent(id, key -> new KList<>()); - candidates.add(new ScopedBindingCandidate("region", region.getLoadKey(), 1, replaceVanilla, required, runtimeBiomeKeys)); - } - } - - for (String biomeKey : discoveryBiomeKeys) { - IrisBiome biome = loadBiomeFromCache(biomeKey, biomeLoader, biomeCache); - if (biome == null) { - continue; - } - - KList bindings = biome.getExternalDatapacks(); - if (bindings == null || bindings.isEmpty()) { - continue; - } - - for (IrisExternalDatapackBinding binding : bindings) { - if (binding == null || !binding.isEnabled()) { - continue; - } - - String id = normalizeExternalDatapackId(binding.getId(), ""); - if (id.isBlank()) { - continue; - } - - IrisExternalDatapack definition = definitionsById.get(id); - if (definition == null) { - Iris.warn("Ignoring biome external datapack binding id '" + id + "' in " + biome.getLoadKey() + " because no matching dimension externalDatapacks entry exists."); - continue; - } - - boolean replaceVanilla = binding.getReplaceOverride() == null - ? definition.isReplace() - : binding.getReplaceOverride(); - boolean required = binding.getRequiredOverride() == null - ? definition.isRequired() - : binding.getRequiredOverride(); - Set biomeSelection = collectBiomeKeys(biome.getLoadKey(), binding.isIncludeChildren(), biomeLoader, biomeCache); - Set runtimeBiomeKeys = resolveRuntimeBiomeKeys(biomeSelection, biomeNamespace, biomeLoader, biomeCache); - if (runtimeBiomeKeys.isEmpty()) { - continue; - } - - KList candidates = candidatesById.computeIfAbsent(id, key -> new KList<>()); - candidates.add(new ScopedBindingCandidate("biome", biome.getLoadKey(), 2, replaceVanilla, required, runtimeBiomeKeys)); - } - } - - for (Map.Entry> entry : candidatesById.entrySet()) { - String id = entry.getKey(); - KList candidates = entry.getValue(); - if (candidates == null || candidates.isEmpty()) { - continue; - } - - LinkedHashMap selectedByBiome = new LinkedHashMap<>(); - for (ScopedBindingCandidate candidate : candidates) { - if (candidate == null || candidate.forcedBiomeKeys() == null || candidate.forcedBiomeKeys().isEmpty()) { - continue; - } - - ArrayList sortedBiomeKeys = new ArrayList<>(candidate.forcedBiomeKeys()); - sortedBiomeKeys.sort(String::compareTo); - for (String runtimeBiomeKey : sortedBiomeKeys) { - ScopedBindingSelection selected = selectedByBiome.get(runtimeBiomeKey); - if (selected == null) { - selectedByBiome.put(runtimeBiomeKey, new ScopedBindingSelection( - candidate.priority(), - candidate.replaceVanilla(), - candidate.required(), - candidate.sourceType(), - candidate.sourceKey() - )); - continue; - } - - if (candidate.priority() > selected.priority()) { - selectedByBiome.put(runtimeBiomeKey, new ScopedBindingSelection( - candidate.priority(), - candidate.replaceVanilla(), - candidate.required(), - candidate.sourceType(), - candidate.sourceKey() - )); - continue; - } - - if (candidate.priority() == selected.priority() - && (candidate.replaceVanilla() != selected.replaceVanilla() || candidate.required() != selected.required())) { - Iris.warn("External datapack scope conflict for id=" + id - + ", biomeKey=" + runtimeBiomeKey - + ", kept=" + selected.sourceType() + "/" + selected.sourceKey() - + ", ignored=" + candidate.sourceType() + "/" + candidate.sourceKey()); - } - } - } - - LinkedHashMap> groupedBiomes = new LinkedHashMap<>(); - LinkedHashMap groupedSelection = new LinkedHashMap<>(); - for (Map.Entry selectedEntry : selectedByBiome.entrySet()) { - String runtimeBiomeKey = selectedEntry.getKey(); - ScopedBindingSelection selection = selectedEntry.getValue(); - String groupKey = selection.replaceVanilla() + "|" + selection.required(); - groupedBiomes.computeIfAbsent(groupKey, key -> new LinkedHashSet<>()).add(runtimeBiomeKey); - groupedSelection.putIfAbsent(groupKey, selection); - } - - for (Map.Entry> groupedEntry : groupedBiomes.entrySet()) { - LinkedHashSet runtimeBiomeKeys = groupedEntry.getValue(); - if (runtimeBiomeKeys == null || runtimeBiomeKeys.isEmpty()) { - continue; - } - - ScopedBindingSelection selection = groupedSelection.get(groupedEntry.getKey()); - if (selection == null) { - continue; - } - - Set forcedBiomeKeys = Set.copyOf(runtimeBiomeKeys); - String scopeKey = buildScopedScopeKey(dimension.getLoadKey(), id, selection.sourceType(), selection.sourceKey(), forcedBiomeKeys); - String source = selection.sourceType() + ":" + selection.sourceKey(); - KList groups = groupedRequestsById.computeIfAbsent(id, key -> new KList<>()); - groups.add(new ScopedBindingGroup(selection.replaceVanilla(), selection.required(), forcedBiomeKeys, scopeKey, source)); - } - } - - return groupedRequestsById; - } - - private static Set collectRegionBiomeKeys( - IrisRegion region, - boolean includeChildren, - ResourceLoader biomeLoader, - Map biomeCache - ) { - LinkedHashSet regionBiomeKeys = new LinkedHashSet<>(); - if (region == null) { - return regionBiomeKeys; - } - - addAllResourceReferences(regionBiomeKeys, region.getLandBiomes()); - addAllResourceReferences(regionBiomeKeys, region.getSeaBiomes()); - addAllResourceReferences(regionBiomeKeys, region.getShoreBiomes()); - addAllResourceReferences(regionBiomeKeys, region.getCaveBiomes()); - if (!includeChildren) { - return regionBiomeKeys; - } - - LinkedHashSet expanded = new LinkedHashSet<>(); - for (String biomeKey : regionBiomeKeys) { - expanded.addAll(collectBiomeKeys(biomeKey, true, biomeLoader, biomeCache)); - } - return expanded; - } - - private static Set collectBiomeKeys( - String biomeKey, - boolean includeChildren, - ResourceLoader biomeLoader, - Map biomeCache - ) { - LinkedHashSet resolved = new LinkedHashSet<>(); - String normalizedBiomeKey = normalizeResourceReference(biomeKey); - if (normalizedBiomeKey.isBlank()) { - return resolved; - } - - if (!includeChildren) { - resolved.add(normalizedBiomeKey); - return resolved; - } - - ArrayDeque queue = new ArrayDeque<>(); - queue.add(normalizedBiomeKey); - while (!queue.isEmpty()) { - String next = normalizeResourceReference(queue.removeFirst()); - if (next.isBlank() || !resolved.add(next)) { - continue; - } - - IrisBiome biome = loadBiomeFromCache(next, biomeLoader, biomeCache); - if (biome == null) { - continue; - } - - addQueueResourceReferences(queue, biome.getChildren()); - } - - return resolved; - } - - private static Set resolveRuntimeBiomeKeys( - Set irisBiomeKeys, - String biomeNamespace, - ResourceLoader biomeLoader, - Map biomeCache - ) { - LinkedHashSet resolved = new LinkedHashSet<>(); - if (irisBiomeKeys == null || irisBiomeKeys.isEmpty()) { - return resolved; - } - - for (String irisBiomeKey : irisBiomeKeys) { - String normalizedBiomeKey = normalizeResourceReference(irisBiomeKey); - if (normalizedBiomeKey.isBlank()) { - continue; - } - - IrisBiome biome = loadBiomeFromCache(normalizedBiomeKey, biomeLoader, biomeCache); - if (biome == null) { - continue; - } - - if (biome.isCustom() && biome.getCustomDerivitives() != null && !biome.getCustomDerivitives().isEmpty()) { - for (IrisBiomeCustom customDerivative : biome.getCustomDerivitives()) { - if (customDerivative == null) { - continue; - } - - String customId = normalizeResourceReference(customDerivative.getId()); - if (customId.isBlank()) { - continue; - } - resolved.add((biomeNamespace + ":" + customId).toLowerCase(Locale.ROOT)); - } - continue; - } - - Biome vanillaDerivative = biome.getVanillaDerivative(); - NamespacedKey vanillaKey = vanillaDerivative == null ? null : vanillaDerivative.getKey(); - if (vanillaKey != null) { - resolved.add(vanillaKey.toString().toLowerCase(Locale.ROOT)); - } - } - - return resolved; - } - - private static String resolveBiomeNamespace(IrisDimension dimension) { - if (dimension == null) { - return "iris"; - } - - String namespace = dimension.getLoadKey() == null ? "" : dimension.getLoadKey().trim().toLowerCase(Locale.ROOT); - namespace = namespace.replaceAll("[^a-z0-9_\\-.]", "_"); - namespace = namespace.replaceAll("_+", "_"); - namespace = namespace.replaceAll("^_+", ""); - namespace = namespace.replaceAll("_+$", ""); - if (namespace.isBlank()) { - return "iris"; - } - return namespace; - } - - private static IrisBiome loadBiomeFromCache( - String biomeKey, - ResourceLoader biomeLoader, - Map biomeCache - ) { - if (biomeLoader == null) { - return null; - } - - String normalizedBiomeKey = normalizeResourceReference(biomeKey); - if (normalizedBiomeKey.isBlank()) { - return null; - } - - if (biomeCache.containsKey(normalizedBiomeKey)) { - return biomeCache.get(normalizedBiomeKey); - } - - IrisBiome biome = biomeLoader.load(normalizedBiomeKey, false); - if (biome != null) { - biomeCache.put(normalizedBiomeKey, biome); - } - return biome; - } - - private static void addAllResourceReferences(Set destination, KList references) { - if (destination == null || references == null || references.isEmpty()) { - return; - } - - for (String reference : references) { - String normalized = normalizeResourceReference(reference); - if (!normalized.isBlank()) { - destination.add(normalized); - } - } - } - - private static void addQueueResourceReferences(ArrayDeque queue, KList references) { - if (queue == null || references == null || references.isEmpty()) { - return; - } - - for (String reference : references) { - String normalized = normalizeResourceReference(reference); - if (!normalized.isBlank()) { - queue.addLast(normalized); - } - } - } - - private static String normalizeResourceReference(String reference) { - if (reference == null) { - return ""; - } - - String normalized = reference.trim().replace('\\', '/'); - normalized = normalized.replaceAll("/+", "/"); - normalized = normalized.replaceAll("^/+", ""); - normalized = normalized.replaceAll("/+$", ""); - return normalized; - } - - private static int mergeDeduplicatedRequest( - KMap deduplicated, - ExternalDataPackPipeline.DatapackRequest request - ) { - if (request == null) { - return 0; - } - - String dedupeKey = request.getDedupeKey(); - ExternalDataPackPipeline.DatapackRequest existing = deduplicated.get(dedupeKey); - if (existing == null) { - deduplicated.put(dedupeKey, request); - return 0; - } - - deduplicated.put(dedupeKey, existing.merge(request)); - return 1; - } - - private static String normalizeExternalDatapackId(String id, String fallbackUrl) { - String normalized = id == null ? "" : id.trim(); - if (!normalized.isBlank()) { - return normalized.toLowerCase(Locale.ROOT); - } - - String fallback = fallbackUrl == null ? "" : fallbackUrl.trim(); - if (fallback.isBlank()) { - return ""; - } - return fallback.toLowerCase(Locale.ROOT); - } - - private static String buildRootScopeKey(String dimensionKey, String id) { - String normalizedDimension = ExternalDataPackPipeline.sanitizePackNameValue(dimensionKey); - if (normalizedDimension.isBlank()) { - normalizedDimension = "dimension"; - } - String normalizedId = ExternalDataPackPipeline.sanitizePackNameValue(id); - if (normalizedId.isBlank()) { - normalizedId = "external"; - } - return "root-" + normalizedDimension + "-" + normalizedId; - } - - private static String buildScopedScopeKey(String dimensionKey, String id, String sourceType, String sourceKey, Set forcedBiomeKeys) { - ArrayList sortedBiomes = new ArrayList<>(); - if (forcedBiomeKeys != null) { - sortedBiomes.addAll(forcedBiomeKeys); - } - sortedBiomes.sort(String::compareTo); - String biomeFingerprint = Integer.toHexString(String.join(",", sortedBiomes).hashCode()); - String normalizedDimension = ExternalDataPackPipeline.sanitizePackNameValue(dimensionKey); - if (normalizedDimension.isBlank()) { - normalizedDimension = "dimension"; - } - String normalizedId = ExternalDataPackPipeline.sanitizePackNameValue(id); - if (normalizedId.isBlank()) { - normalizedId = "external"; - } - String normalizedSourceType = ExternalDataPackPipeline.sanitizePackNameValue(sourceType); - if (normalizedSourceType.isBlank()) { - normalizedSourceType = "scope"; - } - String normalizedSourceKey = ExternalDataPackPipeline.sanitizePackNameValue(sourceKey); - if (normalizedSourceKey.isBlank()) { - normalizedSourceKey = "entry"; - } - return normalizedDimension + "-" + normalizedId + "-" + normalizedSourceType + "-" + normalizedSourceKey + "-" + biomeFingerprint; - } - - private record ScopedBindingCandidate( - String sourceType, - String sourceKey, - int priority, - boolean replaceVanilla, - boolean required, - Set forcedBiomeKeys - ) { - } - - private record ScopedBindingSelection( - int priority, - boolean replaceVanilla, - boolean required, - String sourceType, - String sourceKey - ) { - } - - private record ScopedBindingGroup( - boolean replaceVanilla, - boolean required, - Set forcedBiomeKeys, - String scopeKey, - String source - ) { - } - - private static KMap> collectWorldDatapackFoldersByPack( - KList fallbackFolders, - KMap> extraWorldDatapackFoldersByPack - ) { - KMap> foldersByPack = new KMap<>(); - KMap mappedWorlds = IrisWorlds.get().getWorlds(); - - for (String worldName : mappedWorlds.k()) { - String packName = sanitizePackName(mappedWorlds.get(worldName)); - if (packName.isBlank()) { - continue; - } - org.bukkit.World world = Bukkit.getWorld(worldName); - File datapacksFolder = world == null - ? new File(Bukkit.getWorldContainer(), worldName + File.separator + "datapacks") - : resolveDatapacksFolder(world.getWorldFolder()); - addWorldDatapackFolder(foldersByPack, packName, datapacksFolder); - } - - for (org.bukkit.World world : Bukkit.getWorlds()) { - String worldName = world.getName(); - String mappedPack = mappedWorlds.get(worldName); - String packName = sanitizePackName(mappedPack); - if (packName.isBlank()) { - packName = sanitizePackName(IrisSettings.get().getGenerator().getDefaultWorldType()); - } - if (packName.isBlank()) { - continue; - } - File datapacksFolder = resolveDatapacksFolder(world.getWorldFolder()); - addWorldDatapackFolder(foldersByPack, packName, datapacksFolder); - } - - String defaultPack = sanitizePackName(IrisSettings.get().getGenerator().getDefaultWorldType()); - if (!defaultPack.isBlank()) { - for (File folder : fallbackFolders) { - addWorldDatapackFolder(foldersByPack, defaultPack, folder); - } - } - - if (extraWorldDatapackFoldersByPack != null && !extraWorldDatapackFoldersByPack.isEmpty()) { - for (Map.Entry> entry : extraWorldDatapackFoldersByPack.entrySet()) { - String packName = sanitizePackName(entry.getKey()); - if (packName.isBlank()) { - continue; - } - KList folders = entry.getValue(); - if (folders == null || folders.isEmpty()) { - continue; - } - for (File folder : folders) { - addWorldDatapackFolder(foldersByPack, packName, folder); - } - } - } - - return foldersByPack; - } - - private static void addWorldDatapackFolder(KMap> foldersByPack, String packName, File folder) { - if (folder == null || packName == null || packName.isBlank()) { - return; - } - KList folders = foldersByPack.computeIfAbsent(packName, k -> new KList<>()); - if (!folders.contains(folder)) { - folders.add(folder); - } - } - public static File resolveDatapacksFolder(File worldFolder) { File rootFolder = resolveWorldRootFolder(worldFolder); return new File(rootFolder, "datapacks"); @@ -1043,21 +206,6 @@ public class ServerConfigurator { return worldFolder.getAbsoluteFile(); } - private static String sanitizePackName(String value) { - if (value == null) { - return ""; - } - String sanitized = value.trim().toLowerCase().replace("\\", "/"); - sanitized = sanitized.replaceAll("[^a-z0-9_\\-./]", "_"); - sanitized = sanitized.replaceAll("/+", "/"); - sanitized = sanitized.replaceAll("^/+", ""); - sanitized = sanitized.replaceAll("/+$", ""); - if (sanitized.contains("..")) { - sanitized = sanitized.replace("..", "_"); - } - return sanitized.replace("/", "_"); - } - private static boolean verifyDataPacksPost(boolean allowRestarting) { try (Stream stream = allPacks()) { boolean bad = stream @@ -1177,7 +325,7 @@ public class ServerConfigurator { public static String getWorld(@NonNull IrisData data) { String worldContainer = Bukkit.getWorldContainer().getAbsolutePath(); if (!worldContainer.endsWith(File.separator)) worldContainer += File.separator; - + String path = data.getDataFolder().getAbsolutePath(); if (!path.startsWith(worldContainer)) return null; int l = path.endsWith(File.separator) ? 11 : 10; diff --git a/core/src/main/java/art/arcane/iris/core/StructureNbtJigsawPoolRewriter.java b/core/src/main/java/art/arcane/iris/core/StructureNbtJigsawPoolRewriter.java deleted file mode 100644 index 7842354b7..000000000 --- a/core/src/main/java/art/arcane/iris/core/StructureNbtJigsawPoolRewriter.java +++ /dev/null @@ -1,198 +0,0 @@ -package art.arcane.iris.core; - -import art.arcane.volmlib.util.nbt.io.NBTDeserializer; -import art.arcane.volmlib.util.nbt.io.NBTSerializer; -import art.arcane.volmlib.util.nbt.io.NamedTag; -import art.arcane.volmlib.util.nbt.tag.ByteTag; -import art.arcane.volmlib.util.nbt.tag.CompoundTag; -import art.arcane.volmlib.util.nbt.tag.IntTag; -import art.arcane.volmlib.util.nbt.tag.ListTag; -import art.arcane.volmlib.util.nbt.tag.NumberTag; -import art.arcane.volmlib.util.nbt.tag.ShortTag; -import art.arcane.volmlib.util.nbt.tag.Tag; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -final class StructureNbtJigsawPoolRewriter { - private StructureNbtJigsawPoolRewriter() { - } - - static byte[] rewrite(byte[] bytes, Map remappedKeys) { - if (bytes == null || bytes.length == 0 || remappedKeys == null || remappedKeys.isEmpty()) { - return bytes; - } - - try { - NbtReadResult readResult = readNamedTagWithCompression(bytes); - Tag rootTag = readResult.namedTag().getTag(); - if (!(rootTag instanceof CompoundTag compoundTag)) { - return bytes; - } - - boolean rewritten = rewriteJigsawPoolReferences(compoundTag, remappedKeys); - if (!rewritten) { - return bytes; - } - - return writeNamedTag(readResult.namedTag(), readResult.compressed()); - } catch (Throwable ignored) { - return bytes; - } - } - - private static boolean rewriteJigsawPoolReferences(CompoundTag root, Map remappedKeys) { - ListTag palette = root.getListTag("palette"); - ListTag blocks = root.getListTag("blocks"); - if (palette == null || blocks == null || palette.size() <= 0 || blocks.size() <= 0) { - return false; - } - - Set jigsawStates = new HashSet<>(); - for (int paletteIndex = 0; paletteIndex < palette.size(); paletteIndex++) { - Object paletteRaw = palette.get(paletteIndex); - if (!(paletteRaw instanceof CompoundTag paletteEntry)) { - continue; - } - String blockName = paletteEntry.getString("Name"); - if ("minecraft:jigsaw".equalsIgnoreCase(blockName)) { - jigsawStates.add(paletteIndex); - } - } - - if (jigsawStates.isEmpty()) { - return false; - } - - boolean rewritten = false; - for (Object blockRaw : blocks.getValue()) { - if (!(blockRaw instanceof CompoundTag blockTag)) { - continue; - } - - Integer stateIndex = tagToInt(blockTag.get("state")); - if (stateIndex == null || !jigsawStates.contains(stateIndex)) { - continue; - } - - CompoundTag blockNbt = blockTag.getCompoundTag("nbt"); - if (blockNbt == null || blockNbt.size() <= 0) { - continue; - } - - String poolValue = blockNbt.getString("pool"); - if (poolValue == null || poolValue.isBlank()) { - continue; - } - - String normalizedPool = normalizeResourceKey(poolValue); - if (normalizedPool == null || normalizedPool.isBlank()) { - continue; - } - - String remappedPool = remappedKeys.get(normalizedPool); - if (remappedPool == null || remappedPool.isBlank()) { - continue; - } - - blockNbt.putString("pool", remappedPool); - rewritten = true; - } - - return rewritten; - } - - private static Integer tagToInt(Tag tag) { - if (tag == null) { - return null; - } - if (tag instanceof IntTag intTag) { - return intTag.asInt(); - } - if (tag instanceof ShortTag shortTag) { - return (int) shortTag.asShort(); - } - if (tag instanceof ByteTag byteTag) { - return (int) byteTag.asByte(); - } - if (tag instanceof NumberTag numberTag) { - Number value = numberTag.getValue(); - if (value != null) { - return value.intValue(); - } - } - Object value = tag.getValue(); - if (value instanceof Number number) { - return number.intValue(); - } - return null; - } - - private static String normalizeResourceKey(String value) { - if (value == null) { - return null; - } - - String normalized = value.trim(); - if (normalized.isEmpty()) { - return ""; - } - if (normalized.charAt(0) == '#') { - normalized = normalized.substring(1); - } - - String namespace = "minecraft"; - String path = normalized; - int separator = normalized.indexOf(':'); - if (separator >= 0) { - namespace = normalized.substring(0, separator).trim().toLowerCase(); - path = normalized.substring(separator + 1).trim(); - } - - if (path.startsWith("worldgen/template_pool/")) { - path = path.substring("worldgen/template_pool/".length()); - } - path = path.replace('\\', '/'); - while (path.startsWith("/")) { - path = path.substring(1); - } - while (path.endsWith("/")) { - path = path.substring(0, path.length() - 1); - } - if (path.isEmpty()) { - return ""; - } - - return namespace + ":" + path; - } - - private static NbtReadResult readNamedTagWithCompression(byte[] bytes) throws IOException { - IOException primary = null; - try { - NamedTag uncompressed = new NBTDeserializer(false).fromStream(new ByteArrayInputStream(bytes)); - return new NbtReadResult(uncompressed, false); - } catch (IOException e) { - primary = e; - } - - try { - NamedTag compressed = new NBTDeserializer(true).fromStream(new ByteArrayInputStream(bytes)); - return new NbtReadResult(compressed, true); - } catch (IOException e) { - if (primary != null) { - e.addSuppressed(primary); - } - throw e; - } - } - - private static byte[] writeNamedTag(NamedTag namedTag, boolean compressed) throws IOException { - return new NBTSerializer(compressed).toBytes(namedTag); - } - - private record NbtReadResult(NamedTag namedTag, boolean compressed) { - } -} diff --git a/core/src/main/java/art/arcane/iris/core/VanillaDatapackDumper.java b/core/src/main/java/art/arcane/iris/core/VanillaDatapackDumper.java deleted file mode 100644 index cee6c510a..000000000 --- a/core/src/main/java/art/arcane/iris/core/VanillaDatapackDumper.java +++ /dev/null @@ -1,153 +0,0 @@ -package art.arcane.iris.core; - -import art.arcane.iris.Iris; -import art.arcane.iris.core.nms.INMS; -import art.arcane.iris.core.nms.datapack.DataVersion; -import art.arcane.volmlib.util.collection.KList; -import art.arcane.volmlib.util.json.JSONObject; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.util.Map; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; - -public final class VanillaDatapackDumper { - private static final String DUMP_ZIP_NAME = "00-iris-vanilla-worldgen.zip"; - private static final String MARKER_FILE = "vanilla-datapack-version.txt"; - private static final String PACK_DESCRIPTION = "Iris extracted vanilla worldgen datapack."; - - private VanillaDatapackDumper() { - } - - public static void dumpIfNeeded(KList datapackFolders) { - if (datapackFolders == null || datapackFolders.isEmpty()) { - return; - } - - String currentVersion = resolveVersionKey(); - if (currentVersion == null) { - Iris.warn("Unable to determine server version for vanilla datapack dump."); - return; - } - - boolean needsDump = false; - for (File folder : datapackFolders) { - File zip = new File(folder, DUMP_ZIP_NAME); - File marker = new File(folder, MARKER_FILE); - if (!zip.exists() || !marker.exists() || !currentVersion.equals(readMarker(marker))) { - needsDump = true; - break; - } - } - - if (!needsDump) { - Iris.verbose("Vanilla datapack is up to date, skipping dump."); - return; - } - - Iris.info("Dumping vanilla worldgen datapack..."); - Map entries = INMS.get().extractVanillaDatapack(); - if (entries.isEmpty()) { - Iris.warn("Vanilla datapack extraction returned no entries. Skipping dump."); - return; - } - - byte[] zipBytes = buildZip(entries); - if (zipBytes == null) { - Iris.error("Failed to build vanilla datapack ZIP."); - return; - } - - int written = 0; - for (File folder : datapackFolders) { - folder.mkdirs(); - File zip = new File(folder, DUMP_ZIP_NAME); - File marker = new File(folder, MARKER_FILE); - try { - Files.write(zip.toPath(), zipBytes); - Files.writeString(marker.toPath(), currentVersion, StandardCharsets.UTF_8); - written++; - } catch (IOException e) { - Iris.error("Failed to write vanilla datapack to " + folder.getAbsolutePath()); - e.printStackTrace(); - } - } - - Iris.info("Vanilla datapack written to " + written + " world(s) with " + entries.size() + " entries."); - } - - public static void removeIfPresent(KList datapackFolders) { - if (datapackFolders == null || datapackFolders.isEmpty()) { - return; - } - - int removed = 0; - for (File folder : datapackFolders) { - File zip = new File(folder, DUMP_ZIP_NAME); - File marker = new File(folder, MARKER_FILE); - if (zip.exists() && zip.delete()) { - removed++; - } - if (marker.exists()) { - marker.delete(); - } - } - - if (removed > 0) { - Iris.info("Removed vanilla datapack from " + removed + " world(s) (vanillaStructures disabled)."); - } - } - - private static byte[] buildZip(Map entries) { - try { - java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); - try (ZipOutputStream zos = new ZipOutputStream(baos)) { - zos.putNextEntry(new ZipEntry("pack.mcmeta")); - zos.write(buildPackMeta()); - zos.closeEntry(); - - for (Map.Entry entry : entries.entrySet()) { - zos.putNextEntry(new ZipEntry(entry.getKey())); - zos.write(entry.getValue()); - zos.closeEntry(); - } - } - return baos.toByteArray(); - } catch (IOException e) { - Iris.error("Failed to build vanilla datapack ZIP"); - e.printStackTrace(); - return null; - } - } - - private static byte[] buildPackMeta() { - int packFormat = INMS.get().getDataVersion().getPackFormat(); - JSONObject root = new JSONObject(); - JSONObject pack = new JSONObject(); - pack.put("description", PACK_DESCRIPTION); - pack.put("pack_format", packFormat); - root.put("pack", pack); - return root.toString(4).getBytes(StandardCharsets.UTF_8); - } - - private static String resolveVersionKey() { - try { - DataVersion dv = INMS.get().getDataVersion(); - return dv.getVersion() + ":" + dv.getPackFormat(); - } catch (Exception e) { - return null; - } - } - - private static String readMarker(File marker) { - try { - return Files.readString(marker.toPath(), StandardCharsets.UTF_8).trim(); - } catch (IOException e) { - return null; - } - } -} diff --git a/core/src/main/java/art/arcane/iris/core/commands/CommandDeveloper.java b/core/src/main/java/art/arcane/iris/core/commands/CommandDeveloper.java index e48220841..a07485b6f 100644 --- a/core/src/main/java/art/arcane/iris/core/commands/CommandDeveloper.java +++ b/core/src/main/java/art/arcane/iris/core/commands/CommandDeveloper.java @@ -43,6 +43,7 @@ import art.arcane.iris.util.project.context.IrisContext; import art.arcane.volmlib.util.collection.KList; import art.arcane.iris.util.common.director.DirectorContext; import art.arcane.iris.util.common.director.DirectorExecutor; +import art.arcane.iris.util.common.director.DirectorHelp; import art.arcane.volmlib.util.director.DirectorOrigin; import art.arcane.volmlib.util.director.annotations.Director; import art.arcane.volmlib.util.director.annotations.Param; @@ -90,10 +91,10 @@ import java.util.zip.GZIPOutputStream; @Director(name = "Developer", origin = DirectorOrigin.BOTH, description = "Iris World Manager", aliases = {"dev"}) public class CommandDeveloper implements DirectorExecutor { - private static final long DELETE_CHUNK_HEARTBEAT_MS = 5000L; - private static final int DELETE_CHUNK_MAX_ATTEMPTS = 2; - private static final int DELETE_CHUNK_STACK_LIMIT = 20; - private static final Set ACTIVE_DELETE_CHUNK_WORLDS = ConcurrentHashMap.newKeySet(); + @Director(description = "Show help tree for this command group", aliases = {"?"}) + public void help() { + DirectorHelp.print(sender(), getClass()); + } @Director(description = "Get Loaded TectonicPlates Count", origin = DirectorOrigin.BOTH, sync = true) public void EngineStatus() { @@ -185,7 +186,7 @@ public class CommandDeveloper implements DirectorExecutor { folder.mkdirs(); if (freshDownload) { - Iris.service(StudioSVC.class).downloadSearch(sender(), pack.getLoadKey(), false, true); + Iris.service(StudioSVC.class).downloadSearch(sender(), pack.getLoadKey(), true); } Iris.service(StudioSVC.class).installIntoWorld(sender(), pack.getLoadKey(), folder); @@ -294,53 +295,6 @@ public class CommandDeveloper implements DirectorExecutor { Files.write(target.toPath(), bytes); } - @Director(description = "Test") - public void dumpThreads() { - try { - File fi = Iris.instance.getDataFile("dump", "td-" + new java.sql.Date(M.ms()) + ".txt"); - FileOutputStream fos = new FileOutputStream(fi); - Map f = Thread.getAllStackTraces(); - PrintWriter pw = new PrintWriter(fos); - - pw.println(Thread.activeCount() + "/" + f.size()); - var run = Runtime.getRuntime(); - pw.println("Memory:"); - pw.println("\tMax: " + run.maxMemory()); - pw.println("\tTotal: " + run.totalMemory()); - pw.println("\tFree: " + run.freeMemory()); - pw.println("\tUsed: " + (run.totalMemory() - run.freeMemory())); - - for (Thread i : f.keySet()) { - pw.println("========================================"); - pw.println("Thread: '" + i.getName() + "' ID: " + i.threadId() + " STATUS: " + i.getState().name()); - - for (StackTraceElement j : f.get(i)) { - pw.println(" @ " + j.toString()); - } - - pw.println("========================================"); - pw.println(); - pw.println(); - } - - pw.close(); - Iris.info("DUMPED! See " + fi.getAbsolutePath()); - } catch (Throwable e) { - e.printStackTrace(); - } - } - - @Director(description = "Generate Iris structures for all loaded datapack structures") - public void generateStructures( - @Param(description = "The pack to add the generated structures to", aliases = "pack", defaultValue = "null", customHandler = NullableDimensionHandler.class) - IrisDimension dimension, - @Param(description = "Ignore existing structures", defaultValue = "false") - boolean force - ) { - sender().sendMessage(C.YELLOW + "Legacy structure conversion hooks have been removed."); - sender().sendMessage(C.YELLOW + "Use intrinsic structure generation and datapack ingestion instead."); - } - @Director(description = "Test") public void packBenchmark( @Param(description = "The pack to bench", aliases = {"pack"}, defaultValue = "overworld") @@ -378,9 +332,7 @@ public class CommandDeveloper implements DirectorExecutor { @Director(description = "Delete nearby chunk blocks for regen testing", name = "delete-chunk", aliases = {"delchunk", "dc"}, origin = DirectorOrigin.PLAYER, sync = true) public void deleteChunk( @Param(description = "Radius in chunks around your current chunk", defaultValue = "0") - int radius, - @Param(description = "How many chunks to process in parallel (0 = auto)", aliases = {"threads", "concurrency"}, defaultValue = "0") - int parallelism + int radius ) { if (radius < 0) { sender().sendMessage(C.RED + "Radius must be 0 or greater."); @@ -392,536 +344,54 @@ public class CommandDeveloper implements DirectorExecutor { sender().sendMessage(C.RED + "This is not an Iris world."); return; } - String worldKey = world.getName().toLowerCase(Locale.ROOT); - if (!ACTIVE_DELETE_CHUNK_WORLDS.add(worldKey)) { - sender().sendMessage(C.RED + "A delete-chunk run is already active for this world."); - return; - } - int threads = resolveDeleteChunkThreadCount(parallelism); - int centerX = player().getLocation().getBlockX() >> 4; - int centerZ = player().getLocation().getBlockZ() >> 4; - List targets = buildDeleteChunkTargets(centerX, centerZ, radius); - int totalChunks = targets.size(); - String runId = world.getName() + "-" + System.currentTimeMillis(); PlatformChunkGenerator access = IrisToolbelt.access(world); if (access == null || access.getEngine() == null) { - ACTIVE_DELETE_CHUNK_WORLDS.remove(worldKey); sender().sendMessage(C.RED + "The engine access for this world is null."); return; } art.arcane.volmlib.util.mantle.runtime.Mantle mantle = access.getEngine().getMantle().getMantle(); - VolmitSender sender = sender(); + int centerX = player().getLocation().getBlockX() >> 4; + int centerZ = player().getLocation().getBlockZ() >> 4; + int minY = world.getMinHeight(); + int maxY = world.getMaxHeight(); + int total = (radius * 2 + 1) * (radius * 2 + 1); + int processed = 0; + int failed = 0; - sender.sendMessage(C.GREEN + "Deleting blocks in " + C.GOLD + totalChunks + C.GREEN + " chunk(s) with " + C.GOLD + threads + C.GREEN + " worker(s)."); - if (J.isFolia()) { - sender.sendMessage(C.YELLOW + "Folia maintenance mode enabled for lock-safe chunk wipe + mantle purge."); - } - sender.sendMessage(C.YELLOW + "Delete-chunk run id: " + C.GOLD + runId + C.YELLOW + "."); - Iris.info("Delete-chunk run start: id=" + runId - + " world=" + world.getName() - + " center=" + centerX + "," + centerZ - + " radius=" + radius - + " workers=" + threads - + " chunks=" + totalChunks); - - Set workerThreads = ConcurrentHashMap.newKeySet(); - AtomicInteger workerCounter = new AtomicInteger(); - ThreadFactory threadFactory = runnable -> { - Thread thread = new Thread(runnable, "Iris-DeleteChunk-" + runId + "-" + workerCounter.incrementAndGet()); - thread.setDaemon(true); - workerThreads.add(thread); - return thread; - }; - - Thread orchestrator = new Thread(() -> runDeleteChunkOrchestrator( - sender, - world, - mantle, - targets, - threads, - runId, - worldKey, - workerThreads, - threadFactory - ), "Iris-DeleteChunk-Orchestrator-" + runId); - orchestrator.setDaemon(true); - try { - orchestrator.start(); - if (IrisSettings.get().getGeneral().isDebug()) { - Iris.info("Delete-chunk worker dispatched on dedicated thread=" + orchestrator.getName() + " id=" + runId + "."); - } - } catch (Throwable e) { - ACTIVE_DELETE_CHUNK_WORLDS.remove(worldKey); - sender.sendMessage(C.RED + "Failed to start delete-chunk worker thread. See console."); - Iris.reportError(e); - } - } - - private int resolveDeleteChunkThreadCount(int parallelism) { - int threads = parallelism <= 0 ? Runtime.getRuntime().availableProcessors() : parallelism; - if (J.isFolia() && parallelism <= 0) { - threads = 1; - } - return Math.max(1, threads); - } - - private List buildDeleteChunkTargets(int centerX, int centerZ, int radius) { - int expected = (radius * 2 + 1) * (radius * 2 + 1); - List targets = new ArrayList<>(expected); - for (int ring = 0; ring <= radius; ring++) { - for (int x = -ring; x <= ring; x++) { - for (int z = -ring; z <= ring; z++) { - if (Math.max(Math.abs(x), Math.abs(z)) != ring) { - continue; - } - targets.add(new Position2(centerX + x, centerZ + z)); - } - } - } - return targets; - } - - private void runDeleteChunkOrchestrator( - VolmitSender sender, - World world, - art.arcane.volmlib.util.mantle.runtime.Mantle mantle, - List targets, - int threadCount, - String runId, - String worldKey, - Set workerThreads, - ThreadFactory threadFactory - ) { - long runStart = System.currentTimeMillis(); - AtomicReference phase = new AtomicReference<>("bootstrap"); - AtomicLong phaseSince = new AtomicLong(runStart); - AtomicBoolean runDone = new AtomicBoolean(false); - Thread watchdog = createDeleteChunkSetupWatchdog(world, runId, runDone, phase, phaseSince); - watchdog.start(); - - IrisToolbelt.beginWorldMaintenance(world, "delete-chunk"); - try (ThreadPoolExecutor pool = (ThreadPoolExecutor) Executors.newFixedThreadPool(threadCount, threadFactory)) { - setDeleteChunkPhase(phase, phaseSince, "dispatch", world, runId); - DeleteChunkSummary summary = executeDeleteChunkQueue(world, mantle, targets, pool, workerThreads, runId); - if (summary.failedChunks() <= 0) { - sender.sendMessage(C.GREEN + "Deleted blocks in " + C.GOLD + summary.successChunks() + C.GREEN + "/" + C.GOLD + summary.totalChunks() + C.GREEN + " chunk(s)."); - return; - } - - sender.sendMessage(C.RED + "Delete-chunk completed with " + C.GOLD + summary.failedChunks() + C.RED + " failed chunk(s)."); - sender.sendMessage(C.YELLOW + "Successful chunks: " + C.GOLD + summary.successChunks() + C.YELLOW + "/" + C.GOLD + summary.totalChunks() + C.YELLOW + "."); - sender.sendMessage(C.YELLOW + "Retry attempts used: " + C.GOLD + summary.retryCount() + C.YELLOW + "."); - if (!summary.failedPreview().isEmpty()) { - sender.sendMessage(C.YELLOW + "Failed chunks sample: " + C.GOLD + summary.failedPreview()); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - sender.sendMessage(C.RED + "Delete-chunk run was interrupted."); - Iris.warn("Delete-chunk run interrupted: id=" + runId + " world=" + world.getName()); - } catch (Throwable e) { - sender.sendMessage(C.RED + "Delete-chunk run failed. See console."); - Iris.reportError(e); - } finally { - runDone.set(true); - watchdog.interrupt(); - IrisToolbelt.endWorldMaintenance(world, "delete-chunk"); - ACTIVE_DELETE_CHUNK_WORLDS.remove(worldKey); - if (IrisSettings.get().getGeneral().isDebug()) { - Iris.info("Delete-chunk run closed: id=" + runId + " world=" + world.getName() + " totalMs=" + (System.currentTimeMillis() - runStart)); - } - } - } - - private DeleteChunkSummary executeDeleteChunkQueue( - World world, - art.arcane.volmlib.util.mantle.runtime.Mantle mantle, - List targets, - ThreadPoolExecutor pool, - Set workerThreads, - String runId - ) throws InterruptedException { - ArrayDeque pending = new ArrayDeque<>(targets.size()); - long queuedAt = System.currentTimeMillis(); - for (Position2 target : targets) { - pending.addLast(new DeleteChunkTask(target.getX(), target.getZ(), 1, queuedAt)); - } - - ConcurrentMap activeTasks = new ConcurrentHashMap<>(); - ExecutorCompletionService completion = new ExecutorCompletionService<>(pool); - List failedChunks = new ArrayList<>(); - - int totalChunks = targets.size(); - int successChunks = 0; - int failedCount = 0; - int retryCount = 0; - long submittedTasks = 0L; - long finishedTasks = 0L; - int completedChunks = 0; - int inFlight = 0; - int unchangedHeartbeats = 0; - int lastCompleted = -1; - long lastDump = 0L; - - while (inFlight < pool.getMaximumPoolSize() && !pending.isEmpty()) { - DeleteChunkTask task = pending.removeFirst(); - completion.submit(() -> runDeleteChunkTask(task, world, mantle, activeTasks)); - inFlight++; - submittedTasks++; - } - - while (completedChunks < totalChunks) { - Future future = completion.poll(DELETE_CHUNK_HEARTBEAT_MS, TimeUnit.MILLISECONDS); - if (future == null) { - if (completedChunks == lastCompleted) { - unchangedHeartbeats++; - } else { - unchangedHeartbeats = 0; - lastCompleted = completedChunks; - } - - Iris.warn("Delete-chunk heartbeat: id=" + runId - + " completed=" + completedChunks + "/" + totalChunks - + " remaining=" + (totalChunks - completedChunks) - + " queued=" + pending.size() - + " inFlight=" + inFlight - + " submitted=" + submittedTasks - + " finishedTasks=" + finishedTasks - + " retries=" + retryCount - + " failed=" + failedCount - + " poolActive=" + pool.getActiveCount() - + " poolQueue=" + pool.getQueue().size() - + " poolDone=" + pool.getCompletedTaskCount() - + " activeTasks=" + formatDeleteChunkActiveTasks(activeTasks)); - - if (unchangedHeartbeats >= 3 && System.currentTimeMillis() - lastDump >= 10000L) { - lastDump = System.currentTimeMillis(); - Iris.warn("Delete-chunk appears stalled; dumping worker stack traces for id=" + runId + "."); - dumpDeleteChunkWorkerStacks(workerThreads, world.getName()); - } - continue; - } - - DeleteChunkResult result; - try { - result = future.get(); - } catch (ExecutionException e) { - Throwable cause = e.getCause() == null ? e : e.getCause(); - throw new IllegalStateException("Delete-chunk worker failed unexpectedly for run " + runId, cause); - } - - inFlight--; - finishedTasks++; - long duration = result.finishedAtMs() - result.startedAtMs(); - - if (result.success()) { - completedChunks++; - successChunks++; - if (result.task().attempt() > 1) { - Iris.warn("Delete-chunk recovered after retry: id=" + runId - + " chunk=" + result.task().chunkX() + "," + result.task().chunkZ() - + " attempt=" + result.task().attempt() - + " durationMs=" + duration); - } else if (duration >= 5000L) { - Iris.warn("Delete-chunk slow: id=" + runId - + " chunk=" + result.task().chunkX() + "," + result.task().chunkZ() - + " durationMs=" + duration - + " loadedAtStart=" + result.loadedAtStart()); - } - } else if (result.task().attempt() < DELETE_CHUNK_MAX_ATTEMPTS) { - retryCount++; - DeleteChunkTask retryTask = result.task().retry(System.currentTimeMillis()); - pending.addLast(retryTask); - Iris.warn("Delete-chunk retry scheduled: id=" + runId - + " chunk=" + result.task().chunkX() + "," + result.task().chunkZ() - + " failedAttempt=" + result.task().attempt() - + " nextAttempt=" + retryTask.attempt() - + " error=" + result.errorSummary()); - } else { - completedChunks++; - failedCount++; - Position2 failed = new Position2(result.task().chunkX(), result.task().chunkZ()); - failedChunks.add(failed); - Iris.warn("Delete-chunk terminal failure: id=" + runId - + " chunk=" + result.task().chunkX() + "," + result.task().chunkZ() - + " attempts=" + result.task().attempt() - + " error=" + result.errorSummary()); - if (result.error() != null) { - Iris.reportError(result.error()); - } - } - - while (inFlight < pool.getMaximumPoolSize() && !pending.isEmpty()) { - DeleteChunkTask task = pending.removeFirst(); - completion.submit(() -> runDeleteChunkTask(task, world, mantle, activeTasks)); - inFlight++; - submittedTasks++; - } - } - - String preview = formatDeleteChunkFailedPreview(failedChunks); - Iris.info("Delete-chunk run complete: id=" + runId - + " world=" + world.getName() - + " total=" + totalChunks - + " success=" + successChunks - + " failed=" + failedCount - + " retries=" + retryCount - + " submittedTasks=" + submittedTasks - + " finishedTasks=" + finishedTasks - + " failedPreview=" + preview); - return new DeleteChunkSummary(totalChunks, successChunks, failedCount, retryCount, preview); - } - - private DeleteChunkResult runDeleteChunkTask( - DeleteChunkTask task, - World world, - art.arcane.volmlib.util.mantle.runtime.Mantle mantle, - ConcurrentMap activeTasks - ) { - String worker = Thread.currentThread().getName(); - long startedAt = System.currentTimeMillis(); - boolean loadedAtStart = false; - try { - loadedAtStart = world.isChunkLoaded(task.chunkX(), task.chunkZ()); - } catch (Throwable ignored) { - } - - activeTasks.put(worker, new DeleteChunkActiveTask(task.chunkX(), task.chunkZ(), task.attempt(), startedAt, loadedAtStart)); - try { - DeleteChunkRegionResult regionResult = wipeChunkRegion(world, task.chunkX(), task.chunkZ()); - if (!regionResult.success()) { - return DeleteChunkResult.failure(task, worker, startedAt, System.currentTimeMillis(), loadedAtStart, regionResult.error()); - } - mantle.deleteChunk(task.chunkX(), task.chunkZ()); - return DeleteChunkResult.success(task, worker, startedAt, System.currentTimeMillis(), loadedAtStart); - } catch (Throwable e) { - if (e instanceof InterruptedException) { - Thread.currentThread().interrupt(); - } - return DeleteChunkResult.failure(task, worker, startedAt, System.currentTimeMillis(), loadedAtStart, e); - } finally { - activeTasks.remove(worker); - } - } - - private DeleteChunkRegionResult wipeChunkRegion(World world, int chunkX, int chunkZ) throws InterruptedException { - CountDownLatch latch = new CountDownLatch(1); - AtomicReference failure = new AtomicReference<>(); - if (!J.runRegion(world, chunkX, chunkZ, () -> { - try { - Chunk chunk = world.getChunkAt(chunkX, chunkZ); - for (org.bukkit.entity.Entity entity : chunk.getEntities()) { - if (!(entity instanceof org.bukkit.entity.Player)) { - entity.remove(); - } - } - - int minY = world.getMinHeight(); - int maxY = world.getMaxHeight(); - for (int xx = 0; xx < 16; xx++) { - for (int zz = 0; zz < 16; zz++) { - for (int yy = minY; yy < maxY; yy++) { - chunk.getBlock(xx, yy, zz).setType(org.bukkit.Material.AIR, false); + for (int x = -radius; x <= radius; x++) { + for (int z = -radius; z <= radius; z++) { + int chunkX = centerX + x; + int chunkZ = centerZ + z; + try { + Chunk chunk = world.getChunkAt(chunkX, chunkZ); + for (org.bukkit.entity.Entity entity : chunk.getEntities()) { + if (!(entity instanceof Player)) { + entity.remove(); } } - } - } catch (Throwable e) { - failure.set(e); - } finally { - latch.countDown(); - } - })) { - return DeleteChunkRegionResult.fail(new IllegalStateException("Failed to schedule region task for chunk " + chunkX + "," + chunkZ)); - } - - if (!latch.await(30, TimeUnit.SECONDS)) { - return DeleteChunkRegionResult.fail(new TimeoutException("Timed out waiting for region task at chunk " + chunkX + "," + chunkZ)); - } - - Throwable thrown = failure.get(); - if (thrown != null) { - return DeleteChunkRegionResult.fail(thrown); - } - return DeleteChunkRegionResult.ok(); - } - - private Thread createDeleteChunkSetupWatchdog( - World world, - String runId, - AtomicBoolean runDone, - AtomicReference phase, - AtomicLong phaseSince - ) { - Thread watchdog = new Thread(() -> { - while (!runDone.get()) { - try { - Thread.sleep(DELETE_CHUNK_HEARTBEAT_MS); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - return; - } - - if (!runDone.get()) { - long elapsed = System.currentTimeMillis() - phaseSince.get(); - Iris.warn("Delete-chunk setup heartbeat: id=" + runId - + " phase=" + phase.get() - + " elapsedMs=" + elapsed - + " world=" + world.getName()); + for (int xx = 0; xx < 16; xx++) { + for (int zz = 0; zz < 16; zz++) { + for (int yy = minY; yy < maxY; yy++) { + chunk.getBlock(xx, yy, zz).setType(org.bukkit.Material.AIR, false); + } + } + } + mantle.deleteChunk(chunkX, chunkZ); + processed++; + } catch (Throwable e) { + failed++; + Iris.reportError(e); } } - }, "Iris-DeleteChunk-SetupWatchdog-" + runId); - watchdog.setDaemon(true); - return watchdog; - } - - private void setDeleteChunkPhase( - AtomicReference phase, - AtomicLong phaseSince, - String next, - World world, - String runId - ) { - phase.set(next); - phaseSince.set(System.currentTimeMillis()); - if (IrisSettings.get().getGeneral().isDebug()) { - Iris.info("Delete-chunk phase: id=" + runId + " phase=" + next + " world=" + world.getName()); - } - } - - private String formatDeleteChunkFailedPreview(List failedChunks) { - if (failedChunks.isEmpty()) { - return "[]"; - } - StringBuilder builder = new StringBuilder("["); - int index = 0; - for (Position2 chunk : failedChunks) { - if (index > 0) { - builder.append(", "); - } - if (index >= 10) { - builder.append("..."); - break; - } - builder.append(chunk.getX()).append(",").append(chunk.getZ()); - index++; - } - builder.append("]"); - return builder.toString(); - } - - private String formatDeleteChunkActiveTasks(ConcurrentMap activeTasks) { - if (activeTasks.isEmpty()) { - return "{}"; } - StringBuilder builder = new StringBuilder("{"); - int count = 0; - long now = System.currentTimeMillis(); - for (Map.Entry entry : activeTasks.entrySet()) { - if (count > 0) { - builder.append(", "); - } - if (count >= 8) { - builder.append("..."); - break; - } - DeleteChunkActiveTask activeTask = entry.getValue(); - builder.append(entry.getKey()) - .append("=") - .append(activeTask.chunkX()) - .append(",") - .append(activeTask.chunkZ()) - .append("@") - .append(activeTask.attempt()) - .append("/") - .append(now - activeTask.startedAtMs()) - .append("ms") - .append(activeTask.loadedAtStart() ? ":loaded" : ":cold"); - count++; + if (failed == 0) { + sender().sendMessage(C.GREEN + "Deleted blocks in " + C.GOLD + processed + C.GREEN + "/" + C.GOLD + total + C.GREEN + " chunk(s)."); + } else { + sender().sendMessage(C.YELLOW + "Deleted blocks in " + C.GOLD + processed + C.YELLOW + "/" + C.GOLD + total + C.YELLOW + " chunk(s); " + C.RED + failed + C.YELLOW + " failed."); } - builder.append("}"); - return builder.toString(); - } - - private void dumpDeleteChunkWorkerStacks(Set explicitThreads, String worldName) { - Set threads = new LinkedHashSet<>(); - threads.addAll(explicitThreads); - for (Thread thread : Thread.getAllStackTraces().keySet()) { - if (thread == null || !thread.isAlive()) { - continue; - } - String name = thread.getName(); - if (name.startsWith("Iris-DeleteChunk-") - || name.startsWith("Iris EngineSVC-") - || name.startsWith("Iris World Manager") - || name.contains(worldName)) { - threads.add(thread); - } - } - - for (Thread thread : threads) { - if (thread == null || !thread.isAlive()) { - continue; - } - Iris.warn("Delete-chunk worker thread=" + thread.getName() + " state=" + thread.getState()); - StackTraceElement[] trace = thread.getStackTrace(); - int limit = Math.min(trace.length, DELETE_CHUNK_STACK_LIMIT); - for (int i = 0; i < limit; i++) { - Iris.warn(" at " + trace[i]); - } - } - } - - private record DeleteChunkTask(int chunkX, int chunkZ, int attempt, long queuedAtMs) { - private DeleteChunkTask retry(long now) { - return new DeleteChunkTask(chunkX, chunkZ, attempt + 1, now); - } - } - - private record DeleteChunkActiveTask(int chunkX, int chunkZ, int attempt, long startedAtMs, boolean loadedAtStart) { - } - - private record DeleteChunkResult( - DeleteChunkTask task, - String worker, - long startedAtMs, - long finishedAtMs, - boolean loadedAtStart, - boolean success, - Throwable error - ) { - private static DeleteChunkResult success(DeleteChunkTask task, String worker, long startedAtMs, long finishedAtMs, boolean loadedAtStart) { - return new DeleteChunkResult(task, worker, startedAtMs, finishedAtMs, loadedAtStart, true, null); - } - - private static DeleteChunkResult failure(DeleteChunkTask task, String worker, long startedAtMs, long finishedAtMs, boolean loadedAtStart, Throwable error) { - return new DeleteChunkResult(task, worker, startedAtMs, finishedAtMs, loadedAtStart, false, error); - } - - private String errorSummary() { - if (error == null) { - return "unknown"; - } - String message = error.getMessage(); - if (message == null || message.isEmpty()) { - return error.getClass().getSimpleName(); - } - return error.getClass().getSimpleName() + ": " + message; - } - } - - private record DeleteChunkRegionResult(boolean success, Throwable error) { - private static DeleteChunkRegionResult ok() { - return new DeleteChunkRegionResult(true, null); - } - - private static DeleteChunkRegionResult fail(Throwable error) { - return new DeleteChunkRegionResult(false, error); - } - } - - private record DeleteChunkSummary(int totalChunks, int successChunks, int failedChunks, int retryCount, String failedPreview) { } @Director(description = "UnloadChunks for good reasons.") diff --git a/core/src/main/java/art/arcane/iris/core/commands/CommandEdit.java b/core/src/main/java/art/arcane/iris/core/commands/CommandEdit.java index 5493c1604..2e4ea844d 100644 --- a/core/src/main/java/art/arcane/iris/core/commands/CommandEdit.java +++ b/core/src/main/java/art/arcane/iris/core/commands/CommandEdit.java @@ -22,6 +22,7 @@ import art.arcane.iris.Iris; import art.arcane.iris.core.service.StudioSVC; import art.arcane.iris.engine.object.*; import art.arcane.iris.util.common.director.DirectorExecutor; +import art.arcane.iris.util.common.director.DirectorHelp; import art.arcane.volmlib.util.director.DirectorOrigin; import art.arcane.volmlib.util.director.annotations.Director; import art.arcane.volmlib.util.director.annotations.Param; @@ -32,6 +33,10 @@ import java.awt.*; @Director(name = "edit", origin = DirectorOrigin.PLAYER, studio = true, description = "Edit something") public class CommandEdit implements DirectorExecutor { + @Director(description = "Show help tree for this command group", aliases = {"?"}) + public void help() { + DirectorHelp.print(sender(), getClass()); + } private boolean noStudio() { if (!sender().isPlayer()) { diff --git a/core/src/main/java/art/arcane/iris/core/commands/CommandFind.java b/core/src/main/java/art/arcane/iris/core/commands/CommandFind.java deleted file mode 100644 index f7bcbb88e..000000000 --- a/core/src/main/java/art/arcane/iris/core/commands/CommandFind.java +++ /dev/null @@ -1,174 +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.core.commands; - -import art.arcane.iris.Iris; -import art.arcane.iris.core.ExternalDataPackPipeline; -import art.arcane.iris.core.service.ObjectStudioSaveService; -import art.arcane.iris.engine.framework.Engine; -import art.arcane.iris.engine.object.IrisBiome; -import art.arcane.iris.engine.object.IrisRegion; -import art.arcane.iris.util.common.director.DirectorExecutor; -import art.arcane.volmlib.util.director.DirectorOrigin; -import art.arcane.volmlib.util.director.annotations.Director; -import art.arcane.volmlib.util.director.annotations.Param; -import art.arcane.iris.util.common.director.specialhandlers.ObjectHandler; -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 org.bukkit.Bukkit; -import org.bukkit.entity.Player; - -import java.util.Set; - -@Director(name = "find", origin = DirectorOrigin.PLAYER, description = "Iris Find commands", aliases = "goto") -public class CommandFind implements DirectorExecutor { - @Director(description = "Find a biome") - public void biome( - @Param(description = "The biome to look for") - IrisBiome biome, - @Param(description = "Should you be teleported", defaultValue = "true") - boolean teleport - ) { - Engine e = engine(); - - if (e == null) { - sender().sendMessage(C.GOLD + "Not in an Iris World!"); - return; - } - - e.gotoBiome(biome, player(), teleport); - } - - @Director(description = "Find a region") - public void region( - @Param(description = "The region to look for") - IrisRegion region, - @Param(description = "Should you be teleported", defaultValue = "true") - boolean teleport - ) { - Engine e = engine(); - - if (e == null) { - sender().sendMessage(C.GOLD + "Not in an Iris World!"); - return; - } - - e.gotoRegion(region, player(), teleport); - } - - @Director(description = "Find a point of interest.") - public void poi( - @Param(description = "The type of PoI to look for.") - String type, - @Param(description = "Should you be teleported", defaultValue = "true") - boolean teleport - ) { - Engine e = engine(); - if (e == null) { - sender().sendMessage(C.GOLD + "Not in an Iris World!"); - return; - } - - e.gotoPOI(type, player(), teleport); - } - - @Director(description = "Find an object") - public void object( - @Param(description = "The object to look for", customHandler = ObjectHandler.class) - String object, - @Param(description = "Should you be teleported", defaultValue = "true") - boolean teleport - ) { - Engine e = engine(); - - if (e == null) { - sender().sendMessage(C.GOLD + "Not in an Iris World!"); - return; - } - - Player studioPlayer = player(); - if (studioPlayer != null) { - try { - if (ObjectStudioSaveService.get().teleportTo(studioPlayer, object)) { - sender().sendMessage(C.GREEN + "Object Studio: teleporting to " + object); - return; - } - } catch (Throwable t) { - Iris.reportError(t); - } - } - - if (e.hasObjectPlacement(object)) { - e.gotoObject(object, player(), teleport); - return; - } - - Set structures = ExternalDataPackPipeline.resolveLocateStructuresForObjectKey(object); - VolmitSender commandSender = sender(); - if (structures.isEmpty()) { - if (commandSender != null) { - commandSender.sendMessage(C.RED + object + " is not configured in any region/biome object placements and has no external structure mapping."); - commandSender.sendMessage(C.GRAY + "Try /iris locateexternal for external structure lookups."); - } - return; - } - - Player target = player(); - if (target == null) { - if (commandSender != null) { - commandSender.sendMessage(C.RED + "No active player sender was available for object lookup."); - } - return; - } - - Runnable dispatchTask = () -> { - int dispatched = 0; - for (String structure : structures) { - String command = "locate structure " + structure; - boolean accepted = Bukkit.dispatchCommand(target, command); - if (!accepted) { - if (commandSender != null) { - commandSender.sendMessage(C.RED + "Failed to dispatch: /" + command); - } - } else { - if (commandSender != null) { - commandSender.sendMessage(C.GREEN + "Dispatched: /" + command); - } - dispatched++; - } - } - - if (teleport) { - if (commandSender != null) { - commandSender.sendMessage(C.YELLOW + "External object lookups are structure-backed and dispatch locate commands instead of direct teleport."); - } - } - if (commandSender != null) { - commandSender.sendMessage(C.GREEN + "External object mapping matched locateTargets=" + structures.size() + ", dispatched=" + dispatched + "."); - } - }; - - if (!J.runEntity(target, dispatchTask)) { - if (commandSender != null) { - commandSender.sendMessage(C.RED + "Failed to schedule external object locate dispatch on your region thread."); - } - } - } -} 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 9bfcce437..7413fa0c2 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 @@ -19,7 +19,6 @@ package art.arcane.iris.core.commands; import art.arcane.iris.Iris; -import art.arcane.iris.core.ExternalDataPackPipeline; import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.IrisWorlds; import art.arcane.iris.core.lifecycle.WorldLifecycleService; @@ -29,7 +28,6 @@ import art.arcane.iris.core.service.StudioSVC; import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.object.IrisDimension; -import art.arcane.iris.engine.object.IrisExternalDatapack; import art.arcane.iris.engine.platform.ChunkReplacementListener; import art.arcane.iris.engine.platform.ChunkReplacementOptions; import art.arcane.iris.engine.platform.PlatformChunkGenerator; @@ -37,11 +35,11 @@ import art.arcane.volmlib.util.collection.KList; import art.arcane.iris.util.common.director.DirectorContext; import art.arcane.volmlib.util.director.DirectorParameterHandler; import art.arcane.iris.util.common.director.DirectorExecutor; +import art.arcane.iris.util.common.director.DirectorHelp; import art.arcane.volmlib.util.director.DirectorOrigin; import art.arcane.volmlib.util.director.annotations.Director; import art.arcane.volmlib.util.director.annotations.Param; import art.arcane.volmlib.util.director.exceptions.DirectorParsingException; -import art.arcane.iris.util.common.director.specialhandlers.ExternalDatapackLocateHandler; import art.arcane.iris.util.common.director.specialhandlers.NullablePlayerHandler; import art.arcane.iris.util.common.format.C; import art.arcane.volmlib.util.io.IO; @@ -90,13 +88,17 @@ import static org.bukkit.Bukkit.getServer; @Director(name = "iris", aliases = {"ir", "irs"}, description = "Basic Command") public class CommandIris implements DirectorExecutor { + @Director(description = "Show help tree for this command group", aliases = {"?"}) + public void help() { + DirectorHelp.print(sender(), getClass()); + } + private CommandStudio studio; private CommandPregen pregen; private CommandSettings settings; private CommandObject object; private CommandWhat what; private CommandEdit edit; - private CommandFind find; private CommandDeveloper developer; private CommandPack pack; public static boolean worldCreation = false; @@ -449,170 +451,6 @@ public class CommandIris implements DirectorExecutor { sender().sendMessage(C.GREEN + "Iris v" + Iris.instance.getDescription().getVersion() + " by Volmit Software"); } - @Director(description = "Locate structure targets mapped to an external datapack id in this world's dimension config", aliases = {"locateexternal", "locateext"}, origin = DirectorOrigin.PLAYER, sync = true) - public void locateExternal( - @Param(description = "External datapack id or structure id (comma separated values supported)", customHandler = ExternalDatapackLocateHandler.class) - String id, - @Param(description = "Run locate for every structure mapped to the id(s)", defaultValue = "true") - boolean all - ) { - if (id == null || id.trim().isBlank()) { - sender().sendMessage(C.RED + "You must provide an external datapack id."); - return; - } - - Engine activeEngine = engine(); - if (activeEngine == null || activeEngine.getDimension() == null) { - sender().sendMessage(C.RED + "You must be in an Iris world to use locateexternal."); - return; - } - - IrisDimension dimension = activeEngine.getDimension(); - KList externalDatapacks = dimension.getExternalDatapacks(); - if (externalDatapacks == null || externalDatapacks.isEmpty()) { - sender().sendMessage(C.RED + "This dimension has no externalDatapacks entries."); - return; - } - - LinkedHashSet requestedTokens = new LinkedHashSet<>(); - for (String token : id.split(",")) { - if (token == null) { - continue; - } - - String normalizedToken = normalizeLocateExternalToken(token); - if (!normalizedToken.isBlank()) { - requestedTokens.add(normalizedToken); - } - } - - if (requestedTokens.isEmpty()) { - sender().sendMessage(C.RED + "No valid external datapack ids or structure ids were provided."); - return; - } - - Map> fallbackById = buildExternalLocateFallbackById(externalDatapacks); - LinkedHashSet structures = new LinkedHashSet<>(); - LinkedHashSet matchedIds = new LinkedHashSet<>(); - for (String token : requestedTokens) { - Set resolvedStructures = ExternalDataPackPipeline.resolveLocateStructuresForId(token); - if (resolvedStructures.isEmpty()) { - Set fallbackStructures = fallbackById.get(token); - if (fallbackStructures != null) { - resolvedStructures = fallbackStructures; - } - } - - if (!resolvedStructures.isEmpty()) { - matchedIds.add(token); - structures.addAll(resolvedStructures); - continue; - } - - String structureToken = normalizeLocateStructureToken(token); - if (!structureToken.isBlank()) { - matchedIds.add("structure:" + structureToken); - structures.add(structureToken); - } - } - - if (structures.isEmpty()) { - sender().sendMessage(C.RED + "No external datapack entry matched value(s): " + String.join(", ", requestedTokens)); - return; - } - - VolmitSender commandSender = sender(); - Runnable dispatchTask = () -> dispatchLocateExternalCommands(commandSender, structures, matchedIds, all); - if (commandSender.isPlayer()) { - Player player = commandSender.player(); - if (player == null) { - commandSender.sendMessage(C.RED + "No active player sender was available for locateexternal."); - return; - } - - if (!J.runEntity(player, dispatchTask)) { - commandSender.sendMessage(C.RED + "Failed to schedule locate command dispatch on the player's region thread."); - } - return; - } - - J.s(dispatchTask); - } - - private void dispatchLocateExternalCommands( - VolmitSender commandSender, - Set structures, - Set matchedIds, - boolean all - ) { - org.bukkit.command.CommandSender locateSender = commandSender.isPlayer() - ? commandSender.player() - : Bukkit.getConsoleSender(); - int dispatched = 0; - for (String structure : structures) { - String command = "locate structure " + structure; - boolean accepted = Bukkit.dispatchCommand(locateSender, command); - if (!accepted) { - commandSender.sendMessage(C.RED + "Failed to dispatch: /" + command); - } else { - commandSender.sendMessage(C.GREEN + "Dispatched: /" + command); - dispatched++; - } - - if (!all) { - break; - } - } - - commandSender.sendMessage(C.GREEN + "Matched ids=" + matchedIds.size() + ", locateTargets=" + structures.size() + ", dispatched=" + dispatched + "."); - } - - private static String normalizeLocateExternalToken(String token) { - if (token == null) { - return ""; - } - - String normalized = token.trim().toLowerCase(Locale.ROOT); - if (normalized.isBlank()) { - return ""; - } - - normalized = normalized.replace("minecraft:worldgen/structure/", ""); - normalized = normalized.replace("worldgen/structure/", ""); - if (!normalized.contains(":") && normalized.contains("/")) { - return normalized; - } - - if (!normalized.contains(":")) { - return normalized; - } - - return normalized; - } - - private static Map> buildExternalLocateFallbackById(KList externalDatapacks) { - return new ConcurrentHashMap<>(); - } - - private static String normalizeLocateStructureToken(String structure) { - if (structure == null) { - return ""; - } - - String normalized = structure.trim().toLowerCase(Locale.ROOT); - if (normalized.isBlank()) { - return ""; - } - - normalized = normalized.replace("minecraft:worldgen/structure/", ""); - normalized = normalized.replace("worldgen/structure/", ""); - if (!normalized.contains(":")) { - normalized = "minecraft:" + normalized; - } - - return normalized; - } - /* /todo @Director(description = "Benchmark a pack", origin = DirectorOrigin.CONSOLE) @@ -763,25 +601,21 @@ public class CommandIris implements DirectorExecutor { sender().sendMessage(C.GREEN + "Set debug to: " + to); } - //TODO fix pack trimming @Director(description = "Download a project.", aliases = "dl") public void download( @Param(name = "pack", description = "The pack to download", defaultValue = "overworld", aliases = "project") String pack, @Param(name = "branch", description = "The branch to download from", defaultValue = "stable") String branch, - //@Param(name = "trim", description = "Whether or not to download a trimmed version (do not enable when editing)", defaultValue = "false") - //boolean trim, @Param(name = "overwrite", description = "Whether or not to overwrite the pack with the downloaded one", aliases = "force", defaultValue = "false") boolean overwrite ) { - boolean trim = false; - sender().sendMessage(C.GREEN + "Downloading pack: " + pack + "/" + branch + (trim ? " trimmed" : "") + (overwrite ? " overwriting" : "")); + sender().sendMessage(C.GREEN + "Downloading pack: " + pack + "/" + branch + (overwrite ? " overwriting" : "")); if (pack.equals("overworld")) { String url = "https://github.com/IrisDimensions/overworld/releases/download/" + INMS.OVERWORLD_TAG + "/overworld.zip"; - Iris.service(StudioSVC.class).downloadRelease(sender(), url, trim, overwrite); + Iris.service(StudioSVC.class).downloadRelease(sender(), url, overwrite); } else { - Iris.service(StudioSVC.class).downloadSearch(sender(), "IrisDimensions/" + pack + "/" + branch, trim, overwrite); + Iris.service(StudioSVC.class).downloadSearch(sender(), "IrisDimensions/" + pack + "/" + branch, overwrite); } } diff --git a/core/src/main/java/art/arcane/iris/core/commands/CommandObject.java b/core/src/main/java/art/arcane/iris/core/commands/CommandObject.java index 5c67b1968..10d718b91 100644 --- a/core/src/main/java/art/arcane/iris/core/commands/CommandObject.java +++ b/core/src/main/java/art/arcane/iris/core/commands/CommandObject.java @@ -22,10 +22,13 @@ import art.arcane.iris.Iris; import art.arcane.iris.core.link.WorldEditLink; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.loader.ResourceLoader; +import art.arcane.iris.core.runtime.ObjectStudioActivation; +import art.arcane.iris.core.runtime.WorldRuntimeControlService; import art.arcane.iris.core.service.ObjectSVC; import art.arcane.iris.core.service.StudioSVC; import art.arcane.iris.core.service.WandSVC; import art.arcane.iris.core.tools.IrisConverter; +import art.arcane.iris.core.tools.PlausibilizeMode; import art.arcane.iris.core.tools.TreePlausibilizer; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.object.*; @@ -33,17 +36,23 @@ import art.arcane.volmlib.util.data.Cuboid; import art.arcane.iris.util.common.data.IrisCustomData; import art.arcane.iris.util.common.data.registry.Materials; import art.arcane.iris.util.common.director.DirectorExecutor; +import art.arcane.iris.util.common.director.DirectorHelp; +import art.arcane.iris.util.common.director.specialhandlers.NullableDimensionHandler; +import art.arcane.iris.util.common.plugin.VolmitSender; import art.arcane.iris.util.common.scheduling.J; import art.arcane.volmlib.util.director.DirectorOrigin; import art.arcane.volmlib.util.director.annotations.Director; import art.arcane.volmlib.util.director.annotations.Param; import art.arcane.iris.util.common.director.specialhandlers.ObjectHandler; +import art.arcane.iris.util.common.director.specialhandlers.ObjectTargetHandler; import art.arcane.iris.util.common.format.C; import art.arcane.iris.util.common.math.Direction; import art.arcane.volmlib.util.math.RNG; +import io.papermc.lib.PaperLib; import org.bukkit.*; import org.bukkit.block.Block; import org.bukkit.block.data.BlockData; +import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import org.bukkit.util.Vector; @@ -54,6 +63,105 @@ import java.util.*; @Director(name = "object", aliases = "o", origin = DirectorOrigin.PLAYER, studio = true, description = "Iris object manipulation") public class CommandObject implements DirectorExecutor { + @Director(description = "Show help tree for this command group", aliases = {"?"}) + public void help() { + DirectorHelp.print(sender(), getClass()); + } + + @Director(description = "Open an object studio world (grid of every object; dimension optional, defaults to all packs)", aliases = {"std", "s"}, sync = true) + public void studio( + @Param(defaultValue = "null", description = "Optional dimension whose object pack to lay out; omit to aggregate objects from every pack", aliases = "dim", customHandler = NullableDimensionHandler.class) + IrisDimension dimension, + @Param(defaultValue = "1337", description = "The seed to generate the studio with", aliases = "s") + long seed + ) { + VolmitSender commandSender = sender(); + Map sources = new LinkedHashMap<>(); + IrisDimension hostDimension = dimension; + + if (dimension != null) { + IrisData data = dimension.getLoader(); + if (data == null) { + data = IrisData.get(dimension.getLoadFile().getParentFile().getParentFile()); + } + sources.put(data.getDataFolder().getName(), data); + } else { + File workspace = Iris.service(StudioSVC.class).getWorkspaceFolder(); + File[] packs = workspace == null ? null : workspace.listFiles(); + if (packs != null) { + Arrays.sort(packs, Comparator.comparing(File::getName, String.CASE_INSENSITIVE_ORDER)); + for (File pack : packs) { + if (!pack.isDirectory()) continue; + File dimensionsDir = new File(pack, "dimensions"); + if (!dimensionsDir.isDirectory()) continue; + IrisData data = IrisData.get(pack); + String[] keys = data.getObjectLoader().getPossibleKeys(); + if (keys == null || keys.length == 0) continue; + sources.put(pack.getName(), data); + if (hostDimension == null) { + File[] dimFiles = dimensionsDir.listFiles((f) -> f.isFile() && f.getName().endsWith(".json")); + if (dimFiles != null && dimFiles.length > 0) { + String loadKey = dimFiles[0].getName().replaceFirst("\\.json$", ""); + IrisDimension loaded = data.getDimensionLoader().load(loadKey); + if (loaded != null) { + hostDimension = loaded; + } + } + } + } + } + } + + if (hostDimension == null || sources.isEmpty()) { + commandSender.sendMessage(C.RED + "No packs with objects were found on this server."); + return; + } + + int totalObjects = 0; + for (IrisData d : sources.values()) { + String[] k = d.getObjectLoader().getPossibleKeys(); + if (k != null) totalObjects += k.length; + } + if (totalObjects == 0) { + commandSender.sendMessage(C.RED + "No objects to place across the selected pack(s)."); + return; + } + + hostDimension.setStudioMode(StudioMode.OBJECT_BUFFET); + ObjectStudioActivation.activate(hostDimension.getLoadKey()); + ObjectStudioActivation.setSources(hostDimension.getLoadKey(), sources); + + String scope = dimension == null + ? ("all packs [" + sources.size() + "]") + : ("\"" + hostDimension.getName() + "\""); + commandSender.sendMessage(C.GREEN + "Opening Object Studio for " + scope + " (" + + totalObjects + " objects)"); + + IrisDimension finalHost = hostDimension; + try { + Iris.service(StudioSVC.class).open(commandSender, seed, hostDimension.getLoadKey(), world -> { + if (world == null) return; + try { + WorldRuntimeControlService.get().applyObjectStudioWorldRules(world); + } catch (Throwable e) { + Iris.reportError("Failed to apply object studio world rules for " + world.getName(), e); + } + + if (commandSender.isPlayer()) { + Player p = commandSender.player(); + if (p != null) { + Location target = new Location(world, 0.5D, 66D, 0.5D); + J.runEntity(p, () -> { + PaperLib.teleportAsync(p, target).thenRun(() -> p.setGameMode(GameMode.CREATIVE)); + }); + } + } + }); + } catch (Throwable e) { + Iris.reportError("Failed to open object studio world \"" + finalHost.getLoadKey() + "\".", e); + commandSender.sendMessage(C.RED + "Failed to open object studio: " + e.getMessage()); + } + } private static final Set skipBlocks = Set.of(Materials.GRASS, Material.SNOW, Material.VINE, Material.TORCH, Material.DEAD_BUSH, Material.POPPY, Material.DANDELION); @@ -225,21 +333,18 @@ public class CommandObject implements DirectorExecutor { } } - @Director(description = "Bridge unreachable leaves with hidden logs so trees are vanilla-decay-plausible", + @Director(description = "Make tree leaves vanilla-decay-plausible (every leaf within 6 blocks of a log)", origin = DirectorOrigin.BOTH, studio = false) public void plausibilize( - @Param(description = "Pack key (trees/bonsai/smbase1), pack prefix (trees/), or filesystem path to a .iob file or directory") + @Param(description = "Object key, prefix (trees/), or filesystem path", + customHandler = ObjectTargetHandler.class) String target, + @Param(description = "DEFAULT: tentacle logs, delete orphans. NORMALIZE: + flip persistent=false. FOLIAGE_OVERATURE: add leaves to bridge orphans, no deletions. SMOKE: wipe & repaint canopy shell.", + defaultValue = "DEFAULT") + PlausibilizeMode mode, @Param(description = "Analyze only, do not write", defaultValue = "false") boolean dryRun, - @Param(description = "Flip persistent=true leaves to false and bridge them as well", - defaultValue = "false") - boolean normalize, - @Param(description = "Wipe scattered leaves and repaint a canopy shell around every log, then bridge any gaps with interior log tendrils", - defaultValue = "false") - boolean smoke, - @Param(description = "Canopy shell radius (smoke mode only), clamped to [0,5]", - defaultValue = "2") + @Param(description = "Canopy shell radius (SMOKE only), clamped [0,5]", defaultValue = "2") int radius ) { List targets = resolveTargets(target); @@ -248,13 +353,13 @@ public class CommandObject implements DirectorExecutor { return; } - sender().sendMessage(C.IRIS + "Plausibilize: queued " + targets.size() + " object(s)" - + (dryRun ? " [DRY RUN]" : "") - + (normalize ? " [NORMALIZE]" : "") - + (smoke ? " [SMOKE r=" + radius + "]" : "")); + sender().sendMessage(C.IRIS + "Plausibilize [" + mode.name() + + (dryRun ? " DRY" : "") + + (mode == PlausibilizeMode.SMOKE ? " r=" + radius : "") + + "] queued " + targets.size() + " object(s)"); org.bukkit.command.CommandSender s = sender(); - J.a(() -> runPlausibilize(targets, dryRun, normalize, smoke, radius, s)); + J.a(() -> runPlausibilize(targets, dryRun, mode, radius, s)); } private List resolveTargets(String target) { @@ -330,8 +435,7 @@ public class CommandObject implements DirectorExecutor { private static void runPlausibilize( List targets, boolean dryRun, - boolean normalize, - boolean smoke, + PlausibilizeMode mode, int radius, org.bukkit.command.CommandSender s ) { @@ -340,9 +444,6 @@ public class CommandObject implements DirectorExecutor { int failed = 0; int changed = 0; long totalLogsAdded = 0L; - long totalReachableBefore = 0L; - long totalLeaves = 0L; - long totalPersistentInput = 0L; long totalLeavesAdded = 0L; long totalLeavesRemoved = 0L; long totalNormalized = 0L; @@ -362,8 +463,8 @@ public class CommandObject implements DirectorExecutor { } TreePlausibilizer.Result r = dryRun - ? TreePlausibilizer.analyze(o, normalize, smoke, radius) - : TreePlausibilizer.apply(o, normalize, smoke, radius); + ? TreePlausibilizer.analyze(o, mode, radius) + : TreePlausibilizer.apply(o, mode, radius); if (r.skipReason() != null) { s.sendMessage(C.YELLOW + " skip " + t.key() + ": " + r.skipReason()); @@ -383,9 +484,6 @@ public class CommandObject implements DirectorExecutor { processed++; totalLogsAdded += r.logsAdded(); - totalReachableBefore += r.reachableBefore(); - totalLeaves += r.totalLeaves(); - totalPersistentInput += r.persistentLeavesInput(); totalLeavesAdded += r.leavesAdded(); totalLeavesRemoved += r.leavesRemoved(); totalNormalized += r.leavesNormalized(); @@ -393,14 +491,11 @@ public class CommandObject implements DirectorExecutor { if (touched || targets.size() == 1) { s.sendMessage(C.GRAY + " " + t.key() - + " leaves=" + r.totalLeaves() - + " persistentIn=" + r.persistentLeavesInput() - + " reachable=" + r.reachableBefore() - + " logsAdded=" + r.logsAdded() - + " leavesAdded=" + r.leavesAdded() - + " leavesRemoved=" + r.leavesRemoved() - + " normalized=" + r.leavesNormalized() - + " remaining=" + r.unreachableAfter()); + + C.WHITE + " +" + r.logsAdded() + " logs" + + C.WHITE + " +" + r.leavesAdded() + " leaves" + + C.WHITE + " -" + r.leavesRemoved() + " removed" + + (r.leavesNormalized() > 0 ? C.WHITE + " ~" + r.leavesNormalized() + " normalized" : "") + + (r.unreachableAfter() > 0 ? C.YELLOW + " " + r.unreachableAfter() + " unreachable" : "")); } if (targets.size() > 1 && index % progressStep == 0) { @@ -414,19 +509,11 @@ public class CommandObject implements DirectorExecutor { } } - s.sendMessage(C.IRIS + "Done." - + " processed=" + processed - + " changed=" + changed - + " skipped=" + skipped - + " failed=" + failed - + " leaves=" + totalLeaves - + " persistentIn=" + totalPersistentInput - + " reachableBefore=" + totalReachableBefore - + " logsAdded=" + totalLogsAdded - + " leavesAdded=" + totalLeavesAdded - + " leavesRemoved=" + totalLeavesRemoved - + " normalized=" + totalNormalized - + " remaining=" + totalUnreachableAfter); + s.sendMessage(C.IRIS + "Done: " + processed + " processed, " + changed + " changed, " + + skipped + " skipped, " + failed + " failed"); + s.sendMessage(C.IRIS + "Totals: +" + totalLogsAdded + " logs, +" + totalLeavesAdded + " leaves, -" + + totalLeavesRemoved + " removed, ~" + totalNormalized + " normalized, " + + totalUnreachableAfter + " unreachable"); } private static IrisObject loadTarget(Target t) throws IOException { diff --git a/core/src/main/java/art/arcane/iris/core/commands/CommandPack.java b/core/src/main/java/art/arcane/iris/core/commands/CommandPack.java index 888098307..a8491d43d 100644 --- a/core/src/main/java/art/arcane/iris/core/commands/CommandPack.java +++ b/core/src/main/java/art/arcane/iris/core/commands/CommandPack.java @@ -23,6 +23,7 @@ import art.arcane.iris.core.pack.PackValidationRegistry; import art.arcane.iris.core.pack.PackValidationResult; import art.arcane.iris.core.pack.PackValidator; 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.volmlib.util.director.annotations.Director; @@ -32,6 +33,10 @@ import java.io.File; @Director(name = "pack", aliases = {"pk"}, description = "Pack validation and maintenance") public class CommandPack implements DirectorExecutor { + @Director(description = "Show help tree for this command group", aliases = {"?"}) + public void help() { + DirectorHelp.print(sender(), getClass()); + } @Director(description = "Validate a pack (or all packs) and re-publish results", aliases = {"v", "check"}) public void validate( diff --git a/core/src/main/java/art/arcane/iris/core/commands/CommandPregen.java b/core/src/main/java/art/arcane/iris/core/commands/CommandPregen.java index 1e1f17cb7..3c584422a 100644 --- a/core/src/main/java/art/arcane/iris/core/commands/CommandPregen.java +++ b/core/src/main/java/art/arcane/iris/core/commands/CommandPregen.java @@ -23,6 +23,7 @@ import art.arcane.iris.core.gui.PregeneratorJob; import art.arcane.iris.core.pregenerator.PregenTask; import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.util.common.director.DirectorExecutor; +import art.arcane.iris.util.common.director.DirectorHelp; import art.arcane.volmlib.util.director.annotations.Director; import art.arcane.volmlib.util.director.annotations.Param; import art.arcane.iris.util.common.format.C; @@ -32,6 +33,11 @@ import org.bukkit.util.Vector; @Director(name = "pregen", aliases = "pregenerate", description = "Pregenerate your Iris worlds!") public class CommandPregen implements DirectorExecutor { + @Director(description = "Show help tree for this command group", aliases = {"?"}) + public void help() { + DirectorHelp.print(sender(), getClass()); + } + @Director(description = "Pregenerate a world") public void start( @Param(description = "The radius of the pregen in blocks", aliases = "size") diff --git a/core/src/main/java/art/arcane/iris/core/commands/CommandStudio.java b/core/src/main/java/art/arcane/iris/core/commands/CommandStudio.java index 8d6639efa..41f284ba9 100644 --- a/core/src/main/java/art/arcane/iris/core/commands/CommandStudio.java +++ b/core/src/main/java/art/arcane/iris/core/commands/CommandStudio.java @@ -24,8 +24,6 @@ import art.arcane.iris.core.gui.NoiseExplorerGUI; import art.arcane.iris.core.gui.VisionGUI; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.project.IrisProject; -import art.arcane.iris.core.runtime.ObjectStudioActivation; -import art.arcane.iris.core.runtime.WorldRuntimeControlService; import art.arcane.iris.core.service.StudioSVC; import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.engine.framework.Engine; @@ -38,6 +36,7 @@ import art.arcane.volmlib.util.collection.KMap; import art.arcane.volmlib.util.collection.KSet; import art.arcane.iris.util.common.director.DirectorContext; import art.arcane.iris.util.common.director.DirectorExecutor; +import art.arcane.iris.util.common.director.DirectorHelp; import art.arcane.iris.util.common.director.handlers.DimensionHandler; import art.arcane.iris.util.common.director.specialhandlers.NullableDimensionHandler; import art.arcane.volmlib.util.director.DirectorOrigin; @@ -92,7 +91,11 @@ import java.util.function.Supplier; @Director(name = "studio", aliases = {"std", "s"}, description = "Studio Commands", studio = true) public class CommandStudio implements DirectorExecutor { - private CommandFind find; + @Director(description = "Show help tree for this command group", aliases = {"?"}) + public void help() { + DirectorHelp.print(sender(), getClass()); + } + private CommandEdit edit; //private CommandDeepSearch deepSearch; @@ -125,101 +128,6 @@ public class CommandStudio implements DirectorExecutor { Iris.service(StudioSVC.class).open(sender(), seed, dimension.getLoadKey()); } - @Director(description = "Open an object studio world (grid of every object; dimension optional, defaults to all packs)", aliases = {"obj", "objs"}, sync = true) - public void object( - @Param(defaultValue = "null", description = "Optional dimension whose object pack to lay out; omit to aggregate objects from every pack", aliases = "dim", customHandler = NullableDimensionHandler.class) - IrisDimension dimension, - @Param(defaultValue = "1337", description = "The seed to generate the studio with", aliases = "s") - long seed - ) { - VolmitSender commandSender = sender(); - java.util.Map sources = new java.util.LinkedHashMap<>(); - IrisDimension hostDimension = dimension; - - if (dimension != null) { - IrisData data = dimension.getLoader(); - if (data == null) { - data = IrisData.get(dimension.getLoadFile().getParentFile().getParentFile()); - } - sources.put(data.getDataFolder().getName(), data); - } else { - File workspace = Iris.service(StudioSVC.class).getWorkspaceFolder(); - File[] packs = workspace == null ? null : workspace.listFiles(); - if (packs != null) { - Arrays.sort(packs, java.util.Comparator.comparing(File::getName, String.CASE_INSENSITIVE_ORDER)); - for (File pack : packs) { - if (!pack.isDirectory()) continue; - File dimensionsDir = new File(pack, "dimensions"); - if (!dimensionsDir.isDirectory()) continue; - IrisData data = IrisData.get(pack); - String[] keys = data.getObjectLoader().getPossibleKeys(); - if (keys == null || keys.length == 0) continue; - sources.put(pack.getName(), data); - if (hostDimension == null) { - File[] dimFiles = dimensionsDir.listFiles((f) -> f.isFile() && f.getName().endsWith(".json")); - if (dimFiles != null && dimFiles.length > 0) { - String loadKey = dimFiles[0].getName().replaceFirst("\\.json$", ""); - IrisDimension loaded = data.getDimensionLoader().load(loadKey); - if (loaded != null) { - hostDimension = loaded; - } - } - } - } - } - } - - if (hostDimension == null || sources.isEmpty()) { - commandSender.sendMessage(C.RED + "No packs with objects were found on this server."); - return; - } - - int totalObjects = 0; - for (IrisData d : sources.values()) { - String[] k = d.getObjectLoader().getPossibleKeys(); - if (k != null) totalObjects += k.length; - } - if (totalObjects == 0) { - commandSender.sendMessage(C.RED + "No objects to place across the selected pack(s)."); - return; - } - - hostDimension.setStudioMode(StudioMode.OBJECT_BUFFET); - ObjectStudioActivation.activate(hostDimension.getLoadKey()); - ObjectStudioActivation.setSources(hostDimension.getLoadKey(), sources); - - String scope = dimension == null - ? ("all packs [" + sources.size() + "]") - : ("\"" + hostDimension.getName() + "\""); - commandSender.sendMessage(C.GREEN + "Opening Object Studio for " + scope + " (" - + totalObjects + " objects)"); - - IrisDimension finalHost = hostDimension; - try { - Iris.service(StudioSVC.class).open(commandSender, seed, hostDimension.getLoadKey(), world -> { - if (world == null) return; - try { - WorldRuntimeControlService.get().applyObjectStudioWorldRules(world); - } catch (Throwable e) { - Iris.reportError("Failed to apply object studio world rules for " + world.getName(), e); - } - - if (commandSender.isPlayer()) { - Player p = commandSender.player(); - if (p != null) { - Location target = new Location(world, 0.5D, 66D, 0.5D); - J.runEntity(p, () -> { - PaperLib.teleportAsync(p, target).thenRun(() -> p.setGameMode(GameMode.CREATIVE)); - }); - } - } - }); - } catch (Throwable e) { - Iris.reportError("Failed to open object studio world \"" + finalHost.getLoadKey() + "\".", e); - commandSender.sendMessage(C.RED + "Failed to open object studio: " + e.getMessage()); - } - } - @Director(description = "Open VSCode for a dimension", aliases = {"vsc", "edit"}) public void vscode( @Param(defaultValue = "default", description = "The dimension to open VSCode for", aliases = "dim", customHandler = DimensionHandler.class) @@ -283,42 +191,23 @@ public class CommandStudio implements DirectorExecutor { sender().sendMessage(C.GREEN + "The \"" + dimension.getName() + "\" pack has version: " + dimension.getVersion()); } - @Director(description = "Open the noise explorer (External GUI)", aliases = {"nmap", "n"}) - public void noise() { - if (noGUI()) return; - sender().sendMessage(C.GREEN + "Opening Noise Explorer!"); - NoiseExplorerGUI.launch(); - } - - @Director(description = "Charges all spawners in the area", aliases = "zzt", origin = DirectorOrigin.PLAYER) - public void charge() { - if (!IrisToolbelt.isIrisWorld(world())) { - sender().sendMessage(C.RED + "You must be in an Iris world to charge spawners!"); - return; - } - sender().sendMessage(C.GREEN + "Charging spawners!"); - engine().getWorldManager().chargeEnergy(); - } - - @Director(description = "Preview noise gens (External GUI)", aliases = {"generator", "gen"}) - public void explore( - @Param(description = "The generator to explore", contextual = true) + @Director(description = "Open the noise explorer (External GUI)", aliases = {"nmap", "n", "generator", "gen"}) + public void noise( + @Param(description = "Optional pack generator to preview", defaultValue = "null", contextual = true) IrisGenerator generator, - @Param(description = "The seed to generate with", defaultValue = "12345") + @Param(description = "The seed to preview the generator with", defaultValue = "12345") long seed ) { if (noGUI()) return; sender().sendMessage(C.GREEN + "Opening Noise Explorer!"); - Supplier> l = () -> { + if (generator == null) { + NoiseExplorerGUI.launch(); + return; + } - if (generator == null) { - return (x, z) -> 0D; - } - - return (x, z) -> generator.getHeight(x, z, new RNG(seed).nextParallelRNG(3245).lmax()); - }; - NoiseExplorerGUI.launch(l, "Custom Generator"); + Supplier> supplier = () -> (x, z) -> generator.getHeight(x, z, new RNG(seed).nextParallelRNG(3245).lmax()); + NoiseExplorerGUI.launch(supplier, "Custom Generator"); } @Director(description = "Show loot if a chest were right here", origin = DirectorOrigin.PLAYER, sync = true) @@ -636,50 +525,6 @@ public class CommandStudio implements DirectorExecutor { sender().sendMessage(C.GREEN + "Done! " + report.getPath()); } - @Director(description = "List pack noise generators as pack/generator", aliases = {"pack-noise", "packnoises"}) - public void packnoise() { - LinkedHashSet packFolders = new LinkedHashSet<>(); - File packsFolder = Iris.instance.getDataFolder("packs"); - File[] children = packsFolder.listFiles(); - if (children != null) { - for (File child : children) { - if (child != null && child.isDirectory()) { - packFolders.add(child); - } - } - } - - StudioSVC studioService = Iris.service(StudioSVC.class); - if (studioService != null && studioService.isProjectOpen()) { - IrisProject activeProject = studioService.getActiveProject(); - if (activeProject != null && activeProject.getPath() != null && activeProject.getPath().isDirectory()) { - packFolders.add(activeProject.getPath()); - } - } - - ArrayList entries = new ArrayList<>(); - - for (File packFolder : packFolders) { - IrisData packData = IrisData.get(packFolder); - String packName = packFolder.getName(); - String[] keys = packData.getGeneratorLoader().getPossibleKeys(); - for (String key : keys) { - entries.add(packName + "/" + key); - } - } - - if (entries.isEmpty()) { - sender().sendMessage(C.YELLOW + "No pack noise generators were found."); - return; - } - - Collections.sort(entries); - sender().sendMessage(C.GREEN + "Pack noise generators: " + C.GOLD + entries.size()); - for (String entry : entries) { - sender().sendMessage(C.GRAY + entry); - } - } - private PlatformChunkGenerator resolveProfileGenerator(IrisDimension dimension) { StudioSVC studioService = Iris.service(StudioSVC.class); if (studioService != null && studioService.isProjectOpen()) { diff --git a/core/src/main/java/art/arcane/iris/core/commands/CommandWhat.java b/core/src/main/java/art/arcane/iris/core/commands/CommandWhat.java index 4cc8dcce8..936a9be91 100644 --- a/core/src/main/java/art/arcane/iris/core/commands/CommandWhat.java +++ b/core/src/main/java/art/arcane/iris/core/commands/CommandWhat.java @@ -27,6 +27,7 @@ import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisRegion; import art.arcane.iris.util.common.data.B; import art.arcane.iris.util.common.director.DirectorExecutor; +import art.arcane.iris.util.common.director.DirectorHelp; import art.arcane.volmlib.util.director.DirectorOrigin; import art.arcane.volmlib.util.director.annotations.Director; import art.arcane.volmlib.util.director.annotations.Param; @@ -45,6 +46,11 @@ import java.util.concurrent.atomic.AtomicInteger; @Director(name = "what", origin = DirectorOrigin.PLAYER, studio = true, description = "Iris What?") public class CommandWhat implements DirectorExecutor { + @Director(description = "Show help tree for this command group", aliases = {"?"}) + public void help() { + DirectorHelp.print(sender(), getClass()); + } + @Director(description = "What is in my hand?", origin = DirectorOrigin.PLAYER) public void hand() { try { diff --git a/core/src/main/java/art/arcane/iris/core/edit/DustRevealer.java b/core/src/main/java/art/arcane/iris/core/edit/DustRevealer.java index 6ac5014cd..1f2e19c6a 100644 --- a/core/src/main/java/art/arcane/iris/core/edit/DustRevealer.java +++ b/core/src/main/java/art/arcane/iris/core/edit/DustRevealer.java @@ -124,6 +124,9 @@ public class DustRevealer { } private boolean is(BlockPosition a) { + if (a.getY() < world.getMinHeight() || a.getY() >= world.getMaxHeight()) { + return false; + } int betterY = a.getY() - world.getMinHeight(); if (isValidTry(a) && engine.getObjectPlacementKey(a.getX(), betterY, a.getZ()) != null && engine.getObjectPlacementKey(a.getX(), betterY, a.getZ()).equals(key)) { hits.add(a); diff --git a/core/src/main/java/art/arcane/iris/core/loader/ImageResourceLoader.java b/core/src/main/java/art/arcane/iris/core/loader/ImageResourceLoader.java index d032f1e07..e740bef47 100644 --- a/core/src/main/java/art/arcane/iris/core/loader/ImageResourceLoader.java +++ b/core/src/main/java/art/arcane/iris/core/loader/ImageResourceLoader.java @@ -144,6 +144,14 @@ public class ImageResourceLoader extends ResourceLoader { } public File findFile(String name) { + if (name == null || name.trim().isEmpty()) { + return null; + } + if (name.equals("null")) { + Iris.warn("Refusing " + resourceTypeName + " lookup for literal string \"null\" (called by " + callerHint() + ")"); + return null; + } + for (File i : getFolders(name)) { for (File j : i.listFiles()) { if (j.isFile() && j.getName().endsWith(".png") && j.getName().split("\\Q.\\E")[0].equals(name)) { @@ -158,7 +166,7 @@ public class ImageResourceLoader extends ResourceLoader { } } - Iris.warn("Couldn't find " + resourceTypeName + ": " + name); + Iris.warn("Couldn't find " + resourceTypeName + ": " + name + " (called by " + callerHint() + ")"); return null; } @@ -182,12 +190,19 @@ public class ImageResourceLoader extends ResourceLoader { } } - Iris.warn("Couldn't find " + resourceTypeName + ": " + name); + Iris.warn("Couldn't find " + resourceTypeName + ": " + name + " (called by " + callerHint() + ")"); return null; } public IrisImage load(String name, boolean warn) { + if (name == null || name.trim().isEmpty()) { + return null; + } + if (name.equals("null") && warn) { + Iris.warn("Refusing " + resourceTypeName + " load for literal string \"null\" (called by " + callerHint() + ")"); + return null; + } return loadCache.get(name); } } diff --git a/core/src/main/java/art/arcane/iris/core/loader/JsonSchemaValidator.java b/core/src/main/java/art/arcane/iris/core/loader/JsonSchemaValidator.java new file mode 100644 index 000000000..3b278eb02 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/loader/JsonSchemaValidator.java @@ -0,0 +1,203 @@ +package art.arcane.iris.core.loader; + +import art.arcane.iris.Iris; +import art.arcane.iris.util.common.format.C; +import art.arcane.volmlib.util.json.JSONObject; +import com.google.gson.annotations.SerializedName; + +import java.io.File; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +final class JsonSchemaValidator { + private static final ConcurrentHashMap, Set> FIELD_CACHE = new ConcurrentHashMap<>(); + private static final int SUGGESTION_MAX_DISTANCE = 4; + + private JsonSchemaValidator() { + } + + static void validateTopLevelKeys(JSONObject parsed, String rawText, File file, String resourceTypeName, Class objectClass) { + if (parsed == null || objectClass == null) { + return; + } + Set known = FIELD_CACHE.computeIfAbsent(objectClass, JsonSchemaValidator::collectFieldNames); + for (String key : parsed.keySet()) { + if (known.contains(key)) { + continue; + } + reportUnknownKey(key, rawText, file, resourceTypeName, known); + } + } + + static void reportLoadFailure(File file, String rawText, String resourceTypeName, Throwable error) { + String message = error.getMessage(); + if (message == null || message.isBlank()) { + message = error.getClass().getSimpleName(); + } + int line = extractLineFromMessage(message); + String location = file.getPath(); + if (line > 0) { + location = location + ":" + line; + } + StringBuilder out = new StringBuilder(); + out.append("Couldn't load ").append(resourceTypeName) + .append(C.RED).append(" in ").append(C.WHITE).append(location).append(C.RED) + .append(" -> ").append(message); + String snippet = buildSnippet(rawText, line); + if (snippet != null) { + out.append('\n').append(snippet); + } + Iris.warn(out.toString()); + } + + private static void reportUnknownKey(String key, String rawText, File file, String resourceTypeName, Set known) { + int line = findLineForKey(rawText, key); + String suggestion = closestMatch(key, known); + StringBuilder out = new StringBuilder(); + out.append("Unknown ").append(resourceTypeName).append(" field ") + .append(C.WHITE).append('"').append(key).append('"').append(C.YELLOW) + .append(" in ").append(C.WHITE).append(file.getPath()); + if (line > 0) { + out.append(":").append(line); + } + out.append(C.YELLOW).append(" (Gson will silently ignore this)"); + if (suggestion != null) { + out.append(". Did you mean ").append(C.WHITE).append('"').append(suggestion).append('"').append(C.YELLOW).append("?"); + } + String snippet = buildSnippet(rawText, line); + if (snippet != null) { + out.append('\n').append(snippet); + } + Iris.warn(out.toString()); + } + + private static Set collectFieldNames(Class cls) { + Set names = new LinkedHashSet<>(); + Class c = cls; + while (c != null && c != Object.class) { + for (Field field : c.getDeclaredFields()) { + int mods = field.getModifiers(); + if (Modifier.isStatic(mods) || Modifier.isTransient(mods)) { + continue; + } + if (field.isSynthetic()) { + continue; + } + SerializedName serialized = field.getAnnotation(SerializedName.class); + if (serialized != null) { + names.add(serialized.value()); + Collections.addAll(names, serialized.alternate()); + } else { + names.add(field.getName()); + } + } + c = c.getSuperclass(); + } + return Collections.unmodifiableSet(names); + } + + private static int findLineForKey(String rawText, String key) { + if (rawText == null || key == null) { + return -1; + } + Pattern pattern = Pattern.compile("\"" + Pattern.quote(key) + "\"\\s*:"); + Matcher matcher = pattern.matcher(rawText); + if (!matcher.find()) { + return -1; + } + int index = matcher.start(); + int line = 1; + for (int i = 0; i < index; i++) { + if (rawText.charAt(i) == '\n') { + line++; + } + } + return line; + } + + private static int extractLineFromMessage(String message) { + if (message == null) { + return -1; + } + Matcher m = Pattern.compile("line\\s+(\\d+)").matcher(message); + if (m.find()) { + try { + return Integer.parseInt(m.group(1)); + } catch (NumberFormatException ignored) { + } + } + return -1; + } + + private static String buildSnippet(String rawText, int line) { + if (rawText == null || line <= 0) { + return null; + } + String[] lines = rawText.split("\n", -1); + if (line > lines.length) { + return null; + } + int from = Math.max(0, line - 2); + int to = Math.min(lines.length, line + 1); + StringBuilder out = new StringBuilder(); + int width = String.valueOf(to).length(); + for (int i = from; i < to; i++) { + int n = i + 1; + boolean focus = n == line; + out.append(focus ? C.RED + "> " : C.GRAY + " "); + out.append(String.format("%" + width + "d", n)).append(" | "); + String content = lines[i]; + if (content.length() > 200) { + content = content.substring(0, 200) + "..."; + } + out.append(content); + if (i < to - 1) { + out.append('\n'); + } + } + return out.toString(); + } + + private static String closestMatch(String key, Set known) { + String lowerKey = key.toLowerCase(); + String best = null; + int bestDistance = Integer.MAX_VALUE; + for (String candidate : known) { + int d = levenshtein(lowerKey, candidate.toLowerCase()); + if (d < bestDistance) { + bestDistance = d; + best = candidate; + } + } + if (best == null) { + return null; + } + int threshold = Math.min(SUGGESTION_MAX_DISTANCE, Math.max(1, key.length() / 2)); + return bestDistance <= threshold ? best : null; + } + + private static int levenshtein(String a, String b) { + int[] prev = new int[b.length() + 1]; + int[] curr = new int[b.length() + 1]; + for (int j = 0; j <= b.length(); j++) { + prev[j] = j; + } + for (int i = 1; i <= a.length(); i++) { + curr[0] = i; + for (int j = 1; j <= b.length(); j++) { + int cost = a.charAt(i - 1) == b.charAt(j - 1) ? 0 : 1; + curr[j] = Math.min(Math.min(curr[j - 1] + 1, prev[j] + 1), prev[j - 1] + cost); + } + int[] tmp = prev; + prev = curr; + curr = tmp; + } + return prev[b.length()]; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/loader/MatterObjectResourceLoader.java b/core/src/main/java/art/arcane/iris/core/loader/MatterObjectResourceLoader.java index 57912c71b..deea512d4 100644 --- a/core/src/main/java/art/arcane/iris/core/loader/MatterObjectResourceLoader.java +++ b/core/src/main/java/art/arcane/iris/core/loader/MatterObjectResourceLoader.java @@ -154,6 +154,14 @@ public class MatterObjectResourceLoader extends ResourceLoader // } public File findFile(String name) { + if (name == null || name.trim().isEmpty()) { + return null; + } + if (name.equals("null")) { + Iris.warn("Refusing " + resourceTypeName + " lookup for literal string \"null\" (called by " + callerHint() + ")"); + return null; + } + for (File i : getFolders(name)) { for (File j : i.listFiles()) { if (j.isFile() && j.getName().endsWith(".mat") && j.getName().split("\\Q.\\E")[0].equals(name)) { @@ -168,7 +176,7 @@ public class MatterObjectResourceLoader extends ResourceLoader } } - Iris.warn("Couldn't find " + resourceTypeName + ": " + name); + Iris.warn("Couldn't find " + resourceTypeName + ": " + name + " (called by " + callerHint() + ")"); return null; } @@ -192,12 +200,19 @@ public class MatterObjectResourceLoader extends ResourceLoader } } - Iris.warn("Couldn't find " + resourceTypeName + ": " + name); + Iris.warn("Couldn't find " + resourceTypeName + ": " + name + " (called by " + callerHint() + ")"); return null; } public IrisMatterObject load(String name, boolean warn) { + if (name == null || name.trim().isEmpty()) { + return null; + } + if (name.equals("null") && warn) { + Iris.warn("Refusing " + resourceTypeName + " load for literal string \"null\" (called by " + callerHint() + ")"); + return null; + } return loadCache.get(name); } } diff --git a/core/src/main/java/art/arcane/iris/core/loader/ObjectResourceLoader.java b/core/src/main/java/art/arcane/iris/core/loader/ObjectResourceLoader.java index 65b6542e1..2e04dce65 100644 --- a/core/src/main/java/art/arcane/iris/core/loader/ObjectResourceLoader.java +++ b/core/src/main/java/art/arcane/iris/core/loader/ObjectResourceLoader.java @@ -123,6 +123,14 @@ public class ObjectResourceLoader extends ResourceLoader { } public File findFile(String name) { + if (name == null || name.trim().isEmpty()) { + return null; + } + if (name.equals("null")) { + Iris.warn("Refusing " + resourceTypeName + " lookup for literal string \"null\" (called by " + callerHint() + ")"); + return null; + } + for (File i : getFolders(name)) { for (File j : i.listFiles()) { if (j.isFile() && j.getName().endsWith(".iob") && j.getName().split("\\Q.\\E")[0].equals(name)) { @@ -137,7 +145,7 @@ public class ObjectResourceLoader extends ResourceLoader { } } - Iris.warn("Couldn't find " + resourceTypeName + ": " + name); + Iris.warn("Couldn't find " + resourceTypeName + ": " + name + " (called by " + callerHint() + ")"); return null; } @@ -161,12 +169,21 @@ public class ObjectResourceLoader extends ResourceLoader { } } - Iris.warn("Couldn't find " + resourceTypeName + ": " + name); - return null; } public IrisObject load(String name, boolean warn) { - return loadCache.get(name); + if (name == null || name.trim().isEmpty()) { + return null; + } + if (name.equals("null") && warn) { + Iris.warn("Refusing " + resourceTypeName + " load for literal string \"null\" (called by " + callerHint() + ")"); + return null; + } + IrisObject result = loadCache.get(name); + if (result == null && warn) { + Iris.warn("Couldn't find " + resourceTypeName + ": " + name + " (called by " + callerHint() + ")"); + } + return result; } } diff --git a/core/src/main/java/art/arcane/iris/core/loader/ResourceLoader.java b/core/src/main/java/art/arcane/iris/core/loader/ResourceLoader.java index 62a6cf407..a173c01e7 100644 --- a/core/src/main/java/art/arcane/iris/core/loader/ResourceLoader.java +++ b/core/src/main/java/art/arcane/iris/core/loader/ResourceLoader.java @@ -130,6 +130,14 @@ public class ResourceLoader implements MeteredCache { } public File findFile(String name) { + if (name == null || name.trim().isEmpty()) { + return null; + } + if (name.equals("null")) { + Iris.warn("Refusing " + resourceTypeName + " lookup for literal string \"null\" (called by " + callerHint() + ")"); + return null; + } + for (File i : getFolders(name)) { for (File j : i.listFiles()) { if (j.isFile() && j.getName().endsWith(".json") && j.getName().split("\\Q.\\E")[0].equals(name)) { @@ -144,11 +152,34 @@ public class ResourceLoader implements MeteredCache { } } - Iris.warn("Couldn't find " + resourceTypeName + ": " + name); + Iris.warn("Couldn't find " + resourceTypeName + ": " + name + " (called by " + callerHint() + ")"); return null; } + protected static String describeName(String name) { + if (name == null) return ""; + if (name.isEmpty()) return ""; + if (name.equals("null")) return "\"null\" (literal string)"; + return "\"" + name + "\""; + } + + protected static String callerHint() { + StackWalker walker = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE); + return walker.walk(frames -> frames + .filter(f -> { + String cn = f.getClassName(); + return !cn.startsWith("art.arcane.iris.core.loader.") + && !cn.startsWith("art.arcane.volmlib.util.data.") + && !cn.startsWith("com.github.benmanes.caffeine."); + }) + .limit(3) + .map(f -> f.getClassName().substring(f.getClassName().lastIndexOf('.') + 1) + + "." + f.getMethodName() + ":" + f.getLineNumber()) + .reduce((a, b) -> a + " <- " + b) + .orElse("")); + } + public void logLoad(File path, T t) { loads.getAndIncrement(); @@ -167,7 +198,11 @@ public class ResourceLoader implements MeteredCache { } public void failLoad(File path, Throwable e) { - J.a(() -> Iris.warn("Couldn't Load " + resourceTypeName + " file: " + path.getPath() + ": " + e.getMessage())); + failLoad(path, null, e); + } + + public void failLoad(File path, String rawText, Throwable e) { + J.a(() -> JsonSchemaValidator.reportLoadFailure(path, rawText, resourceTypeName, e)); } private KList matchAllFiles(File root, Predicate f) { @@ -241,10 +276,14 @@ public class ResourceLoader implements MeteredCache { } protected T loadFile(File j, String name) { + String rawText = null; try { PrecisionStopwatch p = PrecisionStopwatch.start(); + rawText = IO.readAll(j); + JSONObject parsed = new JSONObject(rawText); + JsonSchemaValidator.validateTopLevelKeys(parsed, rawText, j, resourceTypeName, objectClass); T t = getManager().getGson() - .fromJson(preprocess(new JSONObject(IO.readAll(j))).toString(0), objectClass); + .fromJson(preprocess(parsed).toString(0), objectClass); t.setLoadKey(name); t.setLoadFile(j); t.setLoader(manager); @@ -254,7 +293,7 @@ public class ResourceLoader implements MeteredCache { return t; } catch (Throwable e) { Iris.reportError(e); - failLoad(j, e); + failLoad(j, rawText, e); return null; } } @@ -358,11 +397,11 @@ public class ResourceLoader implements MeteredCache { } public T load(String name, boolean warn) { - if (name == null) { + if (name == null || name.trim().isEmpty()) { return null; } - - if (name.trim().isEmpty()) { + if (name.equals("null") && warn) { + Iris.warn("Refusing " + resourceTypeName + " load for literal string \"null\" (called by " + callerHint() + ")"); return null; } diff --git a/core/src/main/java/art/arcane/iris/core/nms/INMSBinding.java b/core/src/main/java/art/arcane/iris/core/nms/INMSBinding.java index 6e3200f75..e4618d704 100644 --- a/core/src/main/java/art/arcane/iris/core/nms/INMSBinding.java +++ b/core/src/main/java/art/arcane/iris/core/nms/INMSBinding.java @@ -24,7 +24,6 @@ import art.arcane.iris.core.lifecycle.WorldLifecycleRequest; import art.arcane.iris.core.lifecycle.WorldLifecycleService; import art.arcane.iris.core.nms.container.BiomeColor; import art.arcane.iris.core.nms.container.BlockProperty; -import art.arcane.iris.core.nms.container.StructurePlacement; import art.arcane.iris.core.nms.datapack.DataVersion; import art.arcane.iris.util.common.scheduling.J; import art.arcane.iris.engine.framework.Engine; @@ -155,12 +154,6 @@ public interface INMSBinding { return 441; } - KList getStructureKeys(); - - default KMap> getVanillaStructureBiomeTags() { - return new KMap<>(); - } - boolean missingDimensionTypes(String... keys); default boolean injectBukkit() { @@ -169,10 +162,6 @@ public interface INMSBinding { KMap> getBlockProperties(); - void placeStructures(Chunk chunk); - - KMap collectStructures(); - default Map extractVanillaDatapack() { return Map.of(); } diff --git a/core/src/main/java/art/arcane/iris/core/nms/container/StructurePlacement.java b/core/src/main/java/art/arcane/iris/core/nms/container/StructurePlacement.java deleted file mode 100644 index 5cf6c3bd7..000000000 --- a/core/src/main/java/art/arcane/iris/core/nms/container/StructurePlacement.java +++ /dev/null @@ -1,81 +0,0 @@ -package art.arcane.iris.core.nms.container; - -import com.google.gson.JsonObject; -import lombok.*; -import lombok.experimental.Accessors; -import lombok.experimental.SuperBuilder; -import org.apache.commons.math3.fraction.Fraction; - -import java.util.List; - -@Data -@SuperBuilder -@Accessors(fluent = true, chain = true) -public abstract class StructurePlacement { - private final int salt; - private final float frequency; - private final List structures; - - public abstract JsonObject toJson(String structure); - - protected JsonObject createBase(String structure) { - JsonObject object = new JsonObject(); - object.addProperty("structure", structure); - object.addProperty("salt", salt); - return object; - } - - public int frequencyToSpacing() { - var frac = new Fraction(Math.max(Math.min(frequency, 1), 0.000000001f)); - return (int) Math.round(Math.sqrt((double) frac.getDenominator() / frac.getNumerator())); - } - - public enum SpreadType { - LINEAR, - TRIANGULAR - } - - @Getter - @Accessors(chain = true, fluent = true) - @EqualsAndHashCode(callSuper = true) - @SuperBuilder - public static class RandomSpread extends StructurePlacement { - private final int spacing; - private final int separation; - private final SpreadType spreadType; - - @Override - public JsonObject toJson(String structure) { - JsonObject object = createBase(structure); - object.addProperty("spacing", Math.max(spacing, frequencyToSpacing())); - object.addProperty("separation", separation); - object.addProperty("spreadType", spreadType.name()); - return object; - } - } - - @Getter - @EqualsAndHashCode(callSuper = true) - @SuperBuilder - public static class ConcentricRings extends StructurePlacement { - private final int distance; - private final int spread; - private final int count; - - @Override - public JsonObject toJson(String structure) { - return null; - } - } - - public record Structure( - int weight, - String key, - List tags - ) { - - public boolean isValid() { - return weight > 0 && key != null; - } - } -} diff --git a/core/src/main/java/art/arcane/iris/core/nms/v1X/NMSBinding1X.java b/core/src/main/java/art/arcane/iris/core/nms/v1X/NMSBinding1X.java index 8224e6fe1..0af51949b 100644 --- a/core/src/main/java/art/arcane/iris/core/nms/v1X/NMSBinding1X.java +++ b/core/src/main/java/art/arcane/iris/core/nms/v1X/NMSBinding1X.java @@ -19,13 +19,11 @@ package art.arcane.iris.core.nms.v1X; import art.arcane.iris.Iris; -import art.arcane.iris.core.link.Identifier; import art.arcane.iris.core.nms.INMSBinding; import art.arcane.iris.core.nms.container.BiomeColor; import art.arcane.iris.core.nms.container.BlockProperty; import art.arcane.iris.core.nms.datapack.DataVersion; import art.arcane.iris.core.nms.container.Pair; -import art.arcane.iris.core.nms.container.StructurePlacement; import art.arcane.iris.engine.framework.Engine; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KMap; @@ -40,7 +38,6 @@ import org.bukkit.block.Biome; import org.bukkit.entity.Entity; import org.bukkit.entity.EntityType; import org.bukkit.event.entity.CreatureSpawnEvent; -import org.bukkit.generator.structure.Structure; import org.bukkit.inventory.ItemStack; import java.awt.Color; @@ -115,15 +112,6 @@ public class NMSBinding1X implements INMSBinding { return Color.GREEN; } - @Override - public KList getStructureKeys() { - List list = StreamSupport.stream(Registry.STRUCTURE.spliterator(), false) - .map(Structure::getKeyOrThrow) - .map(NamespacedKey::toString) - .toList(); - return new KList<>(list); - } - @Override public boolean missingDimensionTypes(String... keys) { return false; @@ -138,16 +126,6 @@ public class NMSBinding1X implements INMSBinding { return map; } - @Override - public void placeStructures(Chunk chunk) { - - } - - @Override - public KMap collectStructures() { - return new KMap<>(); - } - @Override public CompoundTag serializeEntity(Entity location) { return null; diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackValidator.java b/core/src/main/java/art/arcane/iris/core/pack/PackValidator.java index 83a8e7d66..ca651420b 100644 --- a/core/src/main/java/art/arcane/iris/core/pack/PackValidator.java +++ b/core/src/main/java/art/arcane/iris/core/pack/PackValidator.java @@ -42,6 +42,9 @@ public final class PackValidator { private static final String TRASH_ROOT = ".iris-trash"; private static final String DATAPACK_IMPORTS = "datapack-imports"; private static final String EXTERNAL_DATAPACKS = "externaldatapacks"; + private static final String INTERNAL_DATAPACKS = "internaldatapacks"; + private static final String DATAPACKS_FOLDER = "datapacks"; + private static final String CACHE_FOLDER = "cache"; private static final String OBJECTS_FOLDER = "objects"; private static final String DIMENSIONS_FOLDER = "dimensions"; private static final List MANAGED_RESOURCE_FOLDERS = List.of( @@ -209,6 +212,15 @@ public final class PackValidator { if (str.contains("/" + EXTERNAL_DATAPACKS + "/")) { return false; } + if (str.contains("/" + INTERNAL_DATAPACKS + "/")) { + return false; + } + if (str.contains("/" + DATAPACKS_FOLDER + "/")) { + return false; + } + if (str.contains("/" + CACHE_FOLDER + "/")) { + return false; + } if (str.contains("/" + OBJECTS_FOLDER + "/")) { return false; } diff --git a/core/src/main/java/art/arcane/iris/core/project/SchemaBuilder.java b/core/src/main/java/art/arcane/iris/core/project/SchemaBuilder.java index 875e984d1..61c1967da 100644 --- a/core/src/main/java/art/arcane/iris/core/project/SchemaBuilder.java +++ b/core/src/main/java/art/arcane/iris/core/project/SchemaBuilder.java @@ -322,6 +322,18 @@ public class SchemaBuilder { fancyType = "Enchantment Type"; prop.put("$ref", "#/definitions/" + key); description.add(SYMBOL_TYPE__N + " Must be a valid Enchantment Type (use ctrl+space for auto complete!)"); + } else if (k.isAnnotationPresent(RegistryListPotionEffect.class)) { + String key = "enum-potion-effect-type"; + + if (!definitions.containsKey(key)) { + JSONObject j = new JSONObject(); + j.put("enum", POTION_TYPES); + definitions.put(key, j); + } + + fancyType = "Potion Effect Type"; + prop.put("$ref", "#/definitions/" + key); + description.add(SYMBOL_TYPE__N + " Must be a valid Potion Effect Type (use ctrl+space for auto complete!)"); } else if (k.isAnnotationPresent(RegistryListFunction.class)) { var functionClass = k.getDeclaredAnnotation(RegistryListFunction.class).value(); try { @@ -537,6 +549,20 @@ public class SchemaBuilder { items.put("$ref", "#/definitions/" + key); prop.put("items", items); description.add(SYMBOL_TYPE__N + " Must be a valid Enchantment Type (use ctrl+space for auto complete!)"); + } else if (k.isAnnotationPresent(RegistryListPotionEffect.class)) { + fancyType = "List of Potion Effect Types"; + String key = "enum-potion-effect-type"; + + if (!definitions.containsKey(key)) { + JSONObject j = new JSONObject(); + j.put("enum", POTION_TYPES); + definitions.put(key, j); + } + + JSONObject items = new JSONObject(); + items.put("$ref", "#/definitions/" + key); + prop.put("items", items); + description.add(SYMBOL_TYPE__N + " Must be a valid Potion Effect Type (use ctrl+space for auto complete!)"); } else if (k.isAnnotationPresent(RegistryListFunction.class)) { var functionClass = k.getDeclaredAnnotation(RegistryListFunction.class).value(); try { diff --git a/core/src/main/java/art/arcane/iris/core/runtime/DatapackReadinessResult.java b/core/src/main/java/art/arcane/iris/core/runtime/DatapackReadinessResult.java deleted file mode 100644 index 27b2cba8e..000000000 --- a/core/src/main/java/art/arcane/iris/core/runtime/DatapackReadinessResult.java +++ /dev/null @@ -1,97 +0,0 @@ -package art.arcane.iris.core.runtime; - -import com.google.gson.GsonBuilder; -import art.arcane.iris.core.ServerConfigurator; -import art.arcane.volmlib.util.collection.KList; -import art.arcane.volmlib.util.collection.KMap; -import lombok.Data; - -import java.io.File; -import java.util.ArrayList; -import java.util.List; - -@Data -public final class DatapackReadinessResult { - private final String requestedPackKey; - private final List resolvedDatapackFolders; - private final String externalDatapackInstallResult; - private final boolean verificationPassed; - private final List verifiedPaths; - private final List missingPaths; - private final boolean restartRequired; - - public String toJson() { - return new GsonBuilder().setPrettyPrinting().create().toJson(this); - } - - public static DatapackReadinessResult installForStudioWorld( - String requestedPackKey, - String dimensionTypeKey, - File worldFolder, - boolean verifyDataPacks, - boolean includeExternalDataPacks, - KMap> extraWorldDatapackFoldersByPack - ) { - ArrayList resolvedFolders = new ArrayList<>(); - File datapacksFolder = ServerConfigurator.resolveDatapacksFolder(worldFolder); - resolvedFolders.add(datapacksFolder.getAbsolutePath()); - if (extraWorldDatapackFoldersByPack != null) { - KList extraFolders = extraWorldDatapackFoldersByPack.get(requestedPackKey); - if (extraFolders != null) { - for (File extraFolder : extraFolders) { - if (extraFolder == null) { - continue; - } - String path = extraFolder.getAbsolutePath(); - if (!resolvedFolders.contains(path)) { - resolvedFolders.add(path); - } - } - } - } - - String externalResult = "ok"; - boolean restartRequired = false; - try { - restartRequired = ServerConfigurator.installDataPacks(verifyDataPacks, includeExternalDataPacks, extraWorldDatapackFoldersByPack); - } catch (Throwable e) { - externalResult = e.getClass().getSimpleName() + ": " + String.valueOf(e.getMessage()); - } - - ArrayList verifiedPaths = new ArrayList<>(); - ArrayList missingPaths = new ArrayList<>(); - String verificationDimensionTypeKey = (dimensionTypeKey == null || dimensionTypeKey.isBlank()) - ? requestedPackKey - : dimensionTypeKey; - for (String folderPath : resolvedFolders) { - File folder = new File(folderPath); - collectVerificationPaths(folder, verificationDimensionTypeKey, verifiedPaths, missingPaths); - } - - boolean verificationPassed = missingPaths.isEmpty() && "ok".equals(externalResult); - return new DatapackReadinessResult( - requestedPackKey, - List.copyOf(resolvedFolders), - externalResult, - verificationPassed, - List.copyOf(verifiedPaths), - List.copyOf(missingPaths), - restartRequired - ); - } - - static void collectVerificationPaths(File folder, String dimensionTypeKey, List verifiedPaths, List missingPaths) { - File packMeta = new File(folder, "iris/pack.mcmeta"); - File dimensionType = new File(folder, "iris/data/iris/dimension_type/" + dimensionTypeKey + ".json"); - if (packMeta.exists()) { - verifiedPaths.add(packMeta.getAbsolutePath()); - } else { - missingPaths.add(packMeta.getAbsolutePath()); - } - if (dimensionType.exists()) { - verifiedPaths.add(dimensionType.getAbsolutePath()); - } else { - missingPaths.add(dimensionType.getAbsolutePath()); - } - } -} 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 6b9eed8fa..3eb09fa61 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 @@ -153,7 +153,7 @@ public final class StudioOpenCoordinator { request.onDone().accept(world); } - future.complete(new StudioOpenResult(world, safeEntry, creator.getLastDatapackReadinessResult())); + future.complete(new StudioOpenResult(world, safeEntry)); } catch (Throwable e) { Iris.reportError("Studio open failed for world \"" + request.worldName() + "\".", e); if (!request.retainOnFailure()) { @@ -544,7 +544,7 @@ public final class StudioOpenCoordinator { public record StudioOpenProgress(double progress, String stage) { } - public record StudioOpenResult(World world, Location entryLocation, DatapackReadinessResult datapackReadiness) { + public record StudioOpenResult(World world, Location entryLocation) { } public record StudioCloseResult( diff --git a/core/src/main/java/art/arcane/iris/core/service/IrisEngineSVC.java b/core/src/main/java/art/arcane/iris/core/service/IrisEngineSVC.java index 86e21bfb5..45a96e457 100644 --- a/core/src/main/java/art/arcane/iris/core/service/IrisEngineSVC.java +++ b/core/src/main/java/art/arcane/iris/core/service/IrisEngineSVC.java @@ -64,6 +64,15 @@ public class IrisEngineSVC implements IrisService { @Override public void onDisable() { + for (World world : worlds.keySet()) { + PlatformChunkGenerator gen = IrisToolbelt.access(world); + if (gen == null) continue; + try { + gen.close(); + } catch (Throwable t) { + Iris.reportError(t); + } + } if (service != null) { service.shutdown(); } diff --git a/core/src/main/java/art/arcane/iris/core/service/ObjectStudioSaveService.java b/core/src/main/java/art/arcane/iris/core/service/ObjectStudioSaveService.java index ce5419c5f..9e7c7f684 100644 --- a/core/src/main/java/art/arcane/iris/core/service/ObjectStudioSaveService.java +++ b/core/src/main/java/art/arcane/iris/core/service/ObjectStudioSaveService.java @@ -26,6 +26,7 @@ import art.arcane.iris.core.runtime.ObjectStudioLayout.GridCell; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.object.IrisObject; import art.arcane.iris.engine.platform.studio.generators.ObjectStudioGenerator; +import art.arcane.iris.util.common.format.C; import art.arcane.iris.util.common.plugin.IrisService; import art.arcane.iris.util.common.scheduling.J; import io.papermc.lib.PaperLib; @@ -41,22 +42,18 @@ import org.bukkit.event.entity.CreatureSpawnEvent; import org.bukkit.event.entity.EntitySpawnEvent; import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.event.world.WorldUnloadEvent; +import org.bukkit.inventory.EquipmentSlot; import java.io.ByteArrayOutputStream; import java.io.File; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicInteger; public class ObjectStudioSaveService implements IrisService { - public static final int INTERVAL_TICKS = 100; - private static final int CELLS_PER_PASS = 50; - private static ObjectStudioSaveService INSTANCE; private final Map studios = new ConcurrentHashMap<>(); - private int taskId = -1; public static ObjectStudioSaveService get() { ObjectStudioSaveService svc = INSTANCE; @@ -68,15 +65,10 @@ public class ObjectStudioSaveService implements IrisService { @Override public void onEnable() { INSTANCE = this; - taskId = J.ar(this::pass, INTERVAL_TICKS); } @Override public void onDisable() { - if (taskId != -1) { - J.car(taskId); - taskId = -1; - } studios.clear(); INSTANCE = null; } @@ -154,7 +146,9 @@ public class ObjectStudioSaveService implements IrisService { @EventHandler(ignoreCancelled = true) public void onPlayerInteract(PlayerInteractEvent event) { - if (event.getAction() != Action.RIGHT_CLICK_BLOCK && event.getAction() != Action.LEFT_CLICK_BLOCK) return; + if (event.getHand() != EquipmentSlot.HAND) return; + Action action = event.getAction(); + if (action != Action.RIGHT_CLICK_BLOCK && action != Action.LEFT_CLICK_BLOCK) return; Block clicked = event.getClickedBlock(); if (clicked == null) return; @@ -162,20 +156,46 @@ public class ObjectStudioSaveService implements IrisService { ActiveStudio studio = studios.get(world.getUID()); if (studio == null) return; - GridCell cell = studio.layout.findAt(clicked.getX(), clicked.getZ()); - if (cell == null) return; - Player player = event.getPlayer(); - Iris.info("Object Studio save triggered by %s for %s", player.getName(), cell.key()); + GridCell cell = findCellNear(studio, clicked.getX(), clicked.getZ()); + if (cell == null) { + player.sendMessage(C.GRAY + "Object Studio: no cell under click (x=" + clicked.getX() + " z=" + clicked.getZ() + ")."); + return; + } + + player.sendMessage(C.AQUA + "Object Studio: saving " + C.WHITE + cell.pack() + "/" + cell.key() + C.GRAY + " (" + cell.w() + "x" + cell.h() + "x" + cell.d() + ")"); + Iris.info("Object Studio save triggered by %s for %s/%s", player.getName(), cell.pack(), cell.key()); J.runRegion(world, cell.chunkMinX(), cell.chunkMinZ(), () -> { try { - captureAndSave(studio, world, cell); + captureAndSave(studio, world, cell, player); } catch (Throwable e) { Iris.reportError(e); } }); } + private static GridCell findCellNear(ActiveStudio studio, int x, int z) { + GridCell inside = studio.layout.findAt(x, z); + if (inside != null) return inside; + int reach = Math.max(1, studio.layout.padding() + 1); + GridCell best = null; + int bestDist = Integer.MAX_VALUE; + for (GridCell cell : studio.layout.cells()) { + int dx = 0; + if (x < cell.originX()) dx = cell.originX() - x; + else if (x >= cell.originX() + cell.w()) dx = x - (cell.originX() + cell.w() - 1); + int dz = 0; + if (z < cell.originZ()) dz = cell.originZ() - z; + else if (z >= cell.originZ() + cell.d()) dz = z - (cell.originZ() + cell.d() - 1); + int dist = Math.max(dx, dz); + if (dist <= reach && dist < bestDist) { + bestDist = dist; + best = cell; + } + } + return best; + } + public boolean teleportTo(Player player, String objectKey) { if (player == null || objectKey == null) return false; for (ActiveStudio studio : studios.values()) { @@ -206,43 +226,7 @@ public class ObjectStudioSaveService implements IrisService { return objects; } - private void pass() { - if (studios.isEmpty()) return; - - for (ActiveStudio studio : studios.values()) { - World world = Bukkit.getWorld(studio.worldId); - if (world == null) continue; - - int budget = CELLS_PER_PASS; - int size = studio.layout.cells().size(); - if (size == 0) continue; - - while (budget-- > 0) { - int idx = studio.cursor.getAndIncrement(); - if (idx >= size) { - studio.cursor.set(0); - idx = 0; - } - GridCell cell = studio.layout.cells().get(idx); - scheduleCapture(studio, world, cell); - if (size <= CELLS_PER_PASS && idx == size - 1) break; - } - } - } - - private void scheduleCapture(ActiveStudio studio, World world, GridCell cell) { - int chunkX = cell.chunkMinX(); - int chunkZ = cell.chunkMinZ(); - J.runRegion(world, chunkX, chunkZ, () -> { - try { - captureAndSave(studio, world, cell); - } catch (Throwable e) { - Iris.reportError(e); - } - }); - } - - private void captureAndSave(ActiveStudio studio, World world, GridCell cell) { + private void captureAndSave(ActiveStudio studio, World world, GridCell cell, Player notify) { if (!allChunksLoaded(world, cell)) { return; } @@ -268,18 +252,29 @@ public class ObjectStudioSaveService implements IrisService { long hash = hashOf(snapshot); Long prior = studio.hashes.get(hashKey); if (prior != null && prior == hash) { + if (notify != null) { + notify.sendMessage(C.GRAY + "Object Studio: no changes for " + cell.pack() + "/" + cell.key() + "."); + } return; } if (!anyBlock && prior == null) { studio.hashes.put(hashKey, hash); + if (notify != null) { + notify.sendMessage(C.GRAY + "Object Studio: empty cell " + cell.pack() + "/" + cell.key() + " (nothing to write)."); + } return; } studio.hashes.put(hashKey, hash); File targetFile = objectFileFor(studio, cell); - if (targetFile == null) return; + if (targetFile == null) { + if (notify != null) { + notify.sendMessage(C.RED + "Object Studio: no target file for " + cell.pack() + "/" + cell.key() + "."); + } + return; + } J.a(() -> { try { @@ -290,8 +285,14 @@ public class ObjectStudioSaveService implements IrisService { snapshot.write(targetFile); Iris.info("Object Studio saved: %s/%s (%dx%dx%d)", cell.pack(), cell.key(), cell.w(), cell.h(), cell.d()); + if (notify != null) { + J.runEntity(notify, () -> notify.sendMessage(C.GREEN + "Object Studio: saved " + C.WHITE + cell.pack() + "/" + cell.key())); + } } catch (Throwable e) { Iris.reportError(e); + if (notify != null) { + J.runEntity(notify, () -> notify.sendMessage(C.RED + "Object Studio: save failed for " + cell.pack() + "/" + cell.key() + " (" + e.getMessage() + ")")); + } } }); } @@ -335,7 +336,6 @@ public class ObjectStudioSaveService implements IrisService { final Map objectsDirs; final String packKey; final Map hashes = new ConcurrentHashMap<>(); - final AtomicInteger cursor = new AtomicInteger(); ActiveStudio(UUID worldId, ObjectStudioLayout layout, Map objectsDirs, String packKey) { this.worldId = worldId; diff --git a/core/src/main/java/art/arcane/iris/core/service/StudioSVC.java b/core/src/main/java/art/arcane/iris/core/service/StudioSVC.java index 0d9467074..45e22fc21 100644 --- a/core/src/main/java/art/arcane/iris/core/service/StudioSVC.java +++ b/core/src/main/java/art/arcane/iris/core/service/StudioSVC.java @@ -74,7 +74,7 @@ public class StudioSVC implements IrisService { if (pack.equals("overworld")) { Iris.info("Downloading Default Pack " + pack); String url = "https://github.com/IrisDimensions/overworld/releases/download/" + INMS.OVERWORLD_TAG + "/overworld.zip"; - Iris.service(StudioSVC.class).downloadRelease(Iris.getSender(), url, false, false); + Iris.service(StudioSVC.class).downloadRelease(Iris.getSender(), url, false); } else { Iris.warn("Default pack '" + pack + "' is not installed. Please download it manually with /iris download"); } @@ -201,11 +201,11 @@ public class StudioSVC implements IrisService { return dim; } - public void downloadSearch(VolmitSender sender, String key, boolean trim) { - downloadSearch(sender, key, trim, false); + public void downloadSearch(VolmitSender sender, String key) { + downloadSearch(sender, key, false); } - public void downloadSearch(VolmitSender sender, String key, boolean trim, boolean forceOverwrite) { + public void downloadSearch(VolmitSender sender, String key, boolean forceOverwrite) { try { String url = getListing(false).get(key); @@ -219,7 +219,7 @@ public class StudioSVC implements IrisService { String[] nodes = url.split("\\Q/\\E"); String repo = nodes.length == 1 ? "IrisDimensions/" + nodes[0] : nodes[0] + "/" + nodes[1]; String branch = nodes.length > 2 ? nodes[2] : "stable"; - download(sender, repo, branch, trim, forceOverwrite, false); + download(sender, repo, branch, forceOverwrite, false); } catch (Throwable e) { Iris.reportError(e); e.printStackTrace(); @@ -227,9 +227,9 @@ public class StudioSVC implements IrisService { } } - public void downloadRelease(VolmitSender sender, String url, boolean trim, boolean forceOverwrite) { + public void downloadRelease(VolmitSender sender, String url, boolean forceOverwrite) { try { - download(sender, "IrisDimensions", url, trim, forceOverwrite, true); + download(sender, "IrisDimensions", url, forceOverwrite, true); } catch (Throwable e) { Iris.reportError(e); e.printStackTrace(); @@ -237,14 +237,14 @@ public class StudioSVC implements IrisService { } } - public void download(VolmitSender sender, String repo, String branch, boolean trim) throws JsonSyntaxException, IOException { - download(sender, repo, branch, trim, false, false); + public void download(VolmitSender sender, String repo, String branch) throws JsonSyntaxException, IOException { + download(sender, repo, branch, false, false); } - public void download(VolmitSender sender, String repo, String branch, boolean trim, boolean forceOverwrite, boolean directUrl) throws JsonSyntaxException, IOException { + public void download(VolmitSender sender, String repo, String branch, boolean forceOverwrite, boolean directUrl) throws JsonSyntaxException, IOException { String url = directUrl ? branch : "https://codeload.github.com/" + repo + "/zip/refs/heads/" + branch; sender.sendMessage("Downloading " + url + " "); //The extra space stops a bug in adventure API from repeating the last letter of the URL - File zip = Iris.getNonCachedFile("pack-" + trim + "-" + repo, url); + File zip = Iris.getNonCachedFile("pack-" + repo, url); File temp = Iris.getTemp(); File work = new File(temp, "dl-" + UUID.randomUUID()); File packs = getWorkspaceFolder(); @@ -331,13 +331,6 @@ public class StudioSVC implements IrisService { FileUtils.copyDirectory(dir, packEntry); - if (trim) { - sender.sendMessage("Trimming " + key); - File cp = compilePackage(sender, key, false, false); - IO.delete(packEntry); - packEntry.mkdirs(); - ZipUtil.unpack(cp, packEntry); - } IrisData.getLoaded(packEntry) .ifPresent(IrisData::hotloaded); 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 4c1ae3beb..89d7c4899 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 @@ -32,7 +32,6 @@ import art.arcane.iris.core.nms.INMS; import art.arcane.iris.core.pregenerator.PregenTask; import art.arcane.iris.core.service.BoardSVC; import art.arcane.iris.core.service.StudioSVC; -import art.arcane.iris.core.runtime.DatapackReadinessResult; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.platform.PlatformChunkGenerator; @@ -113,11 +112,6 @@ public class IrisCreator { */ private boolean benchmark = false; private BiConsumer studioProgressConsumer; - private DatapackReadinessResult lastDatapackReadinessResult; - - public DatapackReadinessResult getLastDatapackReadinessResult() { - return lastDatapackReadinessResult; - } public static boolean removeFromBukkitYml(String name) throws IOException { YamlConfiguration yml = YamlConfiguration.loadConfiguration(BUKKIT_YML); @@ -191,41 +185,7 @@ public class IrisCreator { if (!studio()) { IrisWorlds.get().put(name(), dimension()); } - boolean verifyDataPacks = !studio(); - boolean includeExternalDataPacks = true; - KMap> extraWorldDatapackFoldersByPack = null; - if (studio()) { - File studioDatapackFolder = new File(new File(Bukkit.getWorldContainer(), name()), "datapacks"); - KList studioDatapackFolders = new KList<>(); - studioDatapackFolders.add(studioDatapackFolder); - extraWorldDatapackFoldersByPack = new KMap<>(); - extraWorldDatapackFoldersByPack.put(d.getLoadKey(), studioDatapackFolders); - } - lastDatapackReadinessResult = DatapackReadinessResult.installForStudioWorld( - d.getLoadKey(), - d.getDimensionTypeKey(), - new File(Bukkit.getWorldContainer(), name()), - verifyDataPacks, - includeExternalDataPacks, - extraWorldDatapackFoldersByPack - ); - if (!"ok".equals(lastDatapackReadinessResult.getExternalDatapackInstallResult())) { - throw new IrisException("Datapack external install failed: " + lastDatapackReadinessResult.getExternalDatapackInstallResult()); - } - if (lastDatapackReadinessResult.isRestartRequired()) { - throw new IrisException("Datapack install requested a server restart for " - + d.getLoadKey() - + ". folders=" - + lastDatapackReadinessResult.getResolvedDatapackFolders()); - } - if (!lastDatapackReadinessResult.isVerificationPassed()) { - throw new IrisException("Datapack readiness verification failed for " - + d.getLoadKey() - + ". missingPaths=" - + lastDatapackReadinessResult.getMissingPaths() - + ", folders=" - + lastDatapackReadinessResult.getResolvedDatapackFolders()); - } + ServerConfigurator.installDataPacks(!studio()); reportStudioProgress(0.40D, "install_datapacks"); PlatformChunkGenerator access = (PlatformChunkGenerator) wc.generator(); diff --git a/core/src/main/java/art/arcane/iris/core/tools/IrisToolbelt.java b/core/src/main/java/art/arcane/iris/core/tools/IrisToolbelt.java index 3f36c734d..1bec3c2e6 100644 --- a/core/src/main/java/art/arcane/iris/core/tools/IrisToolbelt.java +++ b/core/src/main/java/art/arcane/iris/core/tools/IrisToolbelt.java @@ -95,7 +95,7 @@ public class IrisToolbelt { } if (!pack.exists()) { - Iris.service(StudioSVC.class).downloadSearch(new VolmitSender(Bukkit.getConsoleSender(), Iris.instance.getTag()), requested, false, false); + Iris.service(StudioSVC.class).downloadSearch(new VolmitSender(Bukkit.getConsoleSender(), Iris.instance.getTag()), requested, false); File found = findCaseInsensitivePack(packsFolder, requested); if (found != null) { pack = found; diff --git a/core/src/main/java/art/arcane/iris/core/tools/PlausibilizeMode.java b/core/src/main/java/art/arcane/iris/core/tools/PlausibilizeMode.java new file mode 100644 index 000000000..939d147eb --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/tools/PlausibilizeMode.java @@ -0,0 +1,26 @@ +/* + * 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.tools; + +public enum PlausibilizeMode { + DEFAULT, + NORMALIZE, + FOLIAGE_OVERATURE, + SMOKE +} diff --git a/core/src/main/java/art/arcane/iris/core/tools/TreePlausibilizer.java b/core/src/main/java/art/arcane/iris/core/tools/TreePlausibilizer.java index 8988f2912..f4091bd2f 100644 --- a/core/src/main/java/art/arcane/iris/core/tools/TreePlausibilizer.java +++ b/core/src/main/java/art/arcane/iris/core/tools/TreePlausibilizer.java @@ -67,15 +67,18 @@ public final class TreePlausibilizer { } } - public static Result analyze(IrisObject obj, boolean normalize, boolean smoke, int shellRadius) { - return run(obj, false, normalize, smoke, shellRadius); + public static Result analyze(IrisObject obj, PlausibilizeMode mode, int shellRadius) { + return run(obj, false, mode, shellRadius); } - public static Result apply(IrisObject obj, boolean normalize, boolean smoke, int shellRadius) { - return run(obj, true, normalize, smoke, shellRadius); + public static Result apply(IrisObject obj, PlausibilizeMode mode, int shellRadius) { + return run(obj, true, mode, shellRadius); } - private static Result run(IrisObject obj, boolean mutate, boolean normalize, boolean smoke, int shellRadius) { + private static Result run(IrisObject obj, boolean mutate, PlausibilizeMode mode, int shellRadius) { + boolean normalize = mode == PlausibilizeMode.NORMALIZE; + boolean smoke = mode == PlausibilizeMode.SMOKE; + boolean foliageOverature = mode == PlausibilizeMode.FOLIAGE_OVERATURE; VectorMap blocks = obj.getBlocks(); Map positions = new HashMap<>(blocks.size() * 2); Set logPositions = new HashSet<>(); @@ -154,6 +157,8 @@ public final class TreePlausibilizer { Set unreached; Map distances; List inserts = new ArrayList<>(); + Set orphanRemovals = new HashSet<>(); + List leafAdds = new ArrayList<>(); if (!leafPositions.isEmpty() && !logPositions.isEmpty()) { Set connectivityLeaves; @@ -168,12 +173,24 @@ public final class TreePlausibilizer { reachableBefore = countReachable(leafPositions, distances); unreached = new HashSet<>(leafPositions); unreached.removeAll(distances.keySet()); - Set frontier = computeInitialFrontier(unreached, logPositions, distances); - logsAdded = bridgeLoop( - unreached, frontier, distances, + if (foliageOverature && !smoke && !unreached.isEmpty()) { + BlockData bridgeLeaf = pickDominantLeaf(leafTypeCounts); + foliageBridge( + unreached, logPositions, distances, + leafPositions, connectivityLeaves, positions, + bridgeLeaf, leafAdds, + minX, minY, minZ, maxX, maxY, maxZ + ); + distances = seedDistances(logPositions, connectivityLeaves); + unreached = new HashSet<>(leafPositions); + unreached.removeAll(distances.keySet()); + } + + logsAdded = tentacleGrow( + unreached, distances, logPositions, leafPositions, connectivityLeaves, positions, - inserts + inserts, orphanRemovals, !foliageOverature ); } else { distances = new HashMap<>(); @@ -181,8 +198,9 @@ public final class TreePlausibilizer { reachableBefore = 0; } - int leavesAdded = 0; - List leafAdds = new ArrayList<>(); + leavesRemoved += orphanRemovals.size(); + + int leavesAdded = leafAdds.size(); if (smoke) { BlockData leafTemplate = pickDominantLeaf(leafTypeCounts); for (long key : leafPositions) { @@ -213,6 +231,9 @@ public final class TreePlausibilizer { } } } + for (long key : orphanRemovals) { + blocks.remove(unpackKey(key)); + } for (LeafAddition addition : leafAdds) { blocks.put(unpackKey(addition.key()), addition.data()); } @@ -288,127 +309,246 @@ public final class TreePlausibilizer { } } - private static int bridgeLoop( + private static int tentacleGrow( Set unreached, - Set frontier, Map distances, Set logPositions, Set leafPositions, Set connectivityLeaves, Map positions, - List inserts + List inserts, + Set orphanRemovals, + boolean deleteOrphans ) { int logsAdded = 0; - int safetyLimit = unreached.size() + 32; - while (!unreached.isEmpty() && logsAdded < safetyLimit) { - long candidateKey = pickInteriorCandidate(frontier, unreached, connectivityLeaves); - BlockData logData = pickLogVariant(candidateKey, positions, logPositions); - inserts.add(new LogInsertion(candidateKey, logData)); + int safetyLimit = unreached.size() * 2 + 32; + long currentTarget = -1L; - logPositions.add(candidateKey); - leafPositions.remove(candidateKey); - connectivityLeaves.remove(candidateKey); - distances.remove(candidateKey); - unreached.remove(candidateKey); - frontier.remove(candidateKey); - positions.put(candidateKey, logData); + while (!unreached.isEmpty() && logsAdded < safetyLimit) { + if (currentTarget == -1L || !unreached.contains(currentTarget)) { + currentTarget = unreached.iterator().next(); + } + + long extensionLeaf = findWoodAdjacentLeafFrom(currentTarget, connectivityLeaves, logPositions); + + if (extensionLeaf == -1L) { + if (deleteOrphans) { + removeOrphanCluster(currentTarget, connectivityLeaves, leafPositions, unreached, distances, positions, orphanRemovals); + } else { + skipOrphanCluster(currentTarget, unreached, connectivityLeaves); + } + currentTarget = -1L; + continue; + } + + BlockData logData = pickLogVariant(extensionLeaf, positions, logPositions); + inserts.add(new LogInsertion(extensionLeaf, logData)); + + logPositions.add(extensionLeaf); + leafPositions.remove(extensionLeaf); + connectivityLeaves.remove(extensionLeaf); + distances.remove(extensionLeaf); + unreached.remove(extensionLeaf); + positions.put(extensionLeaf, logData); logsAdded++; - int[] cx = unpack(candidateKey); - Deque q = new ArrayDeque<>(); - for (int[] n : NEIGHBORS) { - long nk = packXYZ(cx[0] + n[0], cx[1] + n[1], cx[2] + n[2]); - if (connectivityLeaves.contains(nk)) { - Integer cur = distances.get(nk); - if (cur == null || cur > 1) { - if (cur == null) { - unreached.remove(nk); - frontier.remove(nk); - } - distances.put(nk, 1); - q.add(nk); - } - } - if (unreached.contains(nk)) { - frontier.add(nk); - } - } - while (!q.isEmpty()) { - long pos = q.poll(); - int d = distances.get(pos); - if (d >= MAX_DISTANCE) { - continue; - } - int[] px = unpack(pos); - for (int[] n : NEIGHBORS) { - long nk = packXYZ(px[0] + n[0], px[1] + n[1], px[2] + n[2]); - if (connectivityLeaves.contains(nk)) { - Integer cur = distances.get(nk); - if (cur == null || cur > d + 1) { - if (cur == null) { - unreached.remove(nk); - frontier.remove(nk); - } - distances.put(nk, d + 1); - q.add(nk); - } - } - if (unreached.contains(nk)) { - frontier.add(nk); - } - } - } + propagateFromLog(extensionLeaf, distances, connectivityLeaves, unreached); } return logsAdded; } - private static long pickInteriorCandidate( - Set frontier, Set unreached, Set connectivityLeaves - ) { - Set pool = !frontier.isEmpty() ? frontier : unreached; - long best = -1L; - int bestScore = -1; - for (long pos : pool) { + private static long findWoodAdjacentLeafFrom(long start, Set leafPositions, Set logPositions) { + if (!leafPositions.contains(start)) return -1L; + if (hasLogNeighbor(start, logPositions)) return start; + + Set visited = new HashSet<>(); + Deque queue = new ArrayDeque<>(); + queue.add(start); + visited.add(start); + + while (!queue.isEmpty()) { + long pos = queue.poll(); int[] xyz = unpack(pos); - int score = 0; for (int[] n : NEIGHBORS) { long nk = packXYZ(xyz[0] + n[0], xyz[1] + n[1], xyz[2] + n[2]); - if (connectivityLeaves.contains(nk)) { - score++; - } - } - if (score > bestScore) { - bestScore = score; - best = pos; - if (score == 6) break; + if (!leafPositions.contains(nk) || !visited.add(nk)) continue; + if (hasLogNeighbor(nk, logPositions)) return nk; + queue.add(nk); } } - return best; + return -1L; } - private static Set computeInitialFrontier( - Set unreached, Set logPositions, Map distances - ) { - Set frontier = new HashSet<>(); - for (long u : unreached) { - if (hasReachedNeighbor(u, distances, logPositions)) { - frontier.add(u); - } - } - return frontier; - } - - private static boolean hasReachedNeighbor(long key, Map distances, Set logPositions) { + private static boolean hasLogNeighbor(long key, Set logPositions) { int[] xyz = unpack(key); for (int[] n : NEIGHBORS) { long nk = packXYZ(xyz[0] + n[0], xyz[1] + n[1], xyz[2] + n[2]); - if (logPositions.contains(nk) || distances.containsKey(nk)) { - return true; - } + if (logPositions.contains(nk)) return true; } return false; } + private static void propagateFromLog( + long logKey, Map distances, + Set connectivityLeaves, Set unreached + ) { + int[] cx = unpack(logKey); + Deque q = new ArrayDeque<>(); + for (int[] n : NEIGHBORS) { + long nk = packXYZ(cx[0] + n[0], cx[1] + n[1], cx[2] + n[2]); + if (connectivityLeaves.contains(nk)) { + Integer cur = distances.get(nk); + if (cur == null || cur > 1) { + if (cur == null) unreached.remove(nk); + distances.put(nk, 1); + q.add(nk); + } + } + } + while (!q.isEmpty()) { + long pos = q.poll(); + int d = distances.get(pos); + if (d >= MAX_DISTANCE) continue; + int[] px = unpack(pos); + for (int[] n : NEIGHBORS) { + long nk = packXYZ(px[0] + n[0], px[1] + n[1], px[2] + n[2]); + if (connectivityLeaves.contains(nk)) { + Integer cur = distances.get(nk); + if (cur == null || cur > d + 1) { + if (cur == null) unreached.remove(nk); + distances.put(nk, d + 1); + q.add(nk); + } + } + } + } + } + + private static void foliageBridge( + Set unreached, + Set logPositions, + Map distances, + Set leafPositions, + Set connectivityLeaves, + Map positions, + BlockData leafTemplate, + List leafAdds, + int minX, int minY, int minZ, int maxX, int maxY, int maxZ + ) { + Set pending = new HashSet<>(unreached); + while (!pending.isEmpty()) { + long seed = pending.iterator().next(); + Set cluster = new HashSet<>(); + Deque cq = new ArrayDeque<>(); + cq.add(seed); + cluster.add(seed); + while (!cq.isEmpty()) { + long p = cq.poll(); + int[] xyz = unpack(p); + for (int[] n : NEIGHBORS) { + long nk = packXYZ(xyz[0] + n[0], xyz[1] + n[1], xyz[2] + n[2]); + if (pending.contains(nk) && cluster.add(nk)) { + cq.add(nk); + } + } + } + + Map parent = new HashMap<>(); + Deque q = new ArrayDeque<>(); + for (long c : cluster) { + parent.put(c, -1L); + q.add(c); + } + + long pathEnd = -1L; + while (!q.isEmpty() && pathEnd == -1L) { + long p = q.poll(); + int[] xyz = unpack(p); + for (int[] n : NEIGHBORS) { + int nx = xyz[0] + n[0]; + int ny = xyz[1] + n[1]; + int nz = xyz[2] + n[2]; + if (nx < minX || nx > maxX) continue; + if (ny < minY || ny > maxY) continue; + if (nz < minZ || nz > maxZ) continue; + long nk = packXYZ(nx, ny, nz); + if (parent.containsKey(nk)) continue; + if (logPositions.contains(nk) || distances.containsKey(nk)) { + pathEnd = p; + break; + } + if (positions.containsKey(nk)) continue; + parent.put(nk, p); + q.add(nk); + } + } + + pending.removeAll(cluster); + + if (pathEnd == -1L) { + continue; + } + + long cur = pathEnd; + while (cur != -1L && !cluster.contains(cur)) { + if (!positions.containsKey(cur)) { + BlockData clone = leafTemplate.clone(); + positions.put(cur, clone); + leafPositions.add(cur); + connectivityLeaves.add(cur); + leafAdds.add(new LeafAddition(cur, clone)); + } + cur = parent.get(cur); + } + } + } + + private static void skipOrphanCluster(long seed, Set unreached, Set connectivityLeaves) { + Deque queue = new ArrayDeque<>(); + Set visited = new HashSet<>(); + queue.add(seed); + visited.add(seed); + while (!queue.isEmpty()) { + long pos = queue.poll(); + unreached.remove(pos); + int[] xyz = unpack(pos); + for (int[] n : NEIGHBORS) { + long nk = packXYZ(xyz[0] + n[0], xyz[1] + n[1], xyz[2] + n[2]); + if (visited.add(nk) && unreached.contains(nk) && connectivityLeaves.contains(nk)) { + queue.add(nk); + } + } + } + } + + private static void removeOrphanCluster( + long seed, + Set connectivityLeaves, Set leafPositions, Set unreached, + Map distances, Map positions, Set orphanRemovals + ) { + Deque queue = new ArrayDeque<>(); + Set visited = new HashSet<>(); + queue.add(seed); + visited.add(seed); + + while (!queue.isEmpty()) { + long pos = queue.poll(); + int[] xyz = unpack(pos); + for (int[] n : NEIGHBORS) { + long nk = packXYZ(xyz[0] + n[0], xyz[1] + n[1], xyz[2] + n[2]); + if (visited.add(nk) && connectivityLeaves.contains(nk)) { + queue.add(nk); + } + } + orphanRemovals.add(pos); + connectivityLeaves.remove(pos); + leafPositions.remove(pos); + unreached.remove(pos); + distances.remove(pos); + positions.remove(pos); + } + } + private static Map seedDistances(Set logPositions, Set leafPositions) { Map dist = new HashMap<>(); Deque queue = new ArrayDeque<>(); 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 435f25e49..9b22b6337 100644 --- a/core/src/main/java/art/arcane/iris/engine/IrisEngine.java +++ b/core/src/main/java/art/arcane/iris/engine/IrisEngine.java @@ -377,7 +377,7 @@ public class IrisEngine implements Engine { setupEngine(); J.a(() -> { synchronized (ServerConfigurator.class) { - ServerConfigurator.installDataPacks(false, false); + ServerConfigurator.installDataPacks(false); } }); } diff --git a/core/src/main/java/art/arcane/iris/engine/IrisEngineMantle.java b/core/src/main/java/art/arcane/iris/engine/IrisEngineMantle.java index 27addb091..03d66d994 100644 --- a/core/src/main/java/art/arcane/iris/engine/IrisEngineMantle.java +++ b/core/src/main/java/art/arcane/iris/engine/IrisEngineMantle.java @@ -28,6 +28,7 @@ import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.mantle.EngineMantle; import art.arcane.iris.engine.mantle.MantleComponent; import art.arcane.iris.engine.mantle.components.MantleCarvingComponent; +import art.arcane.iris.engine.mantle.components.MantleFloatingObjectComponent; import art.arcane.iris.engine.mantle.components.MantleFluidBodyComponent; import art.arcane.iris.engine.mantle.components.MantleObjectComponent; import art.arcane.iris.util.project.matter.IrisMatterSupport; @@ -82,6 +83,7 @@ public class IrisEngineMantle implements EngineMantle { registerComponent(new MantleFluidBodyComponent(this)); object = new MantleObjectComponent(this); registerComponent(object); + registerComponent(new MantleFloatingObjectComponent(this)); } @Override diff --git a/core/src/main/java/art/arcane/iris/engine/IrisWorldManager.java b/core/src/main/java/art/arcane/iris/engine/IrisWorldManager.java index bdf043636..8442895e0 100644 --- a/core/src/main/java/art/arcane/iris/engine/IrisWorldManager.java +++ b/core/src/main/java/art/arcane/iris/engine/IrisWorldManager.java @@ -80,7 +80,6 @@ public class IrisWorldManager extends EngineAssignedWorldManager { private final KList updateQueue = new KList<>(); private final ChronoLatch cl; private final ChronoLatch clw; - private final ChronoLatch ecl; private final ChronoLatch cln; private final ChronoLatch chunkUpdater; private final ChronoLatch chunkDiscovery; @@ -90,9 +89,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager { private final Set markerFlagQueue = ConcurrentHashMap.newKeySet(); private final Set discoveredFlagQueue = ConcurrentHashMap.newKeySet(); private final Set markerScanQueue = ConcurrentHashMap.newKeySet(); - private double energy = 25; private int entityCount = 0; - private long charge = 0; private int actuallySpawned = 0; private int cooldown = 0; private List precount = new KList<>(); @@ -101,7 +98,6 @@ public class IrisWorldManager extends EngineAssignedWorldManager { public IrisWorldManager() { super(null); cl = null; - ecl = null; cln = null; clw = null; looper = null; @@ -117,7 +113,6 @@ public class IrisWorldManager extends EngineAssignedWorldManager { chunkDiscovery = new ChronoLatch(5000); cln = new ChronoLatch(60000); cl = new ChronoLatch(3000); - ecl = new ChronoLatch(250); clw = new ChronoLatch(1000, true); cleanupService = Executors.newSingleThreadScheduledExecutor(runnable -> { var thread = new Thread(runnable, "Iris Mantle Cleanup " + getTarget().getWorld().name()); @@ -125,7 +120,6 @@ public class IrisWorldManager extends EngineAssignedWorldManager { return thread; }); id = engine.getCacheID(); - energy = 25; looper = new Looper() { @Override protected long loop() { @@ -158,16 +152,6 @@ public class IrisWorldManager extends EngineAssignedWorldManager { return 3000; } - if (getDimension().isInfiniteEnergy()) { - energy += 1000; - fixEnergy(); - } - - if (M.ms() < charge) { - energy += 70; - fixEnergy(); - } - if (precount != null) { entityCount = 0; for (Entity i : precount) { @@ -181,13 +165,6 @@ public class IrisWorldManager extends EngineAssignedWorldManager { precount = null; } - if (energy < 650) { - if (ecl.flip()) { - energy *= 1 + (0.02 * M.clip((1D - getEntitySaturation()), 0D, 1D)); - fixEnergy(); - } - } - onAsyncTick(); } @@ -413,11 +390,6 @@ public class IrisWorldManager extends EngineAssignedWorldManager { actuallySpawned = 0; - if (energy < 100) { - J.sleep(200); - return false; - } - if (!getEngine().getWorld().hasRealWorld()) { Iris.debug("Can't spawn. No real world"); J.sleep(5000); @@ -471,7 +443,6 @@ public class IrisWorldManager extends EngineAssignedWorldManager { spawnChunkSafely(world, c.getX(), c.getZ(), false); } - energy -= (actuallySpawned / 2D); return actuallySpawned > 0; } @@ -581,10 +552,6 @@ public class IrisWorldManager extends EngineAssignedWorldManager { } } - private void fixEnergy() { - energy = M.clip(energy, 1D, getDimension().getMaximumEnergy()); - } - private void spawnIn(Chunk c, boolean initial) { if (getEngine().isClosed()) { return; @@ -599,10 +566,6 @@ public class IrisWorldManager extends EngineAssignedWorldManager { return; } - if (initial) { - energy += 1.2; - } - if (IrisSettings.get().getWorld().isMarkerEntitySpawningSystem()) { forEachMarkerSpawner(c, (block, spawners) -> { IrisSpawner s = new KList<>(spawners).getRandom(); @@ -663,7 +626,6 @@ public class IrisWorldManager extends EngineAssignedWorldManager { actuallySpawned += s; if (s > 0) { ref.spawn(getEngine(), c.getX(), c.getZ()); - energy -= s * ((i.getEnergyMultiplier() * ref.getEnergyMultiplier() * 1)); } } @@ -676,7 +638,6 @@ public class IrisWorldManager extends EngineAssignedWorldManager { actuallySpawned += s; if (s > 0) { ref.spawn(getEngine(), PowerOfTwoCoordinates.blockToChunkFloor(pos.getX()), PowerOfTwoCoordinates.blockToChunkFloor(pos.getZ())); - energy -= s * ((i.getEnergyMultiplier() * ref.getEnergyMultiplier() * 1)); } } @@ -736,8 +697,6 @@ public class IrisWorldManager extends EngineAssignedWorldManager { Long key = Cache.key(e); cleanup.put(key, cleanupService.schedule(() -> { cleanup.remove(key); - energy += 0.3; - fixEnergy(); getEngine().cleanupMantleChunk(cX, cZ); }, Math.max(IrisSettings.get().getPerformance().mantleCleanupDelay * 50L, 0), TimeUnit.MILLISECONDS)); @@ -816,11 +775,6 @@ public class IrisWorldManager extends EngineAssignedWorldManager { return getEngine().getMantle().getMantle(); } - @Override - public void chargeEnergy() { - charge = M.ms() + 3000; - } - @Override public void teleportAsync(PlayerTeleportEvent e) { if (IrisSettings.get().getWorld().getAsyncTeleport().isEnabled()) { diff --git a/core/src/main/java/art/arcane/iris/engine/UpperDimensionContext.java b/core/src/main/java/art/arcane/iris/engine/UpperDimensionContext.java index e426d3c45..803eb1afd 100644 --- a/core/src/main/java/art/arcane/iris/engine/UpperDimensionContext.java +++ b/core/src/main/java/art/arcane/iris/engine/UpperDimensionContext.java @@ -25,12 +25,14 @@ public class UpperDimensionContext implements DataProvider { private final int chunkHeight; private final ProceduralStream heightStream; private final ProceduralStream biomeStream; + private final ProceduralStream regionStream; private final ProceduralStream rockStream; private final boolean selfReferencing; private UpperDimensionContext(IrisDimension dimension, IrisData data, int chunkHeight, ProceduralStream heightStream, ProceduralStream biomeStream, + ProceduralStream regionStream, ProceduralStream rockStream, boolean selfReferencing) { this.dimension = dimension; @@ -38,6 +40,7 @@ public class UpperDimensionContext implements DataProvider { this.chunkHeight = chunkHeight; this.heightStream = heightStream; this.biomeStream = biomeStream; + this.regionStream = regionStream; this.rockStream = rockStream; this.selfReferencing = selfReferencing; } @@ -59,6 +62,7 @@ public class UpperDimensionContext implements DataProvider { chunkHeight, complex.getHeightStream(), complex.getBaseBiomeStream(), + complex.getRegionStream(), complex.getRockStream(), true ); @@ -233,6 +237,7 @@ public class UpperDimensionContext implements DataProvider { chunkHeight, heightStream, baseBiomeStream, + regionStream, rockStream, false ); @@ -247,6 +252,10 @@ public class UpperDimensionContext implements DataProvider { return biomeStream.get((double) x, (double) z); } + public IrisRegion getUpperRegion(int x, int z) { + return regionStream == null ? null : regionStream.get((double) x, (double) z); + } + public BlockData getRockBlock(int x, int z) { return rockStream.get((double) x, (double) z); } diff --git a/core/src/main/java/art/arcane/iris/engine/actuator/IrisDecorantActuator.java b/core/src/main/java/art/arcane/iris/engine/actuator/IrisDecorantActuator.java index 4c0250cb8..e3faa6154 100644 --- a/core/src/main/java/art/arcane/iris/engine/actuator/IrisDecorantActuator.java +++ b/core/src/main/java/art/arcane/iris/engine/actuator/IrisDecorantActuator.java @@ -23,6 +23,7 @@ import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.EngineAssignedActuator; import art.arcane.iris.engine.framework.EngineDecorator; import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.util.common.data.B; import art.arcane.iris.util.project.context.ChunkContext; import art.arcane.volmlib.util.documentation.BlockCoordinates; import art.arcane.iris.util.project.hunk.Hunk; @@ -87,7 +88,8 @@ public class IrisDecorantActuator extends EngineAssignedActuator { continue; } - if (height < getDimension().getFluidHeight() && PREDICATE_SOLID.test(output.get(i, height, j))) { + if (height < getDimension().getFluidHeight() && PREDICATE_SOLID.test(output.get(i, height, j)) + && height + 1 < output.getHeight() && B.isWater(output.get(i, height + 1, j))) { getSeaSurfaceDecorator().decorate(i, j, realX, Math.round(i + 1), Math.round(x + i - 1), realZ, Math.round(z + j + 1), Math.round(z + j - 1), diff --git a/core/src/main/java/art/arcane/iris/engine/decorator/IrisFloatingSurfaceDecorator.java b/core/src/main/java/art/arcane/iris/engine/decorator/IrisFloatingSurfaceDecorator.java new file mode 100644 index 000000000..a2d257adc --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/decorator/IrisFloatingSurfaceDecorator.java @@ -0,0 +1,33 @@ +/* + * 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.decorator; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.IrisDecorator; + +public class IrisFloatingSurfaceDecorator extends IrisSurfaceDecorator { + public IrisFloatingSurfaceDecorator(Engine engine) { + super(engine, "Floating Surface"); + } + + @Override + protected boolean isSlopeValid(IrisDecorator decorator, int realX, int realZ) { + return true; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/decorator/IrisSurfaceDecorator.java b/core/src/main/java/art/arcane/iris/engine/decorator/IrisSurfaceDecorator.java index 5eec3fdf9..3f187b72d 100644 --- a/core/src/main/java/art/arcane/iris/engine/decorator/IrisSurfaceDecorator.java +++ b/core/src/main/java/art/arcane/iris/engine/decorator/IrisSurfaceDecorator.java @@ -39,6 +39,17 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator { super(engine, "Surface", IrisDecorationPart.NONE); } + protected IrisSurfaceDecorator(Engine engine, String name) { + super(engine, name, IrisDecorationPart.NONE); + } + + protected boolean isSlopeValid(IrisDecorator decorator, int realX, int realZ) { + if (decorator.isForcePlace() || decorator.getSlopeCondition().isDefault()) { + return true; + } + return decorator.getSlopeCondition().isValid(getComplex().getSlopeStream().get(realX, realZ)); + } + @BlockCoordinates @Override public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1, Hunk data, IrisBiome biome, int height, int max) { @@ -54,8 +65,7 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator { boolean caveSkipFluid = biome.getInferredType() == InferredType.CAVE; if (decorator != null) { - if (!decorator.isForcePlace() && !decorator.getSlopeCondition().isDefault() - && !decorator.getSlopeCondition().isValid(getComplex().getSlopeStream().get(realX, realZ))) { + if (!isSlopeValid(decorator, realX, realZ)) { return; } diff --git a/core/src/main/java/art/arcane/iris/engine/framework/Engine.java b/core/src/main/java/art/arcane/iris/engine/framework/Engine.java index 7fbbab73a..ab2dcacf5 100644 --- a/core/src/main/java/art/arcane/iris/engine/framework/Engine.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/Engine.java @@ -916,7 +916,13 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat return o.getObject().getLoadKey() + "@" + o.getId(); } - return null; + MantleChunk chunk = getMantle().getMantle().getChunk(x >> 4, z >> 4).use(); + try { + String raw = chunk.get(x & 15, y, z & 15, String.class); + return (raw == null || raw.isEmpty()) ? null : raw; + } finally { + chunk.release(); + } } default PlacedObject getObjectPlacement(int x, int y, int z) { @@ -936,6 +942,9 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat String[] v = objectAt.split("\\Q@\\E"); String object = v[0]; + if (object.isEmpty() || object.equals("null")) { + return null; + } int id = Integer.parseInt(v[1]); diff --git a/core/src/main/java/art/arcane/iris/engine/framework/EngineWorldManager.java b/core/src/main/java/art/arcane/iris/engine/framework/EngineWorldManager.java index 2889a404f..ff8cc8991 100644 --- a/core/src/main/java/art/arcane/iris/engine/framework/EngineWorldManager.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/EngineWorldManager.java @@ -27,8 +27,6 @@ import org.bukkit.event.player.PlayerTeleportEvent; public interface EngineWorldManager { void close(); - double getEnergy(); - int getEntityCount(); int getChunkCount(); @@ -47,7 +45,5 @@ public interface EngineWorldManager { void onChunkUnload(Chunk e); - void chargeEnergy(); - void teleportAsync(PlayerTeleportEvent e); } diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/MantleWriter.java b/core/src/main/java/art/arcane/iris/engine/mantle/MantleWriter.java index 893156ec3..47608f25e 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/MantleWriter.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/MantleWriter.java @@ -163,6 +163,10 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable { return; } + if (y == 0 && t instanceof BlockData && engineMantle.getEngine().getDimension().isBedrock()) { + return; + } + MantleChunk chunk = acquireChunk(cx, cz); if (chunk == null) return; diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleFloatingObjectComponent.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleFloatingObjectComponent.java new file mode 100644 index 000000000..04d08f439 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleFloatingObjectComponent.java @@ -0,0 +1,274 @@ +/* + * 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.mantle.components; + +import art.arcane.iris.Iris; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.IrisComplex; +import art.arcane.iris.engine.data.cache.Cache; +import art.arcane.iris.engine.mantle.ComponentFlag; +import art.arcane.iris.engine.mantle.EngineMantle; +import art.arcane.iris.engine.mantle.IrisMantleComponent; +import art.arcane.iris.engine.mantle.MantleWriter; +import art.arcane.iris.engine.modifier.IrisFloatingChildBiomeModifier; +import art.arcane.iris.engine.object.CarvingMode; +import art.arcane.iris.engine.object.FloatingIslandSample; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisFloatingChildBiomes; +import art.arcane.iris.engine.object.IrisObject; +import art.arcane.iris.engine.object.IrisObjectPlacement; +import art.arcane.iris.engine.object.ObjectPlaceMode; +import art.arcane.iris.util.project.context.ChunkContext; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.documentation.ChunkCoordinates; +import art.arcane.volmlib.util.mantle.flag.ReservedFlag; +import art.arcane.volmlib.util.math.RNG; + +@ComponentFlag(ReservedFlag.FLOATING_OBJECT) +public class MantleFloatingObjectComponent extends IrisMantleComponent { + + public MantleFloatingObjectComponent(EngineMantle engineMantle) { + super(engineMantle, ReservedFlag.FLOATING_OBJECT, 2); + } + + @Override + public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) { + IrisComplex complex = context.getComplex(); + IrisData data = getData(); + int chunkHeight = getEngineMantle().getEngine().getHeight(); + int minX = x << 4; + int minZ = z << 4; + long baseSeed = getEngineMantle().getEngine().getSeedManager().getTerrain() ^ IrisFloatingChildBiomeModifier.FLOATING_BASE_SEED_SALT; + RNG chunkRng = new RNG(Cache.key(x, z) + seed() + 0x0FA710BEL); + + FloatingIslandSample.clearChunkMemo(); + + FloatingIslandSample[] samples = new FloatingIslandSample[256]; + for (int xf = 0; xf < 16; xf++) { + for (int zf = 0; zf < 16; zf++) { + int wx = minX + xf; + int wz = minZ + zf; + IrisBiome parent = complex.getTrueBiomeStream().get(wx, wz); + if (parent == null || parent.getFloatingChildBiomes() == null || parent.getFloatingChildBiomes().isEmpty()) { + continue; + } + FloatingIslandSample sample = FloatingIslandSample.sampleMemoized(parent, wx, wz, chunkHeight, baseSeed, data, complex, getEngineMantle().getEngine()); + if (sample != null) { + samples[(zf << 4) | xf] = sample; + } + } + } + + java.util.IdentityHashMap> entryColumns = new java.util.IdentityHashMap<>(); + for (int i = 0; i < 256; i++) { + FloatingIslandSample s = samples[i]; + if (s == null || s.entry == null) { + continue; + } + entryColumns.computeIfAbsent(s.entry, e -> new KList<>()).add(i); + } + + for (java.util.Map.Entry> ec : entryColumns.entrySet()) { + IrisFloatingChildBiomes entry = ec.getKey(); + KList columns = ec.getValue(); + if (columns.isEmpty()) { + continue; + } + + IrisBiome parent = complex.getTrueBiomeStream().get(minX + (columns.get(0) & 15), minZ + (columns.get(0) >> 4)); + IrisBiome target = entry.getRealBiome(parent, data); + + KList floating = entry.getFloatingObjects(); + if (floating != null && !floating.isEmpty()) { + for (IrisObjectPlacement placement : floating) { + tryPlaceFloatingChunk(writer, complex, chunkRng, data, placement, samples, columns, minX, minZ, entry); + } + } + + if (entry.isInheritObjects() && target != null) { + for (IrisObjectPlacement placement : target.getSurfaceObjects()) { + tryPlaceAnchoredChunk(writer, complex, chunkRng, data, placement, samples, columns, minX, minZ, entry); + } + } + + KList extras = entry.getExtraObjects(); + if (extras != null && !extras.isEmpty()) { + for (IrisObjectPlacement placement : extras) { + tryPlaceAnchoredChunk(writer, complex, chunkRng, data, placement, samples, columns, minX, minZ, entry); + } + } + } + } + + @ChunkCoordinates + private void tryPlaceFloatingChunk(MantleWriter writer, IrisComplex complex, RNG rng, IrisData data, IrisObjectPlacement placement, FloatingIslandSample[] samples, KList columns, int minX, int minZ, IrisFloatingChildBiomes entry) { + if (placement == null || columns == null || columns.isEmpty()) { + return; + } + int density = placement.getDensity(rng, minX, minZ, data); + double perAttempt = placement.getChance(); + for (int i = 0; i < density; i++) { + if (!rng.chance(perAttempt + rng.d(-0.005, 0.005))) { + continue; + } + IrisObject raw = placement.getObject(complex, rng); + if (raw == null) { + continue; + } + IrisObject obj0 = placement.getScale().get(rng, raw); + if (obj0 == null) { + continue; + } + if (entry != null && entry.hasObjectShrink()) { + obj0 = entry.getShrinkScale().get(rng, obj0); + if (obj0 == null) { + continue; + } + } + final IrisObject obj = obj0; + + int key = columns.get(rng.i(0, columns.size() - 1)); + int xx = minX + (key & 15); + int zz = minZ + (key >> 4); + IrisObjectPlacement floatingPlacement = placement.toPlacement(obj.getLoadKey()); + int id = rng.i(0, Integer.MAX_VALUE); + + try { + obj.place(xx, -1, zz, writer, floatingPlacement, rng, (b, bd) -> { + String marker = placementMarker(obj, id); + if (marker != null) { + writer.setData(b.getX(), b.getY(), b.getZ(), marker); + } + }, null, data); + } catch (Throwable e) { + Iris.reportError(e); + } + } + } + + @ChunkCoordinates + private void tryPlaceAnchoredChunk(MantleWriter writer, IrisComplex complex, RNG rng, IrisData data, IrisObjectPlacement placement, FloatingIslandSample[] samples, KList columns, int minX, int minZ, IrisFloatingChildBiomes entry) { + if (placement == null || columns.isEmpty()) { + return; + } + KList interior = interiorColumns(samples, columns); + KList pickPool = interior.isEmpty() ? columns : interior; + int density = placement.getDensity(rng, minX, minZ, data); + double perAttempt = placement.getChance(); + for (int i = 0; i < density; i++) { + if (!rng.chance(perAttempt + rng.d(-0.005, 0.005))) { + continue; + } + IrisObject raw = placement.getObject(complex, rng); + if (raw == null) { + continue; + } + IrisObject obj0 = placement.getScale().get(rng, raw); + if (obj0 == null) { + continue; + } + if (entry != null && entry.hasObjectShrink()) { + obj0 = entry.getShrinkScale().get(rng, obj0); + if (obj0 == null) { + continue; + } + } + final IrisObject obj = obj0; + + int key = pickPool.get(rng.i(0, pickPool.size() - 1)); + int xf = key & 15; + int zf = key >> 4; + FloatingIslandSample sample = samples[(zf << 4) | xf]; + if (sample == null) { + continue; + } + int wx = minX + xf; + int wz = minZ + zf; + + int anchorY = sample.topY() + 1 + obj.getCenter().getBlockY(); + int id = rng.i(0, Integer.MAX_VALUE); + + IrisObjectPlacement anchored = placement.toPlacement(obj.getLoadKey()); + anchored.setCarvingSupport(CarvingMode.ANYWHERE); + anchored.setForcePlace(true); + anchored.setMode(ObjectPlaceMode.STRUCTURE_PIECE); + anchored.setBore(false); + anchored.setMeld(false); + + try { + obj.place(wx, anchorY, wz, writer, anchored, rng, (b, bd) -> { + String marker = placementMarker(obj, id); + if (marker != null) { + writer.setData(b.getX(), b.getY(), b.getZ(), marker); + } + }, null, data); + } catch (Throwable e) { + Iris.reportError(e); + } + } + } + + private static KList interiorColumns(FloatingIslandSample[] samples, KList columns) { + KList interior = new KList<>(); + for (int key : columns) { + int xf = key & 15; + int zf = key >> 4; + if (xf <= 0 || xf >= 15 || zf <= 0 || zf >= 15) { + continue; + } + if (samples[(zf << 4) | (xf + 1)] == null) continue; + if (samples[(zf << 4) | (xf - 1)] == null) continue; + if (samples[((zf + 1) << 4) | xf] == null) continue; + if (samples[((zf - 1) << 4) | xf] == null) continue; + interior.add(key); + } + return interior; + } + + private static String placementMarker(IrisObject object, int id) { + if (object == null) { + return null; + } + String key = object.getLoadKey(); + if (key == null || key.isEmpty() || key.equals("null")) { + return null; + } + return key + "@" + id; + } + + @Override + protected int computeRadius() { + int maxThickness = 0; + int maxHeightAbove = 0; + try { + for (IrisBiome biome : getDimension().getAllBiomes(this::getData)) { + KList entries = biome.getFloatingChildBiomes(); + if (entries == null || entries.isEmpty()) { + continue; + } + for (IrisFloatingChildBiomes entry : entries) { + maxThickness = Math.max(maxThickness, entry.getMaxThickness()); + maxHeightAbove = Math.max(maxHeightAbove, entry.getMaxHeightAboveSurface()); + } + } + } catch (Throwable ignored) { + } + return Math.max(1, (maxThickness + maxHeightAbove) >> 4); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java index dc3d7623a..53336e3c5 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java @@ -22,6 +22,7 @@ import art.arcane.iris.Iris; import art.arcane.iris.core.IrisSettings; import art.arcane.iris.engine.data.cache.Cache; import art.arcane.iris.engine.IrisComplex; +import art.arcane.iris.engine.UpperDimensionContext; import art.arcane.iris.engine.mantle.ComponentFlag; import art.arcane.iris.engine.mantle.EngineMantle; import art.arcane.iris.engine.mantle.IrisMantleComponent; @@ -60,10 +61,27 @@ public class MantleObjectComponent extends IrisMantleComponent { private static final long CAVE_REJECT_LOG_THROTTLE_MS = 5000L; private static final int SURFACE_HEIGHT_CHUNK_FILL_THRESHOLD = 128; private static final Map CAVE_REJECT_LOG_STATE = new ConcurrentHashMap<>(); + private static final Set MISSING_LOAD_KEY_WARNED = ConcurrentHashMap.newKeySet(); + public MantleObjectComponent(EngineMantle engineMantle) { super(engineMantle, ReservedFlag.OBJECT, 1); } + private static String placementMarker(IrisObject object, int id, String context) { + String key = object == null ? null : object.getLoadKey(); + if (key == null || key.isEmpty() || key.equals("null")) { + String fingerprint = context + "|" + (object == null ? "" : object.getClass().getSimpleName()); + if (MISSING_LOAD_KEY_WARNED.add(fingerprint)) { + java.io.File file = object == null ? null : object.getLoadFile(); + Iris.warn("Skipping placement marker write: IrisObject has no loadKey (context=" + context + + ", file=" + (file == null ? "" : file.getPath()) + "). " + + "This would previously produce 'Couldn't find Object: null' warnings on chunk reload."); + } + return null; + } + return key + "@" + id; + } + @Override public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) { IrisComplex complex = context.getComplex(); @@ -106,6 +124,11 @@ public class MantleObjectComponent extends IrisMantleComponent { + " regionCavePlacers=" + region.getCarvingObjects().size()); } ObjectPlacementSummary summary = placeObjects(writer, rng, x, z, surfaceBiome, caveBiome, region, complex, traceRegen, surfaceHeightLookup); + UpperDimensionContext upperCtx = getEngineMantle().getEngine().getUpperContext(); + IrisDimension dimension = getDimension(); + if (upperCtx != null && dimension.isUpperDimensionObjects()) { + placeUpperObjects(writer, rng, x, z, xxx, zzz, surfaceY, upperCtx, dimension, complex, traceRegen); + } if (traceRegen) { Iris.info("Regen object layer done: chunk=" + x + "," + z + " biomeSurfacePlacersChecked=" + summary.biomeSurfacePlacersChecked() @@ -374,6 +397,9 @@ public class MantleObjectComponent extends IrisMantleComponent { boolean overCave = surfaceObjectExclusionDepth > 0 && hasSurfaceCarveExposure(writer, surfaceHeightLookup, xx, zz, surfaceObjectExclusionDepth, surfaceObjectExclusionRadius); int id = rng.i(0, Integer.MAX_VALUE); IrisObjectPlacement effectivePlacement = resolveEffectivePlacement(objectPlacement, v); + if (effectivePlacement.getMode() == ObjectPlaceMode.FLOATING) { + overCave = false; + } try { int result = -1; String fallbackPath = "surface"; @@ -384,7 +410,10 @@ public class MantleObjectComponent extends IrisMantleComponent { IrisObjectPlacement floorPlacement = effectivePlacement.toPlacement(v.getLoadKey()); floorPlacement.setMode(ObjectPlaceMode.FAST_MIN_HEIGHT); result = v.place(xx, caveFloorY, zz, writer, floorPlacement, rng, (b, data) -> { - writer.setData(b.getX(), b.getY(), b.getZ(), v.getLoadKey() + "@" + id); + String marker = placementMarker(v, id, "cave-floor"); + if (marker != null) { + writer.setData(b.getX(), b.getY(), b.getZ(), marker); + } if (effectivePlacement.isDolphinTarget() && effectivePlacement.isUnderwater() && B.isStorageChest(data)) { writer.setData(b.getX(), b.getY(), b.getZ(), MatterStructurePOI.BURIED_TREASURE); } @@ -396,7 +425,10 @@ public class MantleObjectComponent extends IrisMantleComponent { IrisObjectPlacement stiltPlacement = effectivePlacement.toPlacement(v.getLoadKey()); stiltPlacement.setMode(ObjectPlaceMode.FAST_MIN_STILT); result = v.place(xx, -1, zz, writer, stiltPlacement, rng, (b, data) -> { - writer.setData(b.getX(), b.getY(), b.getZ(), v.getLoadKey() + "@" + id); + String marker = placementMarker(v, id, "stilt"); + if (marker != null) { + writer.setData(b.getX(), b.getY(), b.getZ(), marker); + } if (effectivePlacement.isDolphinTarget() && effectivePlacement.isUnderwater() && B.isStorageChest(data)) { writer.setData(b.getX(), b.getY(), b.getZ(), MatterStructurePOI.BURIED_TREASURE); } @@ -405,7 +437,10 @@ public class MantleObjectComponent extends IrisMantleComponent { } } else { result = v.place(xx, -1, zz, writer, effectivePlacement, rng, (b, data) -> { - writer.setData(b.getX(), b.getY(), b.getZ(), v.getLoadKey() + "@" + id); + String marker = placementMarker(v, id, "surface"); + if (marker != null) { + writer.setData(b.getX(), b.getY(), b.getZ(), marker); + } if (effectivePlacement.isDolphinTarget() && effectivePlacement.isUnderwater() && B.isStorageChest(data)) { writer.setData(b.getX(), b.getY(), b.getZ(), MatterStructurePOI.BURIED_TREASURE); } @@ -557,7 +592,10 @@ public class MantleObjectComponent extends IrisMantleComponent { try { int result = object.place(x, y, z, writer, effectivePlacement, rng, (b, data) -> { wrotePlacementData.set(true); - writer.setData(b.getX(), b.getY(), b.getZ(), object.getLoadKey() + "@" + id); + String marker = placementMarker(object, id, "cave"); + if (marker != null) { + writer.setData(b.getX(), b.getY(), b.getZ(), marker); + } if (effectivePlacement.isDolphinTarget() && effectivePlacement.isUnderwater() && B.isStorageChest(data)) { writer.setData(b.getX(), b.getY(), b.getZ(), MatterStructurePOI.BURIED_TREASURE); } @@ -629,6 +667,141 @@ public class MantleObjectComponent extends IrisMantleComponent { return new ObjectPlacementResult(attempts, placed, rejected, nullObjects, errors); } + @ChunkCoordinates + private void placeUpperObjects( + MantleWriter writer, + RNG rng, + int chunkX, + int chunkZ, + int centerX, + int centerZ, + int lowerSurfaceCenterY, + UpperDimensionContext upperCtx, + IrisDimension dimension, + IrisComplex complex, + boolean traceRegen + ) { + IrisBiome upperBiome = upperCtx.getUpperBiome(centerX, centerZ); + IrisRegion upperRegion = upperCtx.getUpperRegion(centerX, centerZ); + if (upperBiome == null && upperRegion == null) { + return; + } + + boolean forcePlace = dimension.isUpperObjectsForcePlace(); + if (upperBiome != null) { + for (IrisObjectPlacement i : upperBiome.getSurfaceObjects()) { + if (!rng.chance(i.getChance() + rng.d(-0.005, 0.005))) { + continue; + } + try { + placeUpperObject(writer, rng, chunkX, chunkZ, i, upperCtx, dimension, complex, forcePlace, traceRegen, "upper-biome-surface"); + } catch (Throwable e) { + Iris.reportError(e); + Iris.error("Failed to place upper-dimension objects in biome " + upperBiome.getName() + + ": " + i.getPlace().toString(", ") + " (" + e.getClass().getSimpleName() + ")"); + e.printStackTrace(); + } + } + } + + if (upperRegion != null) { + for (IrisObjectPlacement i : upperRegion.getSurfaceObjects()) { + if (!rng.chance(i.getChance() + rng.d(-0.005, 0.005))) { + continue; + } + try { + placeUpperObject(writer, rng, chunkX, chunkZ, i, upperCtx, dimension, complex, forcePlace, traceRegen, "upper-region-surface"); + } catch (Throwable e) { + Iris.reportError(e); + Iris.error("Failed to place upper-dimension objects in region " + upperRegion.getName() + + ": " + i.getPlace().toString(", ") + " (" + e.getClass().getSimpleName() + ")"); + e.printStackTrace(); + } + } + } + } + + @ChunkCoordinates + private void placeUpperObject( + MantleWriter writer, + RNG rng, + int chunkX, + int chunkZ, + IrisObjectPlacement objectPlacement, + UpperDimensionContext upperCtx, + IrisDimension dimension, + IrisComplex complex, + boolean forcePlace, + boolean traceRegen, + String scope + ) { + int chunkHeight = getEngineMantle().getEngine().getHeight(); + int upperGap = dimension.getUpperDimensionGap(); + int minX = chunkX << 4; + int minZ = chunkZ << 4; + int density = objectPlacement.getDensity(rng, minX, minZ, getData()); + + for (int i = 0; i < density; i++) { + IrisObject v = objectPlacement.getScale().get(rng, objectPlacement.getObject(complex, rng)); + if (v == null) { + continue; + } + + int xx = rng.i(minX, minX + 15); + int zz = rng.i(minZ, minZ + 15); + int columnLowerSurfaceY = getEngineMantle().getEngine().getHeight(xx, zz, true); + int rawUpperSurface = upperCtx.getUpperSurfaceY(xx, zz); + int upperSurfaceY = Math.max(rawUpperSurface, columnLowerSurfaceY + upperGap); + if (upperSurfaceY >= chunkHeight - 2) { + continue; + } + + int halfH = Math.floorDiv(v.getH(), 2); + int anchorY = upperSurfaceY - 1 - halfH; + if (anchorY <= 1) { + continue; + } + + int id = rng.i(0, Integer.MAX_VALUE); + IrisObjectPlacement placement = objectPlacement.toPlacement(v.getLoadKey()); + placement.setMode(ObjectPlaceMode.CENTER_HEIGHT); + placement.setRotation(buildUpsideDownRotation()); + placement.setCarvingSupport(CarvingMode.ANYWHERE); + if (forcePlace) { + placement.setForcePlace(true); + } + + int result = v.place(xx, anchorY, zz, writer, placement, rng, (b, data) -> { + String marker = placementMarker(v, id, "upper"); + if (marker != null) { + writer.setData(b.getX(), b.getY(), b.getZ(), marker); + } + if (placement.isDolphinTarget() && placement.isUnderwater() && B.isStorageChest(data)) { + writer.setData(b.getX(), b.getY(), b.getZ(), MatterStructurePOI.BURIED_TREASURE); + } + }, null, getData()); + + if (traceRegen) { + Iris.info("Upper object placement: chunk=" + chunkX + "," + chunkZ + + " scope=" + scope + + " object=" + v.getLoadKey() + + " anchorY=" + anchorY + + " upperSurfaceY=" + upperSurfaceY + + " resultY=" + result + + " forcePlace=" + forcePlace); + } + } + } + + private IrisObjectRotation buildUpsideDownRotation() { + IrisObjectRotation rt = new IrisObjectRotation(); + rt.setEnabled(true); + rt.setXAxis(new IrisAxisRotationClamp(true, true, 180D, 180D, 90D)); + rt.setYAxis(new IrisAxisRotationClamp(true, false, 0D, 0D, 90D)); + rt.setZAxis(new IrisAxisRotationClamp()); + return rt; + } + private void logCaveReject( String scope, String reason, @@ -698,18 +871,18 @@ public class MantleObjectComponent extends IrisMantleComponent { } String normalized = loadKey.toLowerCase(Locale.ROOT); - boolean legacyImported = normalized.startsWith("imports/") + boolean imported = normalized.startsWith("imports/") || normalized.contains("/imports/") || normalized.contains("imports/"); - IrisExternalDatapack externalDatapack = resolveExternalDatapackForObjectKey(normalized); - boolean externalImported = externalDatapack != null; - boolean imported = legacyImported || externalImported; if (!imported) { return objectPlacement; } ObjectPlaceMode mode = objectPlacement.getMode(); + if (mode == ObjectPlaceMode.FLOATING || mode == ObjectPlaceMode.STRUCTURE_PIECE) { + return objectPlacement; + } boolean needsModeChange = mode != ObjectPlaceMode.FAST_MIN_STILT; if (!needsModeChange) { return objectPlacement; @@ -720,42 +893,6 @@ public class MantleObjectComponent extends IrisMantleComponent { return effectivePlacement; } - private IrisExternalDatapack resolveExternalDatapackForObjectKey(String normalizedLoadKey) { - if (normalizedLoadKey == null || normalizedLoadKey.isBlank()) { - return null; - } - - int slash = normalizedLoadKey.indexOf('/'); - if (slash <= 0) { - return null; - } - String candidateId = normalizedLoadKey.substring(0, slash); - if (candidateId.isBlank()) { - return null; - } - - IrisDimension dimension = getDimension(); - if (dimension == null || dimension.getExternalDatapacks() == null || dimension.getExternalDatapacks().isEmpty()) { - return null; - } - - for (IrisExternalDatapack externalDatapack : dimension.getExternalDatapacks()) { - if (externalDatapack == null || !externalDatapack.isEnabled()) { - continue; - } - - String id = externalDatapack.getId(); - if (id == null || id.isBlank()) { - continue; - } - if (candidateId.equals(id.toLowerCase(Locale.ROOT))) { - return externalDatapack; - } - } - - return null; - } - private int findNearestCaveFloor(MantleWriter writer, int x, int z) { KList anchors = scanCaveAnchorColumn(writer, IrisCaveAnchorMode.FLOOR, 1, 0, x, z); if (anchors.isEmpty()) { diff --git a/core/src/main/java/art/arcane/iris/engine/mode/ModeOverworld.java b/core/src/main/java/art/arcane/iris/engine/mode/ModeOverworld.java index 8faecf63c..0a5e6130e 100644 --- a/core/src/main/java/art/arcane/iris/engine/mode/ModeOverworld.java +++ b/core/src/main/java/art/arcane/iris/engine/mode/ModeOverworld.java @@ -45,6 +45,7 @@ public class ModeOverworld extends IrisEngineMode implements EngineMode { var deposit = new IrisDepositModifier(getEngine()); var perfection = new IrisPerfectionModifier(getEngine()); var custom = new IrisCustomModifier(getEngine()); + var floatingChildBiomes = new IrisFloatingChildBiomeModifier(getEngine()); EngineStage sBiome = (x, z, k, p, m, c) -> biome.actuate(x, z, p, m, c); EngineStage sGenMatter = (x, z, k, p, m, c) -> { if (shouldBypassMantleStages(getEngine())) { @@ -78,6 +79,8 @@ public class ModeOverworld extends IrisEngineMode implements EngineMode { } getMantle().insertMatter(x >> 4, z >> 4, BlockData.class, K, m); }; + EngineStage sFloatingTerrainSolid = (x, z, k, p, m, c) -> floatingChildBiomes.modify(x, z, k, m, c); + EngineStage sFloatingDecorate = (x, z, k, p, m, c) -> floatingChildBiomes.decorateColumns(x, z, k, m, c); EngineStage sPerfection = (x, z, k, p, m, c) -> perfection.modify(x, z, k, m, c); EngineStage sCustom = (x, z, k, p, m, c) -> { if (shouldBypassMantleStages(getEngine())) { @@ -94,11 +97,13 @@ public class ModeOverworld extends IrisEngineMode implements EngineMode { sCave, sPost )); + registerStage(sFloatingTerrainSolid); registerStage(burst( sDeposit, sInsertMatter, sDecorant )); + registerStage(sFloatingDecorate); registerStage(sPerfection); registerStage(sCustom); } diff --git a/core/src/main/java/art/arcane/iris/engine/modifier/IrisFloatingChildBiomeModifier.java b/core/src/main/java/art/arcane/iris/engine/modifier/IrisFloatingChildBiomeModifier.java new file mode 100644 index 000000000..db6e8a1c3 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/modifier/IrisFloatingChildBiomeModifier.java @@ -0,0 +1,305 @@ +/* + * 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.modifier; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.nms.INMS; +import art.arcane.iris.engine.IrisComplex; +import art.arcane.iris.engine.decorator.IrisFloatingSurfaceDecorator; +import art.arcane.iris.engine.decorator.IrisSeaSurfaceDecorator; +import static art.arcane.iris.engine.mantle.EngineMantle.AIR; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.EngineAssignedModifier; +import art.arcane.iris.engine.framework.EngineDecorator; +import art.arcane.iris.engine.object.FloatingIslandSample; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisBiomeCustom; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisFloatingChildBiomes; +import art.arcane.iris.util.common.data.B; +import art.arcane.iris.util.project.context.ChunkContext; +import art.arcane.iris.util.project.hunk.Hunk; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.volmlib.util.matter.MatterBiomeInject; +import art.arcane.volmlib.util.matter.slices.BiomeInjectMatter; +import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; +import org.bukkit.block.Biome; +import org.bukkit.block.data.BlockData; + +public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier { + public static final long FLOATING_BASE_SEED_SALT = 0x5EED_F107_00F1B10CL; + private static final java.util.concurrent.atomic.AtomicLong columnsChecked = new java.util.concurrent.atomic.AtomicLong(); + private static final java.util.concurrent.atomic.AtomicLong samplesAccepted = new java.util.concurrent.atomic.AtomicLong(); + private static final java.util.concurrent.atomic.AtomicLong decorateInvocations = new java.util.concurrent.atomic.AtomicLong(); + private static final java.util.concurrent.atomic.AtomicLong decorateSkippedNotAir = new java.util.concurrent.atomic.AtomicLong(); + private static final java.util.concurrent.atomic.AtomicLong decorateSkippedNoInherit = new java.util.concurrent.atomic.AtomicLong(); + private static final java.util.concurrent.atomic.AtomicLong decoratePhaseColumns = new java.util.concurrent.atomic.AtomicLong(); + private static final java.util.concurrent.atomic.AtomicLong decoratePlaced = new java.util.concurrent.atomic.AtomicLong(); + private static final java.util.concurrent.atomic.AtomicLong decorateNoChange = new java.util.concurrent.atomic.AtomicLong(); + private static final java.util.concurrent.atomic.AtomicLong decorateFloorNull = new java.util.concurrent.atomic.AtomicLong(); + private static final java.util.concurrent.ConcurrentHashMap floorMatHisto = new java.util.concurrent.ConcurrentHashMap<>(); + private static final java.util.concurrent.atomic.AtomicLong lastReportMs = new java.util.concurrent.atomic.AtomicLong(0L); + private final RNG rng; + private final EngineDecorator surfaceDecorator; + private final EngineDecorator seaSurfaceDecorator; + + public static void reportFloatingStats() { + StringBuilder topFloors = new StringBuilder(); + floorMatHisto.entrySet().stream() + .sorted((a, b) -> Long.compare(b.getValue().get(), a.getValue().get())) + .limit(5) + .forEach(e -> topFloors.append(' ').append(e.getKey()).append('=').append(e.getValue().get())); + art.arcane.iris.Iris.info("[floating-debug] columns=" + columnsChecked.get() + + " samples=" + samplesAccepted.get() + + " decInvoke=" + decorateInvocations.get() + + " decPlaced=" + decoratePlaced.get() + + " decNoChange=" + decorateNoChange.get() + + " decFloorNull=" + decorateFloorNull.get() + + " decSkipNonAir=" + decorateSkippedNotAir.get() + + " decSkipNoInherit=" + decorateSkippedNoInherit.get() + + " decPhaseCols=" + decoratePhaseColumns.get() + + " topFloors:" + (topFloors.length() == 0 ? " " : topFloors.toString())); + } + + private static void maybeReport() { + long now = System.currentTimeMillis(); + long last = lastReportMs.get(); + if (now - last >= 10000L && lastReportMs.compareAndSet(last, now)) { + reportFloatingStats(); + } + } + + public IrisFloatingChildBiomeModifier(Engine engine) { + super(engine, "FloatingChildBiomes"); + rng = new RNG(engine.getSeedManager().getTerrain() ^ 0x7EB0A73F1DCE514DL); + surfaceDecorator = new IrisFloatingSurfaceDecorator(engine); + seaSurfaceDecorator = new IrisSeaSurfaceDecorator(engine); + } + + @Override + public void onModify(int x, int z, Hunk output, boolean multicore, ChunkContext context) { + PrecisionStopwatch p = PrecisionStopwatch.start(); + int chunkHeight = output.getHeight(); + IrisData data = getData(); + IrisDimension dimension = getDimension(); + IrisComplex complex = getComplex(); + long baseSeed = getEngine().getSeedManager().getTerrain() ^ FLOATING_BASE_SEED_SALT; + + for (int xf = 0; xf < 16; xf++) { + for (int zf = 0; zf < 16; zf++) { + int wx = x + xf; + int wz = z + zf; + IrisBiome parent = complex.getTrueBiomeStream().get(wx, wz); + if (parent == null || parent.getFloatingChildBiomes() == null || parent.getFloatingChildBiomes().isEmpty()) { + continue; + } + columnsChecked.incrementAndGet(); + + FloatingIslandSample sample = FloatingIslandSample.sampleMemoized(parent, wx, wz, chunkHeight, baseSeed, data, complex, getEngine()); + if (sample == null) { + continue; + } + samplesAccepted.incrementAndGet(); + + IrisFloatingChildBiomes entry = sample.entry; + IrisBiome target = entry.getRealBiome(parent, data); + long colSeed = FloatingIslandSample.columnSeed(baseSeed, wx, wz); + RNG layerRng = rng.nextParallelRNG((int) (colSeed ^ 0x7A4E)); + int paletteDepth = Math.max(4, sample.solidCount + 4); + KList blocks = target.generateLayers(dimension, wx, wz, layerRng, paletteDepth, paletteDepth, data, complex); + if (blocks == null || blocks.isEmpty()) { + blocks = parent.generateLayers(dimension, wx, wz, layerRng, paletteDepth, paletteDepth, data, complex); + } + BlockData fallbackSolid = B.get("minecraft:stone"); + + int depth = 0; + for (int k = sample.topIdx; k >= 0; k--) { + if (!sample.solidMask[k]) { + continue; + } + int y = sample.islandBaseY + k; + if (y < 0 || y >= chunkHeight) { + continue; + } + BlockData block = null; + if (blocks != null && !blocks.isEmpty()) { + block = blocks.hasIndex(depth) ? blocks.get(depth) : blocks.getLast(); + } + if (block == null) { + block = fallbackSolid; + } + if (block != null) { + output.set(xf, y, zf, block); + } + depth++; + } + + Integer localFluidHeight = entry.getLocalFluidHeight(); + if (localFluidHeight != null && localFluidHeight > 0) { + BlockData fluid = B.get(entry.getFluidBlock()); + if (fluid == null) { + fluid = B.get("minecraft:water"); + } + int fluidCap = Math.min(sample.thickness - 1, localFluidHeight); + for (int k = 1; k <= fluidCap; k++) { + if (sample.solidMask[k]) { + continue; + } + int y = sample.islandBaseY + k; + if (y < 0 || y >= chunkHeight) { + continue; + } + boolean hasSolidBelow = false; + for (int kb = k - 1; kb >= 0; kb--) { + if (sample.solidMask[kb]) { + hasSolidBelow = true; + break; + } + } + if (hasSolidBelow) { + output.set(xf, y, zf, fluid); + } + } + } + + if (target != null) { + writeIslandSkyBiome(target, wx, wz, sample, chunkHeight); + } + } + } + + getEngine().getMetrics().getDeposit().put(p.getMilliseconds()); + } + + public void decorateColumns(int x, int z, Hunk output, boolean multicore, ChunkContext context) { + int chunkHeight = output.getHeight(); + IrisData data = getData(); + IrisComplex complex = getComplex(); + long baseSeed = getEngine().getSeedManager().getTerrain() ^ FLOATING_BASE_SEED_SALT; + + for (int xf = 0; xf < 16; xf++) { + for (int zf = 0; zf < 16; zf++) { + int wx = x + xf; + int wz = z + zf; + IrisBiome parent = complex.getTrueBiomeStream().get(wx, wz); + if (parent == null || parent.getFloatingChildBiomes() == null || parent.getFloatingChildBiomes().isEmpty()) { + continue; + } + FloatingIslandSample sample = FloatingIslandSample.sampleMemoized(parent, wx, wz, chunkHeight, baseSeed, data, complex, getEngine()); + if (sample == null) { + continue; + } + decoratePhaseColumns.incrementAndGet(); + IrisFloatingChildBiomes entry = sample.entry; + IrisBiome target = entry.getRealBiome(parent, data); + + if (!entry.isInheritDecorators() || target == null) { + decorateSkippedNoInherit.incrementAndGet(); + continue; + } + + int topY = sample.topY(); + int max = Math.max(1, chunkHeight - topY); + if (topY + 1 < chunkHeight) { + BlockData above = output.get(xf, topY + 1, zf); + if (above == null || B.isAir(above)) { + decorateInvocations.incrementAndGet(); + BlockData floor = topY >= 0 && topY < chunkHeight ? output.get(xf, topY, zf) : null; + if (floor == null) { + decorateFloorNull.incrementAndGet(); + } else { + String matKey = floor.getMaterial().getKey().getKey(); + floorMatHisto.computeIfAbsent(matKey, k -> new java.util.concurrent.atomic.AtomicLong()).incrementAndGet(); + } + try { + surfaceDecorator.decorate(xf, zf, wx, wz, output, target, topY, max); + } catch (Throwable e) { + art.arcane.iris.Iris.reportError(e); + } + BlockData afterAbove = output.get(xf, topY + 1, zf); + if (afterAbove != null && !B.isAir(afterAbove)) { + decoratePlaced.incrementAndGet(); + } else { + decorateNoChange.incrementAndGet(); + } + } else { + decorateSkippedNotAir.incrementAndGet(); + } + } + + Integer localFluidHeight = entry.getLocalFluidHeight(); + if (localFluidHeight != null && localFluidHeight > 0) { + int fluidCap = Math.min(sample.thickness - 1, localFluidHeight); + int fluidTopY = -1; + for (int k = 1; k <= fluidCap; k++) { + if (sample.solidMask[k]) { + continue; + } + int y = sample.islandBaseY + k; + if (y < 0 || y >= chunkHeight) { + continue; + } + boolean hasSolidBelow = false; + for (int kb = k - 1; kb >= 0; kb--) { + if (sample.solidMask[kb]) { + hasSolidBelow = true; + break; + } + } + if (hasSolidBelow && y > fluidTopY) { + fluidTopY = y; + } + } + if (fluidTopY > 0 && fluidTopY + 1 < chunkHeight && B.isAir(output.get(xf, fluidTopY + 1, zf))) { + try { + seaSurfaceDecorator.decorate(xf, zf, + wx, wx + 1, wx - 1, + wz, wz + 1, wz - 1, + output, target, fluidTopY, chunkHeight); + } catch (Throwable e) { + art.arcane.iris.Iris.reportError(e); + } + } + } + } + } + maybeReport(); + } + + private void writeIslandSkyBiome(IrisBiome target, int wx, int wz, FloatingIslandSample sample, int chunkHeight) { + try { + MatterBiomeInject matter; + if (target.isCustom()) { + IrisBiomeCustom custom = target.getCustomBiome(rng, wx, 0, wz); + matter = BiomeInjectMatter.get(INMS.get().getBiomeBaseIdForKey(getDimension().getLoadKey() + ":" + custom.getId())); + } else { + Biome v = target.getSkyBiome(rng, wx, 0, wz); + matter = BiomeInjectMatter.get(v); + } + int yFrom = Math.max(0, sample.islandBaseY); + int yTo = Math.min(chunkHeight - 1, sample.islandBaseY + sample.topIdx); + for (int y = yFrom; y <= yTo; y += 4) { + getEngine().getMantle().getMantle().set(wx, y, wz, matter); + } + } catch (Throwable e) { + art.arcane.iris.Iris.reportError(e); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/FloatingIslandSample.java b/core/src/main/java/art/arcane/iris/engine/object/FloatingIslandSample.java new file mode 100644 index 000000000..767cde743 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/FloatingIslandSample.java @@ -0,0 +1,304 @@ +/* + * 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.Iris; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.IrisComplex; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.util.project.noise.CNG; +import art.arcane.volmlib.util.collection.KList; + +import java.util.HashMap; +import java.util.concurrent.atomic.AtomicBoolean; + +public final class FloatingIslandSample { + public static final int REJECT_NONE = 0; + public static final int REJECT_NO_ENTRIES = 1; + public static final int REJECT_NO_SEED = 2; + public static final int REJECT_NO_PICK = 3; + public static final int REJECT_ABOVE_HEIGHT = 4; + public static final int REJECT_NO_THICKNESS = 5; + public static final int REJECT_NO_SOLID = 6; + public static final int REJECT_COUNT = 7; + public static final int REJECT_CLUSTER = REJECT_NO_SEED; + + private static final ThreadLocal LAST_REJECT = ThreadLocal.withInitial(() -> new int[1]); + private static final ThreadLocal LAST_DENSITY = ThreadLocal.withInitial(() -> new double[2]); + private static final ThreadLocal> CHUNK_MEMO = ThreadLocal.withInitial(HashMap::new); + private static final AtomicBoolean NULL_CNG_WARNED = new AtomicBoolean(false); + + public static int getLastReject() { + return LAST_REJECT.get()[0]; + } + + public static double getLastClusterValue() { + return LAST_DENSITY.get()[0]; + } + + public static double getLastClusterThreshold() { + return LAST_DENSITY.get()[1]; + } + + public static void clearThreadCaches() { + } + + public static void clearChunkMemo() { + CHUNK_MEMO.get().clear(); + } + + public static FloatingIslandSample sampleMemoized(IrisBiome parent, int wx, int wz, int chunkHeight, long baseSeed, IrisData data, IrisComplex complex, Engine engine) { + long key = (((long) wx) << 32) ^ (wz & 0xFFFFFFFFL); + HashMap memo = CHUNK_MEMO.get(); + if (memo.containsKey(key)) { + return memo.get(key); + } + FloatingIslandSample result = sample(parent, wx, wz, chunkHeight, baseSeed, data, complex, engine); + memo.put(key, result); + return result; + } + + private static FloatingIslandSample reject(int code) { + LAST_REJECT.get()[0] = code; + return null; + } + + private static void warnNullCng(String styleField, IrisBiome parent) { + if (NULL_CNG_WARNED.compareAndSet(false, true)) { + String biomeKey = parent == null ? "" : parent.getLoadKey(); + Iris.warn("Floating child biome on " + biomeKey + " has a null CNG for " + styleField + + " (style factory returned null or AtomicCache swallowed an exception); skipping floating sampling until pack is fixed"); + } + } + + public final IrisFloatingChildBiomes entry; + public final int islandBaseY; + public final int thickness; + public final int topIdx; + public final int solidCount; + public final boolean[] solidMask; + + private FloatingIslandSample(IrisFloatingChildBiomes entry, int islandBaseY, int thickness, int topIdx, int solidCount, boolean[] solidMask) { + this.entry = entry; + this.islandBaseY = islandBaseY; + this.thickness = thickness; + this.topIdx = topIdx; + this.solidCount = solidCount; + this.solidMask = solidMask; + } + + public int topY() { + return islandBaseY + topIdx; + } + + public static long columnSeed(long baseSeed, int wx, int wz) { + return baseSeed ^ ((long) wx * 341873128712L) ^ ((long) wz * 132897987541L); + } + + public static FloatingIslandSample sample(IrisBiome parent, int wx, int wz, int chunkHeight, long baseSeed, IrisData data, IrisComplex complex, Engine engine) { + KList entries = parent.getFloatingChildBiomes(); + if (entries == null || entries.isEmpty()) { + return reject(REJECT_NO_ENTRIES); + } + + IrisFloatingChildBiomes entry; + if (entries.size() == 1) { + entry = entries.getFirst(); + } else { + IrisFloatingChildBiomes reference = entries.getFirst(); + CNG picker = reference.getPickerCng(baseSeed, data); + if (picker == null) { + warnNullCng("pickerStyle", parent); + return reject(REJECT_NO_PICK); + } + double pickerValue = picker.noise(wx, wz); + double clamped = Math.max(0, Math.min(1, pickerValue)); + entry = IRare.pick(entries, clamped); + if (entry == null) { + return reject(REJECT_NO_PICK); + } + } + + CNG footprintCng = entry.getFootprintCng(baseSeed, data); + if (footprintCng == null) { + warnNullCng("footprintStyle", parent); + return reject(REJECT_NO_SEED); + } + double footprintValue = footprintCng.noise(wx, wz); + double signed = (Math.max(0, Math.min(1, footprintValue)) * 2.0) - 1.0; + double threshold = Math.max(0, Math.min(1, entry.getFootprintThreshold())); + double signedCut = (threshold * 2.0) - 1.0; + + double[] diag = LAST_DENSITY.get(); + diag[0] = signed; + diag[1] = signedCut; + + if (signed <= signedCut) { + return reject(REJECT_NO_SEED); + } + + int surfaceY = (int) Math.round(complex.getHeightStream().get(wx & ~63, wz & ~63)); + + CNG altitudeCng = entry.getAltitudeCng(baseSeed, data); + if (altitudeCng == null) { + warnNullCng("altitudeStyle", parent); + return reject(REJECT_NO_SEED); + } + double altNoise = altitudeCng.noise(wx, wz); + double altClamped = Math.max(0, Math.min(1, altNoise)); + int minAlt = Math.max(0, entry.getMinHeightAboveSurface()); + int maxAlt = Math.max(minAlt, entry.getMaxHeightAboveSurface()); + int baseY = surfaceY + minAlt + (int) Math.round(altClamped * (maxAlt - minAlt)); + + IrisBiome target = entry.getRealBiome(parent, data); + int topH = computeTopHeight(entry, target, engine, baseSeed, wx, wz, data); + int topY = baseY + topH; + + double edge = (signed - signedCut) / 0.15; + double edgeClamped = Math.max(0, Math.min(1, edge)); + double edgeFade = edgeClamped * edgeClamped * (3.0 - 2.0 * edgeClamped); + + CNG bottomCng = entry.getBottomCng(baseSeed, data); + if (bottomCng == null) { + warnNullCng("bottomStyle", parent); + return reject(REJECT_NO_SEED); + } + double bottomNoise = bottomCng.noise(wx, wz); + double bottomClamped = Math.max(0, Math.min(1, bottomNoise)); + double bottomShaped = Math.pow(bottomClamped, Math.max(0.1, entry.getBottomExponent())); + int minDepth = Math.max(0, entry.getBottomDepthMin()); + int maxDepth = Math.max(minDepth, entry.getBottomDepthMax()); + int depth = minDepth + (int) Math.round(bottomShaped * (maxDepth - minDepth) * edgeFade); + int botY = baseY - depth; + + Integer minAbsoluteY = entry.getMinAbsoluteY(); + if (minAbsoluteY != null && botY < minAbsoluteY) { + botY = minAbsoluteY; + } + Integer maxAbsoluteY = entry.getMaxAbsoluteY(); + if (maxAbsoluteY != null && topY > maxAbsoluteY) { + topY = maxAbsoluteY; + } + + if (botY < 0) { + botY = 0; + } + if (topY >= chunkHeight) { + topY = chunkHeight - 1; + } + if (topY < botY) { + return reject(REJECT_ABOVE_HEIGHT); + } + + int thickness = topY - botY + 1; + int maxThickness = Math.max(1, entry.getMaxThickness()); + if (thickness > maxThickness) { + botY = topY - maxThickness + 1; + if (botY < 0) { + botY = 0; + } + thickness = topY - botY + 1; + } + if (thickness <= 0) { + return reject(REJECT_NO_THICKNESS); + } + + boolean[] solidMask = new boolean[thickness]; + CNG wallWarp = entry.getWallWarpCng(baseSeed, data); + double warpAmp = Math.max(0, entry.getWallWarpAmplitude()); + CNG carve = entry.getCarveCng(baseSeed, data); + double carveThreshold = entry.getCarveThreshold(); + boolean useWarp = wallWarp != null && warpAmp > 0; + boolean useCarve = carve != null && carveThreshold < 1.0; + int solidCount = 0; + int highestSolidIdx = -1; + + for (int k = 0; k < thickness; k++) { + int wy = botY + k; + double sx = wx; + double sz = wz; + if (useWarp) { + double wnX = wallWarp.noise(wx, wy, wz); + double signedWarpX = (Math.max(0, Math.min(1, wnX)) * 2.0) - 1.0; + sx = wx + signedWarpX * warpAmp; + double wnZ = wallWarp.noise(wx + 1987.3, wy, wz + 2341.1); + double signedWarpZ = (Math.max(0, Math.min(1, wnZ)) * 2.0) - 1.0; + sz = wz + signedWarpZ * warpAmp; + } + double layerFoot = footprintCng.noise(sx, sz); + double layerSigned = (Math.max(0, Math.min(1, layerFoot)) * 2.0) - 1.0; + if (layerSigned <= signedCut) { + continue; + } + if (useCarve) { + double cn = carve.noise(wx, wy, wz); + double cnClamped = Math.max(0, Math.min(1, cn)); + if (cnClamped > carveThreshold) { + continue; + } + } + solidMask[k] = true; + solidCount++; + if (k > highestSolidIdx) { + highestSolidIdx = k; + } + } + + if (solidCount == 0 || highestSolidIdx < 0) { + return reject(REJECT_NO_SOLID); + } + + int topIdx = highestSolidIdx; + + LAST_REJECT.get()[0] = REJECT_NONE; + return new FloatingIslandSample(entry, botY, thickness, topIdx, solidCount, solidMask); + } + + private static int computeTopHeight(IrisFloatingChildBiomes entry, IrisBiome target, Engine engine, long baseSeed, int wx, int wz, IrisData data) { + int maxTopHeight = Math.max(0, entry.getMaxTopHeight()); + if (maxTopHeight == 0) { + return 0; + } + return switch (entry.getTopShapeMode()) { + case FLAT -> maxTopHeight; + case NOISE -> { + CNG topCng = entry.getTopShapeCng(baseSeed, data); + if (topCng == null) { + warnNullCng("topShapeStyle", null); + yield maxTopHeight / 2; + } + double n = topCng.noise(wx, wz); + double clamped = Math.max(0, Math.min(1, n)); + double amp = Math.max(0, Math.min(1, entry.getTopShapeAmp())); + yield (int) Math.round(clamped * amp * maxTopHeight); + } + case BIOME -> { + if (target == null) { + yield maxTopHeight / 2; + } + double h = target.getHeight(engine, wx, wz, baseSeed); + int rounded = (int) Math.round(h); + if (rounded < 0) { + yield 0; + } + yield Math.min(maxTopHeight, rounded); + } + }; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java index 6aa93d6cc..474c8db24 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java @@ -26,6 +26,7 @@ import art.arcane.iris.engine.IrisComplex; import art.arcane.iris.engine.data.cache.AtomicCache; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.object.annotations.*; +import com.google.gson.annotations.SerializedName; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KMap; import art.arcane.volmlib.util.collection.KSet; @@ -106,9 +107,6 @@ public class IrisBiome extends IrisRegistrant implements IRare { private IrisCaveProfile caveProfile = new IrisCaveProfile(); @Desc("Configuration of fluid bodies such as rivers & lakes") private IrisFluidBodies fluidBodies = new IrisFluidBodies(); - @ArrayType(type = IrisExternalDatapackBinding.class, min = 1) - @Desc("Scoped external datapack bindings for this biome") - private KList externalDatapacks = new KList<>(); @MinNumber(1) @MaxNumber(512) @Desc("The rarity of this biome (integer)") @@ -165,6 +163,9 @@ public class IrisBiome extends IrisRegistrant implements IRare { @ArrayType(min = 1, type = IrisObjectPlacement.class) @Desc("Objects define what schematics (iob files) iris will place in this biome") private KList objects = new KList<>(); + @ArrayType(min = 1, type = IrisFloatingChildBiomes.class) + @Desc("Floating child biomes that procedurally generate above this biome's terrain. Each entry references a target biome whose layers, decorators, and objects drive the floating island's visual design, while the config here drives size, shape, altitude, rarity, and water level. Multiple entries are supported and selected by rarity per column.") + private KList floatingChildBiomes = new KList<>(); @Required @ArrayType(min = 1, type = IrisBiomeGeneratorLink.class) @Desc("Generators for this biome. Multiple generators with different interpolation sizes will mix with other biomes how you would expect. This defines your biome height relative to the fluid height. Use negative for oceans.") 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 8c60e8a10..b5bc9cdc2 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 @@ -29,6 +29,7 @@ import art.arcane.iris.core.nms.datapack.IDataFixer.Dimension; import art.arcane.iris.engine.data.cache.AtomicCache; import art.arcane.iris.engine.object.annotations.*; import art.arcane.iris.engine.object.annotations.functions.ComponentFlagFunction; +import com.google.gson.annotations.SerializedName; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KMap; import art.arcane.volmlib.util.collection.KSet; @@ -154,11 +155,6 @@ public class IrisDimension extends IrisRegistrant { private IrisCaveProfile caveProfile = new IrisCaveProfile(); @Desc("Configuration of fluid bodies such as rivers & lakes") private IrisFluidBodies fluidBodies = new IrisFluidBodies(); - @Desc("Enable or disable vanilla structure generation from the extracted vanilla datapack. When disabled, no vanilla structures spawn. When enabled, structures come from the vanilla datapack and can be overridden by external datapacks.") - private boolean vanillaStructures = true; - @ArrayType(type = IrisExternalDatapack.class, min = 1) - @Desc("Pack-scoped external datapack sources for structure import and optional vanilla replacement") - private KList externalDatapacks = new KList<>(); @Desc("forceConvertTo320Height") private Boolean forceConvertTo320Height = false; @Desc("The world environment") @@ -190,6 +186,8 @@ public class IrisDimension extends IrisRegistrant { private boolean upperDimensionCarving = false; @Desc("When true, objects from the mantle (structures, trees, etc.) can be placed in the upper dimension terrain zone. When false, the upper terrain is protected from object placement.") private boolean upperDimensionObjects = false; + @Desc("When true, upper-dimension objects force-place regardless of placement restrictions (slope, underwater, clamp, collisions, carving). Normal dimension objects always place first; upper objects place second and may clip or occlude lower-dimension placements when this is enabled.") + private boolean upperObjectsForcePlace = false; @RegistryListResource(IrisBiome.class) @Desc("Keep this either undefined or empty. Setting any biome name into this will force iris to only generate the specified biome. Great for testing.") private String focus = ""; @@ -241,12 +239,6 @@ public class IrisDimension extends IrisRegistrant { @ArrayType(min = 1, type = IrisShapedGeneratorStyle.class) @Desc("Overlay additional noise on top of the interoplated terrain.") private KList overlayNoise = new KList<>(); - @Desc("If true, the spawner system has infinite energy. This is NOT recommended because it would allow for mobs to keep spawning over and over without a rate limit") - private boolean infiniteEnergy = false; - @MinNumber(0) - @MaxNumber(10000) - @Desc("This is the maximum energy you can have in a dimension") - private double maximumEnergy = 1000; @MinNumber(0.0001) @MaxNumber(512) @Desc("The rock zoom mostly for zooming in on a wispy palette") @@ -468,7 +460,6 @@ public class IrisDimension extends IrisRegistrant { } public void installBiomes(IDataFixer fixer, DataProvider data, KList folders, KSet biomes) { - KMap customBiomeToVanillaBiome = new KMap<>(); String namespace = getLoadKey().toLowerCase(Locale.ROOT); for (IrisBiome irisBiome : getAllBiomes(data)) { @@ -476,13 +467,8 @@ public class IrisDimension extends IrisRegistrant { continue; } - Biome vanillaDerivative = irisBiome.getVanillaDerivative(); - NamespacedKey vanillaDerivativeKey = vanillaDerivative == null ? null : vanillaDerivative.getKey(); - String vanillaBiomeKey = vanillaDerivativeKey == null ? null : vanillaDerivativeKey.toString(); - for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) { String customBiomeId = customBiome.getId(); - String customBiomeKey = namespace + ":" + customBiomeId.toLowerCase(Locale.ROOT); String json = customBiome.generateJson(fixer); synchronized (biomes) { @@ -492,10 +478,6 @@ public class IrisDimension extends IrisRegistrant { } } - if (vanillaBiomeKey != null) { - customBiomeToVanillaBiome.put(customBiomeKey, vanillaBiomeKey); - } - for (File datapacks : folders) { File output = new File(datapacks, "iris/data/" + namespace + "/worldgen/biome/" + customBiomeId + ".json"); @@ -510,126 +492,6 @@ public class IrisDimension extends IrisRegistrant { } } } - - installStructureBiomeTags(folders, customBiomeToVanillaBiome); - } - - private void installStructureBiomeTags(KList folders, KMap customBiomeToVanillaBiome) { - if (customBiomeToVanillaBiome.isEmpty()) { - return; - } - - KMap> vanillaTags = INMS.get().getVanillaStructureBiomeTags(); - if (vanillaTags == null || vanillaTags.isEmpty()) { - return; - } - - KMap> customTagValues = new KMap<>(); - for (Map.Entry customBiomeEntry : customBiomeToVanillaBiome.entrySet()) { - String customBiomeKey = customBiomeEntry.getKey(); - String vanillaBiomeKey = customBiomeEntry.getValue(); - if (vanillaBiomeKey == null) { - continue; - } - - for (Map.Entry> tagEntry : vanillaTags.entrySet()) { - KList values = tagEntry.getValue(); - if (values == null || !values.contains(vanillaBiomeKey)) { - continue; - } - customTagValues.computeIfAbsent(tagEntry.getKey(), key -> new KSet<>()).add(customBiomeKey); - } - } - - if (customTagValues.isEmpty()) { - return; - } - - for (File datapacks : folders) { - for (Map.Entry> tagEntry : customTagValues.entrySet()) { - String tagPath = tagEntry.getKey(); - KSet customValues = tagEntry.getValue(); - if (customValues == null || customValues.isEmpty()) { - continue; - } - - File output = new File(datapacks, "iris/data/minecraft/tags/worldgen/biome/" + tagPath + ".json"); - try { - writeMergedStructureBiomeTag(output, customValues); - } catch (IOException e) { - Iris.reportError(e); - e.printStackTrace(); - } - } - } - } - - private void writeMergedStructureBiomeTag(File output, KSet customValues) throws IOException { - synchronized (IrisDimension.class) { - KSet mergedValues = readExistingStructureBiomeTagValues(output); - mergedValues.addAll(customValues); - - JSONArray values = new JSONArray(); - KList sortedValues = new KList<>(mergedValues).sort(); - for (String value : sortedValues) { - values.put(value); - } - - JSONObject json = new JSONObject(); - json.put("replace", false); - json.put("values", values); - - writeAtomicFile(output, json.toString(4)); - } - } - - private KSet readExistingStructureBiomeTagValues(File output) { - KSet values = new KSet<>(); - if (output == null || !output.exists()) { - return values; - } - - try { - JSONObject json = new JSONObject(IO.readAll(output)); - if (!json.has("values")) { - return values; - } - - JSONArray existingValues = json.getJSONArray("values"); - for (int index = 0; index < existingValues.length(); index++) { - Object rawValue = existingValues.get(index); - if (rawValue == null) { - continue; - } - - String value = String.valueOf(rawValue).trim(); - if (!value.isEmpty()) { - values.add(value); - } - } - } catch (Throwable e) { - Iris.warn("Skipping malformed existing structure biome tag file: " + output.getPath()); - } - - return values; - } - - private void writeAtomicFile(File output, String contents) throws IOException { - File parent = output.getParentFile(); - if (parent != null && !parent.exists()) { - parent.mkdirs(); - } - - File temp = new File(parent, output.getName() + ".tmp-" + System.nanoTime()); - IO.writeAll(temp, contents); - - Path tempPath = temp.toPath(); - Path outputPath = output.toPath(); - try { - Files.move(tempPath, outputPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); - } catch (AtomicMoveNotSupportedException e) { - Files.move(tempPath, outputPath, StandardCopyOption.REPLACE_EXISTING); - } } public Dimension getBaseDimension() { diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisEffect.java b/core/src/main/java/art/arcane/iris/engine/object/IrisEffect.java index 2c697c628..dc9e4b4af 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisEffect.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisEffect.java @@ -24,16 +24,13 @@ import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.object.annotations.*; import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.scheduling.ChronoLatch; -import art.arcane.iris.util.common.reflect.KeyedType; import art.arcane.iris.util.common.scheduling.J; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.Accessors; import org.bukkit.Location; -import org.bukkit.NamespacedKey; import org.bukkit.Particle; -import org.bukkit.Registry; import org.bukkit.Sound; import org.bukkit.entity.Entity; import org.bukkit.entity.Player; @@ -41,8 +38,6 @@ import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; import org.bukkit.util.Vector; -import java.util.Locale; - @Snippet("effect") @Accessors(chain = true) @NoArgsConstructor @@ -52,6 +47,7 @@ import java.util.Locale; public class IrisEffect { private final transient AtomicCache pt = new AtomicCache<>(); private final transient AtomicCache latch = new AtomicCache<>(); + @RegistryListPotionEffect @Desc("The potion effect to apply in this area") private String potionEffect = ""; @Desc("The particle effect to apply in the area") @@ -162,29 +158,24 @@ public class IrisEffect { public PotionEffectType getRealType() { return pt.aquire(() -> { - PotionEffectType t = PotionEffectType.LUCK; - - if (getPotionEffect().isEmpty()) { - return t; + if (getPotionEffect() == null || getPotionEffect().isEmpty()) { + return PotionEffectType.LUCK; } try { - for (PotionEffectType i : Registry.EFFECT) { - NamespacedKey key = KeyedType.getKey(i); - if (key != null && key.getKey().toUpperCase(Locale.ROOT).replaceAll("\\Q \\E", "_").equals(getPotionEffect())) { - t = i; - - return t; - } + PotionEffectType resolved = PotionEffectTypes.resolve(getPotionEffect()); + if (resolved != null) { + return resolved; } } catch (Throwable e) { Iris.reportError(e); - } - Iris.warn("Unknown Potion Effect Type: " + getPotionEffect()); + if (PotionEffectTypes.shouldWarn(getPotionEffect())) { + Iris.warn("Unknown Potion Effect Type: \"" + getPotionEffect() + "\". Valid types: " + PotionEffectTypes.knownTypesList()); + } - return t; + return PotionEffectType.LUCK; }); } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisEntitySpawn.java b/core/src/main/java/art/arcane/iris/engine/object/IrisEntitySpawn.java index ca78a3a8d..a24eea864 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisEntitySpawn.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisEntitySpawn.java @@ -52,8 +52,6 @@ public class IrisEntitySpawn implements IRare { @Required @Desc("The entity") private String entity = ""; - @Desc("The energy multiplier when calculating spawn energy usage") - private double energyMultiplier = 1; @MinNumber(1) @Desc("The 1 in RARITY chance for this entity to spawn") private int rarity = 1; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisExternalDatapack.java b/core/src/main/java/art/arcane/iris/engine/object/IrisExternalDatapack.java deleted file mode 100644 index 079caa5a3..000000000 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisExternalDatapack.java +++ /dev/null @@ -1,29 +0,0 @@ -package art.arcane.iris.engine.object; - -import art.arcane.iris.engine.object.annotations.Desc; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@Data -@NoArgsConstructor -@AllArgsConstructor -@Accessors(chain = true) -@Desc("Defines an external datapack source. When replace is true, minecraft namespace entries override the vanilla datapack.") -public class IrisExternalDatapack { - @Desc("Stable id for this external datapack entry") - private String id = ""; - - @Desc("Datapack source URL. Modrinth version page URLs are supported.") - private String url = ""; - - @Desc("Enable or disable this external datapack entry") - private boolean enabled = true; - - @Desc("If true, Iris hard-fails startup when this external datapack cannot be synced/imported/installed") - private boolean required = false; - - @Desc("If true, this datapack replaces vanilla worldgen entries. The datapack itself determines what it overrides.") - private boolean replace = false; -} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisExternalDatapackBinding.java b/core/src/main/java/art/arcane/iris/engine/object/IrisExternalDatapackBinding.java deleted file mode 100644 index 9dd7b76dd..000000000 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisExternalDatapackBinding.java +++ /dev/null @@ -1,29 +0,0 @@ -package art.arcane.iris.engine.object; - -import art.arcane.iris.engine.object.annotations.Desc; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@Data -@NoArgsConstructor -@AllArgsConstructor -@Accessors(chain = true) -@Desc("Scoped binding to a dimension external datapack id") -public class IrisExternalDatapackBinding { - @Desc("Target external datapack id defined on the dimension") - private String id = ""; - - @Desc("Enable or disable this scoped binding") - private boolean enabled = true; - - @Desc("Override replace behavior for this scoped binding (null keeps dimension default)") - private Boolean replaceOverride = null; - - @Desc("Include child biomes recursively when collecting scoped biome boundaries") - private boolean includeChildren = true; - - @Desc("Override required behavior for this scoped binding (null keeps dimension default)") - private Boolean requiredOverride = null; -} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisFloatingChildBiomes.java b/core/src/main/java/art/arcane/iris/engine/object/IrisFloatingChildBiomes.java new file mode 100644 index 000000000..0ddc0e276 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisFloatingChildBiomes.java @@ -0,0 +1,240 @@ +/* + * 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.core.loader.IrisData; +import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.object.annotations.ArrayType; +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.Snippet; +import art.arcane.iris.util.project.noise.CNG; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Snippet("floating-child-biome") +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode +@Desc("Declares a floating biome layer above this biome's terrain. A 2D footprint noise decides which columns are part of the island (threshold + style control blankets, swirls, scattered blobs). The top profile is driven by the target biome's own terrain generators (so a mountains biome produces real peaks, desert produces real dunes). The bottom tail hanging below is a separately configurable noise (pick VASCULAR for drippy roots, FRACTAL_RM_SIMPLEX for crystalline spikes, PERLIN for smooth rounded bowls).") +@Data +public class IrisFloatingChildBiomes implements IRare { + private final transient AtomicCache resolvedBiome = new AtomicCache<>(); + private final transient AtomicCache footprintCache = new AtomicCache<>(); + private final transient AtomicCache pickerCache = new AtomicCache<>(); + private final transient AtomicCache altitudeCache = new AtomicCache<>(); + private final transient AtomicCache topShapeCache = new AtomicCache<>(); + private final transient AtomicCache bottomCache = new AtomicCache<>(); + private final transient AtomicCache wallWarpCache = new AtomicCache<>(); + private final transient AtomicCache carveCache = new AtomicCache<>(); + private final transient AtomicCache shrinkScaleCache = new AtomicCache<>(); + + public CNG getFootprintCng(long baseSeed, IrisData data) { + return footprintCache.aquire(() -> getFootprintStyle().create(new RNG(baseSeed ^ 0xF007B17DL), data)); + } + + public CNG getPickerCng(long baseSeed, IrisData data) { + return pickerCache.aquire(() -> getPickerStyle().create(new RNG(baseSeed ^ 0x91C4E72DL), data)); + } + + public CNG getAltitudeCng(long baseSeed, IrisData data) { + return altitudeCache.aquire(() -> getAltitudeStyle().create(new RNG(baseSeed ^ 0xA17DEBBL), data)); + } + + public CNG getTopShapeCng(long baseSeed, IrisData data) { + return topShapeCache.aquire(() -> getTopShapeStyle().create(new RNG(baseSeed ^ 0x70970601DEFL), data)); + } + + public CNG getBottomCng(long baseSeed, IrisData data) { + return bottomCache.aquire(() -> getBottomStyle().create(new RNG(baseSeed ^ 0xB0770075CAFEL), data)); + } + + public CNG getWallWarpCng(long baseSeed, IrisData data) { + IrisGeneratorStyle style = getWallWarpStyle(); + if (style == null) { + return null; + } + return wallWarpCache.aquire(() -> style.create(new RNG(baseSeed ^ 0xA117BA17E0FL), data)); + } + + public CNG getCarveCng(long baseSeed, IrisData data) { + IrisGeneratorStyle style = getCarveStyle(); + if (style == null) { + return null; + } + return carveCache.aquire(() -> style.create(new RNG(baseSeed ^ 0xCA5EC1EE5EL), data)); + } + + @RegistryListResource(IrisBiome.class) + @Desc("The target biome whose visual design (layers, palette, decorators, surface objects, derivative, and — when topShapeMode=BIOME — generator profile) drives the floating island. Leave empty to reuse the parent biome (self).") + private String biome = ""; + + @MinNumber(1) + @MaxNumber(512) + @Desc("Selection rarity when multiple floating child entries are defined on one parent biome. Lower is more common.") + private int rarity = 1; + + @Desc("2D noise that decides which columns are part of this island. Set feature size via the style's own zoom field (e.g. {\"style\":\"CELLULAR\",\"zoom\":0.3} for ~30-block shards, {\"style\":\"SIMPLEX\",\"zoom\":1.0} for ~100-block blobs). Pick SIMPLEX for smooth blobs, CELLULAR for angular shards, VASCULAR for vein/branch strips, FRACTAL_FBM_SIMPLEX for large irregular blanket regions. Fracture (domain warp) this to get swirly silhouettes.") + private IrisGeneratorStyle footprintStyle = NoiseStyle.SIMPLEX.style(); + + @MinNumber(0) + @MaxNumber(1) + @Desc("Coverage threshold (0..1). Roughly the fraction of the world that is NOT island: 0.0 = every column becomes island, 0.5 ≈ 50% of columns, 0.8 ≈ sparse scattered ~20% coverage, 1.0 = no islands at all. Values near 0.5 feel most natural.") + private double footprintThreshold = 0.5; + + @Desc("Picker noise used when multiple floating child entries exist. Samples once per column to deterministically choose which entry's footprint is tested there. Use a large style zoom (e.g. zoom: 4 for ~400-block regions) so each entry owns broad coherent areas.") + private IrisGeneratorStyle pickerStyle = NoiseStyle.SIMPLEX.style(); + + @Desc("Altitude noise — varies the base platform Y across one island so it isn't a flat plane. Use a large style zoom (e.g. zoom: 2 for ~200-block altitude patches) so an island sits at roughly one altitude.") + private IrisGeneratorStyle altitudeStyle = NoiseStyle.SIMPLEX.style(); + + @MinNumber(0) + @MaxNumber(2032) + @Desc("Minimum blocks above the parent biome surface where the island base can sit.") + private int minHeightAboveSurface = 60; + + @MinNumber(0) + @MaxNumber(2032) + @Desc("Maximum blocks above the parent biome surface where the island base can sit.") + private int maxHeightAboveSurface = 110; + + @Desc("Optional absolute minimum world Y for the island base. When set, baseY is clamped upward so the tail bottom stays above this value.") + private Integer minAbsoluteY = null; + + @Desc("Optional absolute maximum world Y for the island top. When set, the top is clamped downward.") + private Integer maxAbsoluteY = null; + + @Desc("How the top profile of the island is shaped. BIOME = evaluate target biome's own generators (mountains biome -> real mountains). NOISE = use topShapeStyle as a user-controlled heightmap. FLAT = constant maxTopHeight slab.") + private TopShapeMode topShapeMode = TopShapeMode.BIOME; + + @MinNumber(0) + @MaxNumber(512) + @Desc("Maximum top profile height in blocks above the island base. Caps how tall the biome terrain can grow on top.") + private int maxTopHeight = 40; + + @Desc("Used only when topShapeMode=NOISE. 2D noise driving the top heightmap. Set feature scale via the style's zoom field (small zoom = rugged peaks, large zoom = broad rolling shapes).") + private IrisGeneratorStyle topShapeStyle = NoiseStyle.SIMPLEX.style(); + + @MinNumber(0) + @MaxNumber(1) + @Desc("Amplitude multiplier applied to the NOISE top profile. 0 = no top (flat at base), 1 = full maxTopHeight range.") + private double topShapeAmp = 1.0; + + @Desc("2D noise driving the bottom tail hanging below the island base. VASCULAR = drippy organic roots. FRACTAL_RM_SIMPLEX = crystalline spikes. CELLULAR = jagged chunks. PERLIN = smooth rounded bowl. SIMPLEX = gentle lobes. Set feature scale via the style's zoom field.") + private IrisGeneratorStyle bottomStyle = NoiseStyle.SIMPLEX.style(); + + @MinNumber(0) + @MaxNumber(512) + @Desc("Minimum blocks below the base where the tail extends.") + private int bottomDepthMin = 4; + + @MinNumber(0) + @MaxNumber(512) + @Desc("Maximum blocks below the base where the tail extends.") + private int bottomDepthMax = 20; + + @MinNumber(0.1) + @MaxNumber(8) + @Desc("Power curve applied to the bottom noise before mapping to depth. >1 = most columns shallow with occasional deeper spikes (sparse roots). <1 = most columns deep with occasional shallow spots (dense curtains). 1.0 = linear.") + private double bottomExponent = 1.0; + + @MinNumber(1) + @MaxNumber(512) + @Desc("Hard cap on the total Y-extent (top minus bottom) of a single island column. Safety limit.") + private int maxThickness = 96; + + @Desc("Optional 3D noise that shifts the footprint's XZ sample position per Y layer — naturalizes the walls so they stop looking like a straight extrusion of the 2D footprint. Leave null to disable and keep straight vertical walls. Good defaults: {\"style\":\"SIMPLEX\",\"zoom\":0.25} for gentle undulation, {\"style\":\"FRACTAL_FBM_SIMPLEX\",\"zoom\":0.4} for craggier walls.") + private IrisGeneratorStyle wallWarpStyle = null; + + @MinNumber(0) + @MaxNumber(64) + @Desc("Amplitude in blocks of the per-layer XZ shift applied when wallWarpStyle is set. 0 = no warp (straight walls). 4..8 = gentle naturalization. 16+ = heavily meandering walls. Ignored when wallWarpStyle is null.") + private double wallWarpAmplitude = 6.0; + + @Desc("Optional 3D noise that swiss-cheeses the island interior by marking individual blocks as air when the noise exceeds carveThreshold. Leave null to keep the island solid. Good defaults: {\"style\":\"CELLULAR\",\"zoom\":0.3} for bubble pockets, {\"style\":\"VASCULAR\",\"zoom\":0.25} for wormy tunnels.") + private IrisGeneratorStyle carveStyle = null; + + @MinNumber(0) + @MaxNumber(1) + @Desc("Threshold (0..1) above which carveStyle noise carves air pockets. 1.0 = no carving. 0.75 = sparse pockets. 0.55 = heavy swiss-cheese. 0.4 = shredded lattice. Ignored when carveStyle is null.") + private double carveThreshold = 1.0; + + @Desc("Optional water surface height above the island base, in blocks. null = no internal water. Positive = water fills any dip in the top profile up to baseY + localFluidHeight (forms lakes/ponds in concavities of the biome-top heightmap).") + private Integer localFluidHeight = null; + + @Desc("Block used for the internal water pool when localFluidHeight is positive.") + private String fluidBlock = "minecraft:water"; + + @Desc("When true, the target biome's decorators apply to the island's top surface.") + private boolean inheritDecorators = true; + + @Desc("When true, the target biome's surface objects are placed on the island's top surface instead of the parent terrain.") + private boolean inheritObjects = true; + + @MinNumber(0.01) + @MaxNumber(1) + @Desc("Uniform shrink factor applied to every object placed on this floating island (inherited, extra, and free-floating). 1.0 = native size, 0.5 = half size, 0.25 = quarter. Useful for making small floating biomes feel believable.") + private double objectShrinkFactor = 1.0; + + @ArrayType(min = 1, type = IrisObjectPlacement.class) + @Desc("Additional object placements anchored to the island top.") + private KList extraObjects = new KList<>(); + + @ArrayType(min = 1, type = IrisObjectPlacement.class) + @Desc("Additional object placements that float freely in air, independent of the island. Forced to ObjectPlaceMode.FLOATING.") + private KList floatingObjects = new KList<>(); + + @Desc("Visualization color for this floating child in Iris Studio.") + private String color = null; + + public boolean hasObjectShrink() { + return objectShrinkFactor > 0 && objectShrinkFactor < 1.0; + } + + public IrisObjectScale getShrinkScale() { + return shrinkScaleCache.aquire(() -> { + IrisObjectScale s = new IrisObjectScale(); + s.setSize(Math.max(0.01, Math.min(1.0, objectShrinkFactor))); + return s; + }); + } + + public IrisBiome getRealBiome(IrisBiome parent, IrisData data) { + return resolvedBiome.aquire(() -> { + if (biome == null || biome.isBlank() || biome.equals(parent.getLoadKey())) { + return parent; + } + + IrisBiome loaded = data.getBiomeLoader().load(biome); + if (loaded == null) { + return parent; + } + + return loaded; + }); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObject.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObject.java index a3e828752..59afb8da4 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisObject.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObject.java @@ -80,6 +80,8 @@ public class IrisObject extends IrisRegistrant { protected static final BlockData VAIR = B.get("VOID_AIR"); protected static final BlockData VAIR_DEBUG = B.get("COBWEB"); protected static final BlockData[] SNOW_LAYERS = new BlockData[]{B.get("minecraft:snow[layers=1]"), B.get("minecraft:snow[layers=2]"), B.get("minecraft:snow[layers=3]"), B.get("minecraft:snow[layers=4]"), B.get("minecraft:snow[layers=5]"), B.get("minecraft:snow[layers=6]"), B.get("minecraft:snow[layers=7]"), B.get("minecraft:snow[layers=8]")}; + private static final long IMPLAUSIBLE_BEDROCK_WARN_THROTTLE_MS = 5000L; + private static final java.util.concurrent.ConcurrentHashMap IMPLAUSIBLE_BEDROCK_WARNS = new java.util.concurrent.ConcurrentHashMap<>(); protected transient final Lock readLock; protected transient final Lock writeLock; @Getter @@ -297,7 +299,7 @@ public class IrisObject extends IrisRegistrant { public synchronized IrisObject copy() { IrisObject o = new IrisObject(w, h, d); - o.setLoadKey(o.getLoadKey()); + o.setLoadKey(getLoadKey()); o.setLoader(getLoader()); o.setLoadFile(getLoadFile()); o.setCenter(getCenter().clone()); @@ -679,6 +681,7 @@ public class IrisObject extends IrisRegistrant { } boolean warped = !config.getWarp().isFlat(); + boolean rawStructurePiece = config.getMode() == ObjectPlaceMode.STRUCTURE_PIECE; boolean stilting = (config.getMode().equals(ObjectPlaceMode.STILT) || config.getMode().equals(ObjectPlaceMode.FAST_STILT) || config.getMode() == ObjectPlaceMode.MIN_STILT || config.getMode() == ObjectPlaceMode.FAST_MIN_STILT || config.getMode() == ObjectPlaceMode.CENTER_STILT || config.getMode() == ObjectPlaceMode.ERODE_STILT); @@ -819,17 +822,19 @@ public class IrisObject extends IrisRegistrant { bail = true; } } + } else if (config.getMode().equals(ObjectPlaceMode.FLOATING)) { + y = rty; } } else { y = yv; - if (!config.isForcePlace()) { + if (!config.isForcePlace() && !rawStructurePiece) { if (shouldBailForCarvingAnchor(placer, config, x, y, z)) { bail = true; } } } - if (yv >= 0 && config.isBottom()) { + if (yv >= 0 && config.isBottom() && !rawStructurePiece) { y += Math.floorDiv(h, 2); CarvingMode carvingMode = config.getCarvingSupport(); if (!config.isForcePlace() && !carvingMode.equals(CarvingMode.CARVING_ONLY)) { @@ -839,29 +844,42 @@ public class IrisObject extends IrisRegistrant { } } + if (yv < 0 + && !config.isForcePlace() + && !config.isFromBottom() + && config.getMode() != ObjectPlaceMode.FLOATING + && !rawStructurePiece + && config.getCarvingSupport().supportsSurface() + && placer.getEngine() != null + && placer.getEngine().getDimension().isBedrock() + && y <= 1) { + warnImplausibleBedrockPlacement(placer, config, x, y, z); + return -1; + } + if (bail && !config.isForcePlace()) { return -1; } - if (yv < 0) { + if (yv < 0 && !config.getMode().equals(ObjectPlaceMode.FLOATING) && !rawStructurePiece) { if (!config.isForcePlace() && !config.isUnderwater() && !config.isOnwater() && placer.isUnderwater(x, z)) { return -1; } } - if (!config.isForcePlace() && c != null && Math.max(0, h + yrand + ty) + 1 >= c.getHeight()) { + if (!config.isForcePlace() && !rawStructurePiece && c != null && Math.max(0, h + yrand + ty) + 1 >= c.getHeight()) { return -1; } - if (!config.isForcePlace() && config.isUnderwater() && y + rty + ty >= placer.getFluidHeight()) { + if (!config.isForcePlace() && !rawStructurePiece && config.isUnderwater() && y + rty + ty >= placer.getFluidHeight()) { return -1; } - if (!config.isForcePlace() && !config.getClamp().canPlace(y + rty + ty, y - rty + ty)) { + if (!config.isForcePlace() && !rawStructurePiece && !config.getClamp().canPlace(y + rty + ty, y - rty + ty)) { return -1; } - if (!config.isForcePlace() && (!config.getAllowedCollisions().isEmpty() || !config.getForbiddenCollisions().isEmpty())) { + if (!config.isForcePlace() && !rawStructurePiece && (!config.getAllowedCollisions().isEmpty() || !config.getForbiddenCollisions().isEmpty())) { Engine engine = rdata.getEngine(); BlockVector offset = new BlockVector(config.getTranslate().getX(), config.getTranslate().getY(), config.getTranslate().getZ()); for (int i = x - Math.floorDiv(w, 2) + (int) offset.getX(); i <= x + Math.floorDiv(w, 2) - (w % 2 == 0 ? 1 : 0) + (int) offset.getX(); i++) { @@ -1019,7 +1037,7 @@ public class IrisObject extends IrisRegistrant { } } - if (config.isMeld() && !placer.isSolid(xx, yy, zz)) { + if (config.isMeld() && !rawStructurePiece && !placer.isSolid(xx, yy, zz)) { continue; } @@ -1241,6 +1259,24 @@ public class IrisObject extends IrisRegistrant { return y; } + private void warnImplausibleBedrockPlacement(IObjectPlacer placer, IrisObjectPlacement config, int x, int y, int z) { + String key = getLoadKey(); + String fingerprint = (key == null ? "" : key) + "|" + config.getMode(); + long now = System.currentTimeMillis(); + Long last = IMPLAUSIBLE_BEDROCK_WARNS.get(fingerprint); + if (last != null && now - last < IMPLAUSIBLE_BEDROCK_WARN_THROTTLE_MS) { + return; + } + IMPLAUSIBLE_BEDROCK_WARNS.put(fingerprint, now); + Iris.warn("Implausible object placement rejected: " + + (key == null ? "" : key) + + " resolved anchorY=" + y + " at (" + x + "," + z + ") mode=" + config.getMode() + + " carving=" + config.getCarvingSupport() + + ". Surface-anchored placement should never land on the bedrock row. " + + "Height sampling returned a bogus value — not configured for floor placement " + + "(forcePlace=false, fromBottom=false, mode!=FLOATING). Skipping to protect bedrock."); + } + private boolean shouldBailForCarvingAnchor(IObjectPlacer placer, IrisObjectPlacement placement, int x, int y, int z) { boolean carved = isCarvedAnchor(placer, x, y, z); CarvingMode carvingMode = placement.getCarvingSupport(); @@ -1339,6 +1375,9 @@ public class IrisObject extends IrisRegistrant { } public IrisObject scaled(double scale, IrisObjectPlacementScaleInterpolator interpolation) { + if (interpolation == null) { + interpolation = IrisObjectPlacementScaleInterpolator.NONE; + } Vector sm1 = new Vector(scale - 1, scale - 1, scale - 1); scale = Math.max(0.001, Math.min(50, scale)); if (scale < 1) { @@ -1361,6 +1400,9 @@ public class IrisObject extends IrisRegistrant { } IrisObject oo = new IrisObject((int) Math.ceil((w * scale) + (scale * 2)), (int) Math.ceil((h * scale) + (scale * 2)), (int) Math.ceil((d * scale) + (scale * 2))); + oo.setLoadKey(getLoadKey()); + oo.setLoader(getLoader()); + oo.setLoadFile(getLoadFile()); readLock.lock(); for (var entry : blocks) { diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectScale.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectScale.java index f3ab0bedc..dfdaf09ef 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectScale.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectScale.java @@ -89,6 +89,9 @@ public class IrisObjectScale { } public IrisObject get(RNG rng, IrisObject origin) { + if (origin == null) { + return null; + } if (!shouldScale()) { return origin; } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisPotionEffect.java b/core/src/main/java/art/arcane/iris/engine/object/IrisPotionEffect.java index ab736f06d..0a106e663 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisPotionEffect.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisPotionEffect.java @@ -21,19 +21,14 @@ package art.arcane.iris.engine.object; import art.arcane.iris.Iris; import art.arcane.iris.engine.data.cache.AtomicCache; import art.arcane.iris.engine.object.annotations.*; -import art.arcane.iris.util.common.reflect.KeyedType; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.Accessors; -import org.bukkit.NamespacedKey; import org.bukkit.entity.LivingEntity; -import org.bukkit.Registry; import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; -import java.util.Locale; - @Accessors(chain = true) @NoArgsConstructor @AllArgsConstructor @@ -44,6 +39,7 @@ import java.util.Locale; public class IrisPotionEffect { private final transient AtomicCache pt = new AtomicCache<>(); @Required + @RegistryListPotionEffect @Desc("The potion effect to apply in this area") private String potionEffect = ""; @Required @@ -63,29 +59,24 @@ public class IrisPotionEffect { public PotionEffectType getRealType() { return pt.aquire(() -> { - PotionEffectType t = PotionEffectType.LUCK; - - if (getPotionEffect().isEmpty()) { - return t; + if (getPotionEffect() == null || getPotionEffect().isEmpty()) { + return PotionEffectType.LUCK; } try { - for (PotionEffectType i : Registry.EFFECT) { - NamespacedKey key = KeyedType.getKey(i); - if (key != null && key.getKey().toUpperCase(Locale.ROOT).replaceAll("\\Q \\E", "_").equals(getPotionEffect())) { - t = i; - - return t; - } + PotionEffectType resolved = PotionEffectTypes.resolve(getPotionEffect()); + if (resolved != null) { + return resolved; } } catch (Throwable e) { Iris.reportError(e); - } - Iris.warn("Unknown Potion Effect Type: " + getPotionEffect()); + if (PotionEffectTypes.shouldWarn(getPotionEffect())) { + Iris.warn("Unknown Potion Effect Type: \"" + getPotionEffect() + "\". Valid types: " + PotionEffectTypes.knownTypesList()); + } - return t; + return PotionEffectType.LUCK; }); } 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 9a95e4a4d..faa8dbe17 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 @@ -24,6 +24,7 @@ import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.loader.IrisRegistrant; import art.arcane.iris.engine.data.cache.AtomicCache; import art.arcane.iris.engine.object.annotations.*; +import com.google.gson.annotations.SerializedName; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KMap; import art.arcane.volmlib.util.collection.KSet; @@ -118,9 +119,6 @@ public class IrisRegion extends IrisRegistrant implements IRare { private IrisCaveProfile caveProfile = new IrisCaveProfile(); @Desc("Configuration of fluid bodies such as rivers & lakes") private IrisFluidBodies fluidBodies = new IrisFluidBodies(); - @ArrayType(type = IrisExternalDatapackBinding.class, min = 1) - @Desc("Scoped external datapack bindings for this region") - private KList externalDatapacks = new KList<>(); @RegistryListResource(IrisBiome.class) @Required @ArrayType(min = 1, type = String.class) diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisSpawner.java b/core/src/main/java/art/arcane/iris/engine/object/IrisSpawner.java index 8cce9948c..ed046333a 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisSpawner.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisSpawner.java @@ -50,9 +50,6 @@ public class IrisSpawner extends IrisRegistrant { @Desc("The entity spawns to add initially. EXECUTES PER CHUNK!") private KList initialSpawns = new KList<>(); - @Desc("The energy multiplier when calculating spawn energy usage") - private double energyMultiplier = 1; - @Desc("This spawner will not spawn in a given chunk if that chunk has more than the defined amount of living entities.") private int maxEntitiesPerChunk = 1; diff --git a/core/src/main/java/art/arcane/iris/engine/object/ObjectPlaceMode.java b/core/src/main/java/art/arcane/iris/engine/object/ObjectPlaceMode.java index fe3a661ed..5496bb4fa 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/ObjectPlaceMode.java +++ b/core/src/main/java/art/arcane/iris/engine/object/ObjectPlaceMode.java @@ -68,5 +68,13 @@ public enum ObjectPlaceMode { @Desc("Samples the height of the terrain at every x,z position of your object and pushes it down to the surface. It's pretty much like a melt function over the terrain.") - PAINT + PAINT, + + @Desc("Places the object in pure air at an absolute Y driven entirely by translate.y (plus optional translate.yRandom). Terrain height, underwater, and carving anchor checks are skipped. Use this for floating islands, sky structures, clouds, or blimps where the object must not be translated to the ground.") + + FLOATING, + + @Desc("Raw stamp at the caller-supplied (x, y, z). No terrain sampling, no stilting, no Y recomputation, no underwater or carving anchor guards. Used internally to route native Minecraft structure pieces (villages etc.) through the Iris object placer.") + + STRUCTURE_PIECE } diff --git a/core/src/main/java/art/arcane/iris/engine/object/PotionEffectTypes.java b/core/src/main/java/art/arcane/iris/engine/object/PotionEffectTypes.java new file mode 100644 index 000000000..ef5fafe57 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/PotionEffectTypes.java @@ -0,0 +1,98 @@ +/* + * 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.util.common.reflect.KeyedType; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; +import org.bukkit.potion.PotionEffectType; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; + +public final class PotionEffectTypes { + private static final Map LEGACY_ALIASES; + private static final Set MISSING_WARNED = ConcurrentHashMap.newKeySet(); + + static { + Map m = new HashMap<>(); + m.put("SLOW", "SLOWNESS"); + m.put("FAST_DIGGING", "HASTE"); + m.put("SLOW_DIGGING", "MINING_FATIGUE"); + m.put("INCREASE_DAMAGE", "STRENGTH"); + m.put("HEAL", "INSTANT_HEALTH"); + m.put("HARM", "INSTANT_DAMAGE"); + m.put("JUMP", "JUMP_BOOST"); + m.put("CONFUSION", "NAUSEA"); + m.put("DAMAGE_RESISTANCE", "RESISTANCE"); + LEGACY_ALIASES = Collections.unmodifiableMap(m); + } + + private PotionEffectTypes() { + } + + public static String normalize(String input) { + if (input == null) { + return ""; + } + String upper = input.trim().toUpperCase(Locale.ROOT).replace(' ', '_'); + if (upper.contains(":")) { + upper = upper.substring(upper.indexOf(':') + 1); + } + return LEGACY_ALIASES.getOrDefault(upper, upper); + } + + public static PotionEffectType resolve(String rawName) { + if (rawName == null || rawName.trim().isEmpty()) { + return null; + } + String wanted = normalize(rawName); + for (PotionEffectType i : Registry.EFFECT) { + NamespacedKey key = KeyedType.getKey(i); + if (key == null) { + continue; + } + String candidate = key.getKey().toUpperCase(Locale.ROOT).replace(' ', '_'); + if (candidate.equals(wanted)) { + return i; + } + } + return null; + } + + public static String knownTypesList() { + TreeSet names = new TreeSet<>(); + for (PotionEffectType i : Registry.EFFECT) { + NamespacedKey key = KeyedType.getKey(i); + if (key != null) { + names.add(key.getKey().toUpperCase(Locale.ROOT).replace(' ', '_')); + } + } + return String.join(", ", names); + } + + public static boolean shouldWarn(String rawName) { + return MISSING_WARNED.add(normalize(rawName)); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/TopShapeMode.java b/core/src/main/java/art/arcane/iris/engine/object/TopShapeMode.java new file mode 100644 index 000000000..8dcf8028a --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/TopShapeMode.java @@ -0,0 +1,33 @@ +/* + * 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 the top profile of a floating-child island is shaped.") +public enum TopShapeMode { + @Desc("Evaluate the target biome's own terrain generators to build the island top. Mountains biome produces real peaks, desert produces dunes, plains is flat. Recommended default.") + BIOME, + + @Desc("Drive the top profile from topShapeStyle noise, independent of the target biome's generators. Amplitude controlled by topShapeAmp.") + NOISE, + + @Desc("Flat slab on top, topHeight blocks above the base. Ignores noise and biome generators.") + FLAT +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListPotionEffect.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListPotionEffect.java new file mode 100644 index 000000000..f2c1b2aa4 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/annotations/RegistryListPotionEffect.java @@ -0,0 +1,12 @@ +package art.arcane.iris.engine.object.annotations; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention(RUNTIME) +@Target({PARAMETER, TYPE, FIELD}) +public @interface RegistryListPotionEffect { +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/annotations/functions/StructureKeyFunction.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/functions/StructureKeyFunction.java deleted file mode 100644 index 71f96baa0..000000000 --- a/core/src/main/java/art/arcane/iris/engine/object/annotations/functions/StructureKeyFunction.java +++ /dev/null @@ -1,23 +0,0 @@ -package art.arcane.iris.engine.object.annotations.functions; - -import art.arcane.iris.core.loader.IrisData; -import art.arcane.iris.core.nms.INMS; -import art.arcane.iris.engine.framework.ListFunction; -import art.arcane.volmlib.util.collection.KList; - -public class StructureKeyFunction implements ListFunction> { - @Override - public String key() { - return "structure-key"; - } - - @Override - public String fancyName() { - return "Structure Key"; - } - - @Override - public KList apply(IrisData irisData) { - return INMS.get().getStructureKeys().removeWhere(t -> t.startsWith("#")); - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/object/annotations/functions/StructureKeyOrTagFunction.java b/core/src/main/java/art/arcane/iris/engine/object/annotations/functions/StructureKeyOrTagFunction.java deleted file mode 100644 index 1b3f60ac9..000000000 --- a/core/src/main/java/art/arcane/iris/engine/object/annotations/functions/StructureKeyOrTagFunction.java +++ /dev/null @@ -1,23 +0,0 @@ -package art.arcane.iris.engine.object.annotations.functions; - -import art.arcane.iris.core.loader.IrisData; -import art.arcane.iris.core.nms.INMS; -import art.arcane.iris.engine.framework.ListFunction; -import art.arcane.volmlib.util.collection.KList; - -public class StructureKeyOrTagFunction implements ListFunction> { - @Override - public String key() { - return "structure-key-or-tag"; - } - - @Override - public String fancyName() { - return "Structure Key or Tag"; - } - - @Override - public KList apply(IrisData irisData) { - return INMS.get().getStructureKeys(); - } -} 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 3d445e975..b71794a4f 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 @@ -376,8 +376,6 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun effectiveListener.onOverlay(x, z, overlayMetrics.appliedBlocks(), overlayMetrics.objectKeys(), System.currentTimeMillis()); } - setChunkReplacementPhase(phaseRef, effectiveListener, "structures", x, z); - INMS.get().placeStructures(c); setChunkReplacementPhase(phaseRef, effectiveListener, "chunk-load-callback", x, z); engine.getWorldManager().onChunkLoad(c, true); world.refreshChunk(c.getX(), c.getZ()); @@ -458,10 +456,6 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun effectiveListener.onOverlay(x, z, overlayMetrics.appliedBlocks(), overlayMetrics.objectKeys(), System.currentTimeMillis()); }, syncExecutor).get(); } - CompletableFuture.runAsync(() -> { - setChunkReplacementPhase(phaseRef, effectiveListener, "structures", x, z); - INMS.get().placeStructures(c); - }, syncExecutor).get(); CompletableFuture.runAsync(() -> { setChunkReplacementPhase(phaseRef, effectiveListener, "chunk-load-callback", x, z); engine.getWorldManager().onChunkLoad(c, true); @@ -811,10 +805,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun @Override public boolean shouldGenerateStructures() { - if (isStudio() && art.arcane.iris.core.runtime.ObjectStudioActivation.isActive(getEngine().getDimension().getLoadKey())) { - return false; - } - return IrisSettings.get().getGeneral().isAutoGenerateIntrinsicStructures(); + return false; } @Override diff --git a/core/src/main/java/art/arcane/iris/util/common/director/DirectorHelp.java b/core/src/main/java/art/arcane/iris/util/common/director/DirectorHelp.java new file mode 100644 index 000000000..090be36b1 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/director/DirectorHelp.java @@ -0,0 +1,118 @@ +/* + * 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.util.common.director; + +import art.arcane.iris.util.common.format.C; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.volmlib.util.director.annotations.Director; +import art.arcane.volmlib.util.director.annotations.Param; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +public final class DirectorHelp { + private DirectorHelp() { + } + + public static void print(VolmitSender sender, Class commandRoot) { + Director rootAnnotation = commandRoot.getAnnotation(Director.class); + String rootName = rootAnnotation == null || rootAnnotation.name().isEmpty() + ? lowercaseDefault(commandRoot.getSimpleName()) + : rootAnnotation.name(); + String rootDesc = rootAnnotation == null ? "" : rootAnnotation.description(); + + sender.sendMessage(C.IRIS + "/" + rootName + C.GRAY + " — " + rootDesc); + + List methods = new ArrayList<>(); + for (Method m : commandRoot.getDeclaredMethods()) { + if (m.isAnnotationPresent(Director.class)) { + methods.add(m); + } + } + methods.sort(Comparator.comparing(m -> methodName(m))); + + for (Method m : methods) { + Director d = m.getAnnotation(Director.class); + String name = methodName(m); + String aliases = formatAliases(d.aliases()); + sender.sendMessage(C.WHITE + " " + name + aliases + C.GRAY + " — " + d.description()); + for (Parameter p : m.getParameters()) { + Param pa = p.getAnnotation(Param.class); + if (pa == null) continue; + String key = pa.name().isEmpty() ? p.getName() : pa.name(); + String type = simpleTypeName(p.getType()); + String def = pa.defaultValue().isEmpty() ? "" : C.GRAY + " (default: " + pa.defaultValue() + ")"; + String pAliases = formatAliases(pa.aliases()); + sender.sendMessage(C.GRAY + " " + C.AQUA + key + "=" + C.YELLOW + "<" + type + ">" + + C.GRAY + pAliases + C.GRAY + " — " + pa.description() + def); + } + } + + List subGroups = new ArrayList<>(); + for (Field f : commandRoot.getDeclaredFields()) { + if (f.getType().isAnnotationPresent(Director.class)) { + subGroups.add(f); + } + } + if (!subGroups.isEmpty()) { + sender.sendMessage(C.IRIS + " Subcommand groups:"); + for (Field f : subGroups) { + Director sub = f.getType().getAnnotation(Director.class); + String subName = sub.name().isEmpty() ? lowercaseDefault(f.getType().getSimpleName()) : sub.name(); + sender.sendMessage(C.WHITE + " /" + rootName + " " + subName + + C.GRAY + " — " + sub.description() + C.GRAY + " (try: /" + rootName + " " + subName + " help)"); + } + } + } + + private static String methodName(Method m) { + Director d = m.getAnnotation(Director.class); + if (d != null && !d.name().isEmpty()) return d.name(); + return m.getName(); + } + + private static String formatAliases(String[] aliases) { + if (aliases == null || aliases.length == 0) return ""; + List valid = new ArrayList<>(); + for (String a : aliases) { + if (a != null && !a.isEmpty()) valid.add(a); + } + if (valid.isEmpty()) return ""; + return C.GRAY + " [" + String.join(", ", valid) + "]"; + } + + private static String simpleTypeName(Class type) { + if (type.isEnum()) { + Object[] constants = type.getEnumConstants(); + List names = new ArrayList<>(); + for (Object c : constants) names.add(((Enum) c).name()); + return String.join("|", names); + } + return type.getSimpleName(); + } + + private static String lowercaseDefault(String simpleName) { + String s = simpleName.startsWith("Command") ? simpleName.substring("Command".length()) : simpleName; + return s.isEmpty() ? simpleName.toLowerCase() : s.toLowerCase(); + } +} diff --git a/core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/ExternalDatapackLocateHandler.java b/core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/ExternalDatapackLocateHandler.java deleted file mode 100644 index 093132d3a..000000000 --- a/core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/ExternalDatapackLocateHandler.java +++ /dev/null @@ -1,112 +0,0 @@ -package art.arcane.iris.util.common.director.specialhandlers; - -import art.arcane.iris.core.ExternalDataPackPipeline; -import art.arcane.volmlib.util.collection.KList; -import art.arcane.iris.util.common.director.DirectorParameterHandler; -import art.arcane.volmlib.util.director.exceptions.DirectorParsingException; - -import java.util.LinkedHashSet; -import java.util.Locale; -import java.util.Map; -import java.util.Set; - -public class ExternalDatapackLocateHandler implements DirectorParameterHandler { - @Override - public KList getPossibilities() { - LinkedHashSet tokens = new LinkedHashSet<>(); - Map> locateById = ExternalDataPackPipeline.snapshotLocateStructuresById(); - for (Map.Entry> entry : locateById.entrySet()) { - if (entry == null) { - continue; - } - - String id = entry.getKey(); - if (id != null && !id.isBlank()) { - tokens.add(id); - } - - Set structures = entry.getValue(); - if (structures == null || structures.isEmpty()) { - continue; - } - - for (String structure : structures) { - if (structure != null && !structure.isBlank()) { - tokens.add(structure); - } - } - } - - KList possibilities = new KList<>(); - possibilities.add(tokens); - return possibilities; - } - - @Override - public KList getPossibilities(String input) { - String rawInput = input == null ? "" : input; - String[] split = rawInput.split(",", -1); - String partial = split.length == 0 ? "" : split[split.length - 1].trim().toLowerCase(Locale.ROOT); - StringBuilder prefixBuilder = new StringBuilder(); - if (split.length > 1) { - for (int index = 0; index < split.length - 1; index++) { - String value = split[index] == null ? "" : split[index].trim(); - if (value.isBlank()) { - continue; - } - if (!prefixBuilder.isEmpty()) { - prefixBuilder.append(','); - } - prefixBuilder.append(value); - } - } - - String prefix = prefixBuilder.toString(); - LinkedHashSet completions = new LinkedHashSet<>(); - for (String possibility : getPossibilities()) { - if (possibility == null || possibility.isBlank()) { - continue; - } - String normalized = possibility.toLowerCase(Locale.ROOT); - if (!partial.isBlank() && !normalized.startsWith(partial)) { - continue; - } - - if (prefix.isBlank()) { - completions.add(possibility); - } else { - completions.add(prefix + "," + possibility); - } - } - - KList results = new KList<>(); - results.add(completions); - return results; - } - - @Override - public String toString(String value) { - return value == null ? "" : value; - } - - @Override - public String parse(String in, boolean force) throws DirectorParsingException { - if (in == null || in.trim().isBlank()) { - throw new DirectorParsingException("You must provide at least one external datapack id or structure id."); - } - - return in.trim(); - } - - @Override - public boolean supports(Class type) { - return type.equals(String.class); - } - - @Override - public String getRandomDefault() { - KList possibilities = getPossibilities(); - String random = possibilities.getRandom(); - return random == null ? "external-datapack-id" : random; - } -} diff --git a/core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/ObjectTargetHandler.java b/core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/ObjectTargetHandler.java new file mode 100644 index 000000000..1de6f2c7e --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/common/director/specialhandlers/ObjectTargetHandler.java @@ -0,0 +1,92 @@ +/* + * 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.util.common.director.specialhandlers; + +import art.arcane.iris.Iris; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.iris.util.common.director.DirectorParameterHandler; +import art.arcane.volmlib.util.director.exceptions.DirectorParsingException; + +import java.io.File; +import java.util.HashSet; +import java.util.Set; + +public class ObjectTargetHandler implements DirectorParameterHandler { + @Override + public KList getPossibilities() { + KList out = new KList<>(); + Set prefixes = new HashSet<>(); + + IrisData data = data(); + if (data != null) { + for (String k : data.getObjectLoader().getPossibleKeys()) { + out.add(k); + collectPrefixes(k, prefixes); + } + } else { + File packsFolder = Iris.instance.getDataFolder("packs"); + File[] packs = packsFolder.listFiles(); + if (packs != null) { + for (File pack : packs) { + if (!pack.isDirectory()) continue; + IrisData d = IrisData.get(pack); + for (String k : d.getObjectLoader().getPossibleKeys()) { + out.add(k); + collectPrefixes(k, prefixes); + } + } + } + } + + for (String p : prefixes) { + out.add(p); + } + return out; + } + + private static void collectPrefixes(String key, Set prefixes) { + int idx = 0; + while ((idx = key.indexOf('/', idx)) >= 0) { + prefixes.add(key.substring(0, idx + 1)); + idx++; + } + } + + @Override + public String toString(String irisObject) { + return irisObject; + } + + @Override + public String parse(String in, boolean force) throws DirectorParsingException { + return in; + } + + @Override + public boolean supports(Class type) { + return type.equals(String.class); + } + + @Override + public String getRandomDefault() { + String f = getPossibilities().getRandom(); + return f == null ? "trees/" : f; + } +} diff --git a/core/src/test/java/art/arcane/iris/core/ExternalDataPackPipelineNbtRewriteTest.java b/core/src/test/java/art/arcane/iris/core/ExternalDataPackPipelineNbtRewriteTest.java deleted file mode 100644 index 9101cf23a..000000000 --- a/core/src/test/java/art/arcane/iris/core/ExternalDataPackPipelineNbtRewriteTest.java +++ /dev/null @@ -1,93 +0,0 @@ -package art.arcane.iris.core; - -import art.arcane.volmlib.util.nbt.io.NBTDeserializer; -import art.arcane.volmlib.util.nbt.io.NBTSerializer; -import art.arcane.volmlib.util.nbt.io.NamedTag; -import art.arcane.volmlib.util.nbt.tag.CompoundTag; -import art.arcane.volmlib.util.nbt.tag.IntTag; -import art.arcane.volmlib.util.nbt.tag.ListTag; -import art.arcane.volmlib.util.nbt.tag.Tag; -import org.junit.Test; - -import java.io.ByteArrayInputStream; -import java.util.HashMap; -import java.util.Map; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; - -public class ExternalDataPackPipelineNbtRewriteTest { - @Test - public void rewritesOnlyJigsawPoolReferencesForCompressedAndUncompressedNbt() throws Exception { - for (boolean compressed : new boolean[]{false, true}) { - byte[] source = encodeStructureNbt(compressed, true); - Map remapped = new HashMap<>(); - remapped.put("minecraft:witch_hut/foundation", "iris_external_1:witch_hut/foundation"); - - byte[] rewritten = invokeRewrite(source, remapped); - CompoundTag root = decodeRoot(rewritten, compressed); - ListTag blocks = root.getListTag("blocks"); - - CompoundTag jigsawBlock = (CompoundTag) blocks.get(0); - CompoundTag nonJigsawBlock = (CompoundTag) blocks.get(1); - assertEquals("iris_external_1:witch_hut/foundation", jigsawBlock.getCompoundTag("nbt").getString("pool")); - assertEquals("minecraft:witch_hut/foundation", nonJigsawBlock.getCompoundTag("nbt").getString("pool")); - } - } - - @Test - public void nonJigsawPayloadIsLeftUnchanged() throws Exception { - byte[] source = encodeStructureNbt(false, false); - Map remapped = new HashMap<>(); - remapped.put("minecraft:witch_hut/foundation", "iris_external_1:witch_hut/foundation"); - - byte[] rewritten = invokeRewrite(source, remapped); - assertArrayEquals(source, rewritten); - } - - private byte[] invokeRewrite(byte[] input, Map remappedKeys) { - return StructureNbtJigsawPoolRewriter.rewrite(input, remappedKeys); - } - - private byte[] encodeStructureNbt(boolean compressed, boolean includeJigsaw) throws Exception { - CompoundTag root = new CompoundTag(); - ListTag palette = new ListTag<>(CompoundTag.class); - - CompoundTag firstPalette = new CompoundTag(); - firstPalette.putString("Name", includeJigsaw ? "minecraft:jigsaw" : "minecraft:stone"); - palette.add(firstPalette); - - CompoundTag secondPalette = new CompoundTag(); - secondPalette.putString("Name", "minecraft:stone"); - palette.add(secondPalette); - root.put("palette", palette); - - ListTag blocks = new ListTag<>(CompoundTag.class); - blocks.add(blockTag(0, "minecraft:witch_hut/foundation")); - blocks.add(blockTag(1, "minecraft:witch_hut/foundation")); - root.put("blocks", blocks); - - NamedTag named = new NamedTag("test", root); - return new NBTSerializer(compressed).toBytes(named); - } - - private CompoundTag blockTag(int state, String pool) { - CompoundTag block = new CompoundTag(); - block.putInt("state", state); - CompoundTag nbt = new CompoundTag(); - nbt.putString("pool", pool); - block.put("nbt", nbt); - ListTag pos = new ListTag<>(IntTag.class); - pos.add(new IntTag(0)); - pos.add(new IntTag(0)); - pos.add(new IntTag(0)); - block.put("pos", pos); - return block; - } - - private CompoundTag decodeRoot(byte[] bytes, boolean compressed) throws Exception { - NamedTag namedTag = new NBTDeserializer(compressed).fromStream(new ByteArrayInputStream(bytes)); - Tag rootTag = namedTag.getTag(); - return (CompoundTag) rootTag; - } -} diff --git a/core/src/test/java/art/arcane/iris/core/ServerConfiguratorDatapackFolderTest.java b/core/src/test/java/art/arcane/iris/core/ServerConfiguratorDatapackFolderTest.java deleted file mode 100644 index 7fbd17956..000000000 --- a/core/src/test/java/art/arcane/iris/core/ServerConfiguratorDatapackFolderTest.java +++ /dev/null @@ -1,44 +0,0 @@ -package art.arcane.iris.core; - -import art.arcane.volmlib.util.collection.KList; -import art.arcane.volmlib.util.collection.KMap; -import org.junit.Test; - -import java.io.File; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -public class ServerConfiguratorDatapackFolderTest { - @Test - public void resolvesDimensionWorldFolderBackToRootDatapacks() { - File folder = new File("/tmp/server/world/dimensions/minecraft/overworld"); - File datapacks = ServerConfigurator.resolveDatapacksFolder(folder); - assertEquals(new File("/tmp/server/world/datapacks").getAbsolutePath(), datapacks.getAbsolutePath()); - } - - @Test - public void keepsStandaloneWorldFolderDatapacksUnchanged() { - File folder = new File("/tmp/server/custom_world"); - File datapacks = ServerConfigurator.resolveDatapacksFolder(folder); - assertEquals(new File("/tmp/server/custom_world/datapacks").getAbsolutePath(), datapacks.getAbsolutePath()); - } - - @Test - public void installFoldersIncludeExtraStudioWorldDatapackTargets() { - File baseFolder = new File("/tmp/server/world/datapacks"); - File extraFolder = new File("/tmp/server/iris-studio/datapacks"); - KList baseFolders = new KList<>(); - baseFolders.add(baseFolder); - KList extraFolders = new KList<>(); - extraFolders.add(extraFolder); - KMap> extrasByPack = new KMap<>(); - extrasByPack.put("overworld", extraFolders); - - KList folders = ServerConfigurator.collectInstallDatapackFolders(baseFolders, extrasByPack); - - assertEquals(2, folders.size()); - assertTrue(folders.contains(baseFolder)); - assertTrue(folders.contains(extraFolder)); - } -} diff --git a/core/src/test/java/art/arcane/iris/core/runtime/DatapackReadinessResultTest.java b/core/src/test/java/art/arcane/iris/core/runtime/DatapackReadinessResultTest.java deleted file mode 100644 index 08d30c9c2..000000000 --- a/core/src/test/java/art/arcane/iris/core/runtime/DatapackReadinessResultTest.java +++ /dev/null @@ -1,45 +0,0 @@ -package art.arcane.iris.core.runtime; - -import org.junit.Test; - -import java.io.File; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -public class DatapackReadinessResultTest { - @Test - public void verificationUsesDimensionTypeKeyPath() throws Exception { - Path root = Files.createTempDirectory("iris-datapack-readiness"); - Path datapackRoot = root.resolve("iris"); - Files.createDirectories(datapackRoot.resolve("data/iris/dimension_type")); - Files.writeString(datapackRoot.resolve("pack.mcmeta"), "{}"); - Files.writeString(datapackRoot.resolve("data/iris/dimension_type/runtime-key.json"), "{}"); - - ArrayList verifiedPaths = new ArrayList<>(); - ArrayList missingPaths = new ArrayList<>(); - DatapackReadinessResult.collectVerificationPaths(root.toFile(), "runtime-key", verifiedPaths, missingPaths); - - assertTrue(missingPaths.isEmpty()); - assertEquals(2, verifiedPaths.size()); - } - - @Test - public void verificationMarksMissingDimensionTypePath() throws Exception { - Path root = Files.createTempDirectory("iris-datapack-readiness-missing"); - Path datapackRoot = root.resolve("iris"); - Files.createDirectories(datapackRoot); - Files.writeString(datapackRoot.resolve("pack.mcmeta"), "{}"); - - ArrayList verifiedPaths = new ArrayList<>(); - ArrayList missingPaths = new ArrayList<>(); - DatapackReadinessResult.collectVerificationPaths(root.toFile(), "runtime-key", verifiedPaths, missingPaths); - - assertEquals(1, verifiedPaths.size()); - assertEquals(1, missingPaths.size()); - assertTrue(missingPaths.get(0).endsWith(File.separator + "iris" + File.separator + "data" + File.separator + "iris" + File.separator + "dimension_type" + File.separator + "runtime-key.json")); - } -} 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 732c7af3d..6cd1e5655 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 @@ -2,19 +2,11 @@ package art.arcane.iris.core.nms.v1_21_R7; import com.mojang.datafixers.util.Pair; import com.mojang.serialization.MapCodec; -import art.arcane.iris.Iris; -import art.arcane.iris.core.ExternalDataPackPipeline; -import art.arcane.iris.core.IrisSettings; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.util.common.reflect.WrappedField; import art.arcane.iris.util.common.reflect.WrappedReturningMethod; -import net.minecraft.CrashReport; -import net.minecraft.CrashReportCategory; -import net.minecraft.ReportedException; import net.minecraft.core.*; -import net.minecraft.core.registries.Registries; import net.minecraft.resources.ResourceKey; -import net.minecraft.resources.Identifier; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.WorldGenRegion; import net.minecraft.util.random.WeightedList; @@ -29,12 +21,9 @@ import net.minecraft.world.level.chunk.ChunkGeneratorStructureState; import net.minecraft.world.level.chunk.status.ChunkStatus; import net.minecraft.world.level.levelgen.*; import net.minecraft.world.level.levelgen.blending.Blender; -import net.minecraft.world.level.levelgen.structure.BoundingBox; import net.minecraft.world.level.levelgen.structure.Structure; -import net.minecraft.world.level.levelgen.structure.StructureStart; import net.minecraft.world.level.levelgen.structure.StructureSet; import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; -import org.bukkit.Bukkit; import org.bukkit.World; import org.bukkit.craftbukkit.CraftWorld; import org.bukkit.craftbukkit.generator.CustomChunkGenerator; @@ -45,18 +34,12 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.*; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Supplier; public class IrisChunkGenerator extends CustomChunkGenerator { private static final WrappedField BIOME_SOURCE; private static final WrappedReturningMethod SET_HEIGHT; - private static final int EXTERNAL_FOUNDATION_MAX_DEPTH = 96; - private static final Set loggedExternalStructureFingerprintKeys = ConcurrentHashMap.newKeySet(); private final ChunkGenerator delegate; private final Engine engine; - private volatile Registry cachedStructureRegistry; - private volatile Map cachedStructureOrder; public IrisChunkGenerator(ChunkGenerator delegate, long seed, Engine engine, World world) { super(((CraftWorld) world).getHandle(), edit(delegate, new CustomBiomeSource(seed, engine, world)), null); @@ -66,10 +49,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator { @Override public @Nullable Pair> findNearestMapStructure(ServerLevel level, HolderSet holders, BlockPos pos, int radius, boolean findUnexplored) { - if (holders.size() == 0) return null; - if (engine.getDimension().isDisableExplorerMaps()) - return null; - return delegate.findNearestMapStructure(level, holders, pos, radius, findUnexplored); + return null; } @Override @@ -96,12 +76,6 @@ public class IrisChunkGenerator extends CustomChunkGenerator { @Override public void createStructures(RegistryAccess registryAccess, ChunkGeneratorStructureState structureState, StructureManager structureManager, ChunkAccess access, StructureTemplateManager templateManager, ResourceKey levelKey) { - if (!structureManager.shouldGenerateStructures()) - return; - if (!IrisSettings.get().getGeneral().isAutoGenerateIntrinsicStructures()) { - return; - } - delegate.createStructures(registryAccess, structureState, structureManager, access, templateManager, levelKey); } @Override @@ -157,18 +131,8 @@ public class IrisChunkGenerator extends CustomChunkGenerator { @Override public void addVanillaDecorations(WorldGenLevel level, ChunkAccess chunkAccess, StructureManager structureManager) { - if (!structureManager.shouldGenerateStructures()) - return; - if (!IrisSettings.get().getGeneral().isAutoGenerateIntrinsicStructures()) { - return; - } - SectionPos sectionPos = SectionPos.of(chunkAccess.getPos(), level.getMinSectionY()); BlockPos blockPos = sectionPos.origin(); - WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed())); - long i = random.setDecorationSeed(level.getSeed(), blockPos.getX(), blockPos.getZ()); - Registry structureRegistry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE); - Map structureOrder = getStructureOrder(structureRegistry); Heightmap surface = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG); Heightmap ocean = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG); @@ -180,403 +144,18 @@ public class IrisChunkGenerator extends CustomChunkGenerator { int wX = x + blockPos.getX(); int wZ = z + blockPos.getZ(); - int noAir = engine.getHeight(wX, wZ, false) + engine.getMinHeight() + 1; - int noFluid = engine.getHeight(wX, wZ, true) + engine.getMinHeight() + 1; - int oceanHeight = ocean.getFirstAvailable(x, z); - int surfaceHeight = surface.getFirstAvailable(x, z); - int motionHeight = motion.getFirstAvailable(x, z); - int motionNoLeavesHeight = motionNoLeaves.getFirstAvailable(x, z); - if (noFluid > oceanHeight) { - SET_HEIGHT.invoke(ocean, x, z, noFluid); - } - if (noAir > surfaceHeight) { - SET_HEIGHT.invoke(surface, x, z, noAir); - } - if (noAir > motionHeight) { - SET_HEIGHT.invoke(motion, x, z, noAir); - } - if (noAir > motionNoLeavesHeight) { - SET_HEIGHT.invoke(motionNoLeaves, x, z, noAir); - } - } - } - - List starts = new ArrayList<>(structureManager.startsForStructure(chunkAccess.getPos(), structure -> true)); - starts.sort(Comparator.comparingInt(start -> structureOrder.getOrDefault(start.getStructure(), Integer.MAX_VALUE))); - Set externalSmartBoreStructures = ExternalDataPackPipeline.snapshotSmartBoreStructureKeys(); - IrisSettings.IrisSettingsGeneral general = IrisSettings.get().getGeneral(); - boolean intrinsicFoundationsEnabled = general.isIntrinsicStructureFoundations(); - int intrinsicFoundationDepth = Math.max(0, general.getIntrinsicFoundationMaxDepth()); - List intrinsicAllowlist = general.getIntrinsicStructureAllowlist(); - - int seededStructureIndex = Integer.MIN_VALUE; - for (int j = 0; j < starts.size(); j++) { - StructureStart start = starts.get(j); - Structure structure = start.getStructure(); - int structureIndex = structureOrder.getOrDefault(structure, j); - if (structureIndex != seededStructureIndex) { - random.setFeatureSeed(i, structureIndex, structure.step().ordinal()); - seededStructureIndex = structureIndex; - } - Supplier supplier = () -> structureRegistry.getResourceKey(structure).map(Object::toString).orElseGet(structure::toString); - String structureKey = resolveStructureKey(structureRegistry, structure); - boolean isExternalSmartBoreStructure = externalSmartBoreStructures.contains(structureKey); - boolean isIntrinsicFoundationStructure = !isExternalSmartBoreStructure - && intrinsicFoundationsEnabled - && intrinsicFoundationDepth > 0 - && matchesIntrinsicAllowlist(structureKey, intrinsicAllowlist); - int foundationDepth = isExternalSmartBoreStructure - ? EXTERNAL_FOUNDATION_MAX_DEPTH - : (isIntrinsicFoundationStructure ? intrinsicFoundationDepth : 0); - BitSet[] beforeSolidColumns = null; - if (foundationDepth > 0) { - beforeSolidColumns = snapshotChunkSolidColumns(level, chunkAccess); - } - - try { - level.setCurrentlyGenerating(supplier); - start.placeInChunk(level, structureManager, this, random, getWritableArea(chunkAccess), chunkAccess.getPos()); - if (beforeSolidColumns != null) { - applyStructureFoundations(level, chunkAccess, beforeSolidColumns, foundationDepth); - } - if (shouldLogExternalStructureFingerprint(structureKey)) { - logExternalStructureFingerprint(structureKey, start); - } - } catch (Exception exception) { - CrashReport crashReport = CrashReport.forThrowable(exception, "Feature placement"); - CrashReportCategory category = crashReport.addCategory("Feature"); - category.setDetail("Description", supplier::get); - throw new ReportedException(crashReport); + int terrainTop = engine.getHeight(wX, wZ, false) + engine.getMinHeight() + 1; + int terrainNoFluid = engine.getHeight(wX, wZ, true) + engine.getMinHeight() + 1; + SET_HEIGHT.invoke(ocean, x, z, terrainNoFluid); + SET_HEIGHT.invoke(surface, x, z, terrainTop); + SET_HEIGHT.invoke(motion, x, z, terrainTop); + SET_HEIGHT.invoke(motionNoLeaves, x, z, terrainTop); } } Heightmap.primeHeightmaps(chunkAccess, ChunkStatus.FINAL_HEIGHTMAPS); } - private static String resolveStructureKey(Registry structureRegistry, Structure structure) { - Identifier directKey = structureRegistry.getKey(structure); - if (directKey != null) { - return directKey.toString().toLowerCase(Locale.ROOT); - } - - String fallback = String.valueOf(structure); - int slash = fallback.lastIndexOf('/'); - int end = fallback.lastIndexOf(']'); - if (slash >= 0 && end > slash) { - return fallback.substring(slash + 1, end).toLowerCase(Locale.ROOT); - } - - return fallback.toLowerCase(Locale.ROOT); - } - - private static BoundingBox getWritableArea(ChunkAccess ichunkaccess) { - ChunkPos chunkPos = ichunkaccess.getPos(); - int minX = chunkPos.getMinBlockX(); - int minZ = chunkPos.getMinBlockZ(); - LevelHeightAccessor heightAccessor = ichunkaccess.getHeightAccessorForGeneration(); - int minY = heightAccessor.getMinY() + 1; - int maxY = heightAccessor.getMaxY(); - return new BoundingBox(minX, minY, minZ, minX + 15, maxY, minZ + 15); - } - - private static BitSet[] snapshotChunkSolidColumns(WorldGenLevel level, ChunkAccess chunkAccess) { - int minY = level.getMinY(); - int maxY = level.getMaxY(); - int ySpan = maxY - minY; - if (ySpan <= 0) { - return new BitSet[0]; - } - - ChunkPos chunkPos = chunkAccess.getPos(); - int minX = chunkPos.getMinBlockX(); - int minZ = chunkPos.getMinBlockZ(); - BitSet[] columns = new BitSet[16 * 16]; - BlockPos.MutableBlockPos mutablePos = new BlockPos.MutableBlockPos(); - for (int localX = 0; localX < 16; localX++) { - for (int localZ = 0; localZ < 16; localZ++) { - int index = (localX << 4) | localZ; - BitSet solids = new BitSet(ySpan); - int worldX = minX + localX; - int worldZ = minZ + localZ; - for (int y = minY; y < maxY; y++) { - mutablePos.set(worldX, y, worldZ); - if (isFoundationSolid(level.getBlockState(mutablePos))) { - solids.set(y - minY); - } - } - columns[index] = solids; - } - } - - return columns; - } - - private static boolean matchesIntrinsicAllowlist(String structureKey, List allowlist) { - if (structureKey == null || structureKey.isBlank() || allowlist == null || allowlist.isEmpty()) { - return false; - } - String key = structureKey.toLowerCase(Locale.ROOT); - for (String raw : allowlist) { - if (raw == null) { - continue; - } - String pattern = raw.trim().toLowerCase(Locale.ROOT); - if (pattern.isEmpty()) { - continue; - } - if (pattern.endsWith("*")) { - if (key.startsWith(pattern.substring(0, pattern.length() - 1))) { - return true; - } - } else if (key.equals(pattern)) { - return true; - } - } - return false; - } - - private static void applyStructureFoundations( - WorldGenLevel level, - ChunkAccess chunkAccess, - BitSet[] beforeSolidColumns, - int maxDepth - ) { - if (beforeSolidColumns == null || beforeSolidColumns.length == 0 || maxDepth <= 0) { - return; - } - - int minY = level.getMinY(); - int maxY = level.getMaxY(); - int ySpan = maxY - minY; - if (ySpan <= 0) { - return; - } - - ChunkPos chunkPos = chunkAccess.getPos(); - int minX = chunkPos.getMinBlockX(); - int minZ = chunkPos.getMinBlockZ(); - BlockPos.MutableBlockPos mutablePos = new BlockPos.MutableBlockPos(); - for (int localX = 0; localX < 16; localX++) { - for (int localZ = 0; localZ < 16; localZ++) { - int index = (localX << 4) | localZ; - BitSet before = beforeSolidColumns[index]; - if (before == null) { - continue; - } - - int worldX = minX + localX; - int worldZ = minZ + localZ; - int lowestNewSolidY = Integer.MIN_VALUE; - for (int y = minY; y < maxY; y++) { - mutablePos.set(worldX, y, worldZ); - BlockState state = level.getBlockState(mutablePos); - if (!isFoundationSolid(state)) { - continue; - } - - if (before.get(y - minY)) { - continue; - } - - lowestNewSolidY = y; - break; - } - - if (lowestNewSolidY == Integer.MIN_VALUE) { - continue; - } - - mutablePos.set(worldX, lowestNewSolidY, worldZ); - BlockState foundationState = level.getBlockState(mutablePos); - if (!isFoundationSolid(foundationState)) { - continue; - } - - int depth = 0; - for (int y = lowestNewSolidY - 1; y >= minY && depth < maxDepth; y--) { - mutablePos.set(worldX, y, worldZ); - BlockState state = level.getBlockState(mutablePos); - if (isFoundationSolid(state)) { - break; - } - - level.setBlock(mutablePos, foundationState, 2); - depth++; - } - } - } - } - - private static boolean isFoundationSolid(BlockState state) { - if (state == null || state.isAir()) { - return false; - } - if (!state.getFluidState().isEmpty()) { - return false; - } - return Heightmap.Types.MOTION_BLOCKING_NO_LEAVES.isOpaque().test(state); - } - - private static boolean shouldLogExternalStructureFingerprint(String structureKey) { - if (!IrisSettings.get().getGeneral().isDebug()) { - return false; - } - if (structureKey == null || structureKey.isBlank()) { - return false; - } - - String normalized = structureKey.toLowerCase(Locale.ROOT); - if (!"minecraft:ancient_city".equals(normalized) - && !"minecraft:mineshaft".equals(normalized) - && !"minecraft:mineshaft_mesa".equals(normalized)) { - return false; - } - - return loggedExternalStructureFingerprintKeys.add(normalized); - } - - private static void logExternalStructureFingerprint(String structureKey, StructureStart start) { - if (start == null) { - return; - } - - List pieces = extractPieces(start); - int pieceCount = pieces.size(); - String firstPieceType = "none"; - String firstPieceFingerprint = "none"; - if (!pieces.isEmpty()) { - Object firstPiece = pieces.get(0); - if (firstPiece != null) { - firstPieceType = firstPiece.getClass().getName(); - firstPieceFingerprint = resolvePieceFingerprint(firstPiece); - } - } - - Iris.debug("External structure fingerprint: key=" + structureKey - + ", pieces=" + pieceCount - + ", firstPiece=" + firstPieceType - + ", fingerprint=" + firstPieceFingerprint); - } - - private static List extractPieces(StructureStart start) { - try { - Method getPiecesMethod = start.getClass().getMethod("getPieces"); - Object result = getPiecesMethod.invoke(start); - if (result instanceof List list) { - return list; - } - if (result != null) { - Method piecesMethod = result.getClass().getMethod("pieces"); - Object piecesResult = piecesMethod.invoke(result); - if (piecesResult instanceof List list) { - return list; - } - } - } catch (Throwable ignored) { - } - - try { - Method piecesMethod = start.getClass().getMethod("pieces"); - Object result = piecesMethod.invoke(start); - if (result instanceof List list) { - return list; - } - } catch (Throwable ignored) { - } - - return List.of(); - } - - private static String resolvePieceFingerprint(Object piece) { - if (piece == null) { - return "unknown"; - } - - try { - Method templateNameMethod = piece.getClass().getMethod("templateName"); - Object value = templateNameMethod.invoke(piece); - if (value != null) { - String normalized = String.valueOf(value); - if (!normalized.isBlank()) { - return normalized; - } - } - } catch (Throwable ignored) { - } - - try { - Method templateMethod = piece.getClass().getMethod("template"); - Object value = templateMethod.invoke(piece); - if (value != null) { - return value.getClass().getName(); - } - } catch (Throwable ignored) { - } - - Class current = piece.getClass(); - while (current != null && current != Object.class) { - Field[] fields = current.getDeclaredFields(); - for (Field field : fields) { - try { - field.setAccessible(true); - Object value = field.get(piece); - if (value == null) { - continue; - } - - if (value instanceof Identifier identifier) { - String normalized = identifier.toString(); - if (!normalized.isBlank()) { - return normalized; - } - } - - if (value instanceof String text) { - String fieldName = field.getName() == null ? "" : field.getName().toLowerCase(Locale.ROOT); - if (fieldName.contains("template") || fieldName.contains("name") || fieldName.contains("id")) { - if (!text.isBlank()) { - return text; - } - } - } - } catch (Throwable ignored) { - } - } - current = current.getSuperclass(); - } - - return piece.getClass().getSimpleName(); - } - - private Map getStructureOrder(Registry structureRegistry) { - Map localOrder = cachedStructureOrder; - Registry localRegistry = cachedStructureRegistry; - if (localRegistry == structureRegistry && localOrder != null) { - return localOrder; - } - - synchronized (this) { - Map synchronizedOrder = cachedStructureOrder; - Registry synchronizedRegistry = cachedStructureRegistry; - if (synchronizedRegistry == structureRegistry && synchronizedOrder != null) { - return synchronizedOrder; - } - - List sortedStructures = structureRegistry.stream() - .sorted(Comparator.comparingInt(structure -> structure.step().ordinal())) - .toList(); - Map builtOrder = new IdentityHashMap<>(sortedStructures.size()); - for (int index = 0; index < sortedStructures.size(); index++) { - Structure structure = sortedStructures.get(index); - builtOrder.put(structure, index); - } - - cachedStructureRegistry = structureRegistry; - cachedStructureOrder = builtOrder; - return builtOrder; - } - } - @Override public void spawnOriginalMobs(WorldGenRegion regionlimitedworldaccess) { delegate.spawnOriginalMobs(regionlimitedworldaccess); 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 4ce01f48a..a6a18347b 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 @@ -5,7 +5,6 @@ import art.arcane.iris.Iris; import art.arcane.iris.core.nms.INMSBinding; import art.arcane.iris.core.nms.container.BiomeColor; import art.arcane.iris.core.nms.container.Pair; -import art.arcane.iris.core.nms.container.StructurePlacement; import art.arcane.iris.core.nms.container.BlockProperty; import art.arcane.iris.core.nms.datapack.DataVersion; import art.arcane.iris.engine.data.cache.AtomicCache; @@ -731,65 +730,6 @@ public class NMSBinding implements INMSBinding { return 0; } - @Override - public KList getStructureKeys() { - KList keys = new KList<>(); - - var registry = registry().lookup(Registries.STRUCTURE).orElse(null); - if (registry == null) return keys; - registry.keySet().stream().map(Identifier::toString).forEach(keys::add); - registry.getTags() - .map(HolderSet.Named::key) - .map(TagKey::location) - .map(Identifier::toString) - .map(s -> "#" + s) - .forEach(keys::add); - - return keys; - } - - @Override - public KMap> getVanillaStructureBiomeTags() { - KMap> tags = new KMap<>(); - - Registry registry = registry().lookup(Registries.BIOME).orElse(null); - if (registry == null) { - return tags; - } - - registry.getTags().forEach(named -> { - TagKey tagKey = named.key(); - Identifier location = tagKey.location(); - if (!"minecraft".equals(location.getNamespace())) { - return; - } - - String path = location.getPath(); - if (!path.startsWith("has_structure/")) { - return; - } - - KList values = new KList<>(); - named.stream().forEach(holder -> { - net.minecraft.world.level.biome.Biome biome = holder.value(); - Identifier biomeLocation = registry.getKey(biome); - if (biomeLocation == null) { - return; - } - if ("minecraft".equals(biomeLocation.getNamespace())) { - values.add(biomeLocation.toString()); - } - }); - - KList uniqueValues = values.removeDuplicates(); - if (!uniqueValues.isEmpty()) { - tags.put(path, uniqueValues); - } - }); - - return tags; - } - @Override public boolean missingDimensionTypes(String... keys) { var type = registry().lookupOrThrow(Registries.DIMENSION_TYPE); @@ -848,76 +788,8 @@ public class NMSBinding implements INMSBinding { return new BlockProperty(property.getName(), property.getValueClass(), state.getValue(property), property.getPossibleValues(), property::getName); } - @Override - public void placeStructures(Chunk chunk) { - var craft = ((CraftChunk) chunk); - var level = craft.getCraftWorld().getHandle(); - var access = craft.getHandle(ChunkStatus.FEATURES); - if (access instanceof LevelChunk) { - return; - } - level.getChunkSource().getGenerator().applyBiomeDecoration(level, access, level.structureManager()); - } - - @Override - public KMap collectStructures() { - var structureSets = registry().lookupOrThrow(Registries.STRUCTURE_SET); - var structurePlacements = registry().lookupOrThrow(Registries.STRUCTURE_PLACEMENT); - return structureSets.keySet() - .stream() - .map(structureSets::get) - .filter(Optional::isPresent) - .map(Optional::get) - .map(holder -> { - var set = holder.value(); - var placement = set.placement(); - var key = holder.key().identifier(); - StructurePlacement.StructurePlacementBuilder builder; - if (placement instanceof RandomSpreadStructurePlacement random) { - builder = StructurePlacement.RandomSpread.builder() - .separation(random.separation()) - .spacing(random.spacing()) - .spreadType(switch (random.spreadType()) { - case LINEAR -> StructurePlacement.SpreadType.LINEAR; - case TRIANGULAR -> StructurePlacement.SpreadType.TRIANGULAR; - }); - } else if (placement instanceof ConcentricRingsStructurePlacement rings) { - builder = StructurePlacement.ConcentricRings.builder() - .distance(rings.distance()) - .spread(rings.spread()) - .count(rings.count()); - } else { - Iris.warn("Unsupported structure placement for set " + key + " with type " + structurePlacements.getKey(placement.type())); - return null; - } - - return new Pair<>(new art.arcane.iris.core.link.Identifier(key.getNamespace(), key.getPath()), builder - .salt(placement.salt) - .frequency(placement.frequency) - .structures(set.structures() - .stream() - .map(entry -> new StructurePlacement.Structure( - entry.weight(), - entry.structure() - .unwrapKey() - .map(ResourceKey::identifier) - .map(Identifier::toString) - .orElse(null), - entry.structure().tags() - .map(TagKey::location) - .map(Identifier::toString) - .toList() - )) - .filter(StructurePlacement.Structure::isValid) - .toList()) - .build()); - }) - .filter(Objects::nonNull) - .collect(Collectors.toMap(Pair::getA, Pair::getB, (a, b) -> a, KMap::new)); - } - private static final Pattern VANILLA_DATAPACK_ENTRY = Pattern.compile( - "^data/minecraft/(?:worldgen/(?:structure|structure_set|template_pool|processor_list)/.+\\.json" + "^data/minecraft/(?:worldgen/(?:structure|structure_set|template_pool|processor_list|configured_feature|placed_feature)/.+\\.json" + "|structures?/.+\\.nbt" + "|tags/worldgen/biome/has_structure/.+\\.json)$" );