diff --git a/core/src/main/java/art/arcane/iris/core/ExternalDataPackPipeline.java b/core/src/main/java/art/arcane/iris/core/ExternalDataPackPipeline.java index 0523f2972..4236a84a5 100644 --- a/core/src/main/java/art/arcane/iris/core/ExternalDataPackPipeline.java +++ b/core/src/main/java/art/arcane/iris/core/ExternalDataPackPipeline.java @@ -90,6 +90,7 @@ public final class ExternalDataPackPipeline { private static final String MANAGED_PACK_META_DESCRIPTION = "Iris managed external structure datapack assets."; 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 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())); @@ -97,6 +98,7 @@ public final class ExternalDataPackPipeline { 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 AtomicCache> VANILLA_STRUCTURE_PLACEMENTS = new AtomicCache<>(); private static final BlockData AIR = B.getAir(); @@ -132,6 +134,27 @@ public final class ExternalDataPackPipeline { 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(); @@ -184,6 +207,7 @@ public final class ExternalDataPackPipeline { PipelineSummary summary = new PipelineSummary(); PACK_ENVIRONMENT_CACHE.clear(); RESOLVED_LOCATE_STRUCTURES_BY_ID.clear(); + RESOLVED_LOCATE_STRUCTURES_BY_OBJECT_KEY.clear(); Set knownWorldDatapackFolders = new LinkedHashSet<>(); if (worldDatapackFoldersByPack != null) { @@ -208,12 +232,14 @@ public final class ExternalDataPackPipeline { if (normalizedRequests.isEmpty()) { Iris.info("Downloading datapacks [0/0] Downloading/Done!"); writeLocateManifest(Map.of()); + writeObjectLocateManifest(Map.of()); summary.legacyWorldCopyRemovals += pruneManagedWorldDatapacks(knownWorldDatapackFolders, Set.of()); return summary; } List sourceInputs = new ArrayList<>(); LinkedHashMap> resolvedLocateStructuresById = new LinkedHashMap<>(); + LinkedHashMap> resolvedLocateStructuresByObjectKey = new LinkedHashMap<>(); for (int requestIndex = 0; requestIndex < normalizedRequests.size(); requestIndex++) { DatapackRequest request = normalizedRequests.get(requestIndex); if (request == null) { @@ -245,10 +271,10 @@ public final class ExternalDataPackPipeline { if (syncResult.downloaded()) { summary.syncedRequests++; + Iris.info("Downloading datapacks [" + (requestIndex + 1) + "/" + normalizedRequests.size() + "] Downloading/Done!"); } else if (syncResult.restored()) { summary.restoredRequests++; } - Iris.info("Downloading datapacks [" + (requestIndex + 1) + "/" + normalizedRequests.size() + "] Downloading/Done!"); mergeResolvedLocateStructures(resolvedLocateStructuresById, request.id(), request.resolvedLocateStructures()); sourceInputs.add(new RequestedSourceInput(syncResult.source(), request)); } @@ -258,7 +284,9 @@ public final class ExternalDataPackPipeline { summary.legacyWorldCopyRemovals += pruneManagedWorldDatapacks(knownWorldDatapackFolders, Set.of()); } writeLocateManifest(resolvedLocateStructuresById); + writeObjectLocateManifest(resolvedLocateStructuresByObjectKey); RESOLVED_LOCATE_STRUCTURES_BY_ID.putAll(resolvedLocateStructuresById); + RESOLVED_LOCATE_STRUCTURES_BY_OBJECT_KEY.putAll(resolvedLocateStructuresByObjectKey); return summary; } @@ -281,7 +309,7 @@ public final class ExternalDataPackPipeline { continue; } - SourceDescriptor sourceDescriptor = createSourceDescriptor(entry, request.targetPack(), request.requiredEnvironment()); + 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())) { @@ -299,30 +327,39 @@ public final class ExternalDataPackPipeline { } seenSourceKeys.add(sourceDescriptor.sourceKey()); - File sourceRoot = resolveSourceRoot(sourceDescriptor.targetPack(), 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; if (cachedSource != null && sourceDescriptor.fingerprint().equals(cachedSource.optString("fingerprint", "")) && sameTargetPack + && sameObjectRoot && sourceRoot.exists()) { newSources.put(cachedSource); addSourceToSummary(importSummary, cachedSource, true); + activeSource = cachedSource; } else { - if (cachedTargetPack != null && !cachedTargetPack.equals(sourceDescriptor.targetPack())) { - File previousSourceRoot = resolveSourceRoot(cachedTargetPack, sourceDescriptor.sourceKey()); + 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); + JSONObject sourceResult = convertSource(entry, sourceDescriptor, sourceRoot, request.id()); newSources.put(sourceResult); addSourceToSummary(importSummary, sourceResult, false); + activeSource = sourceResult; int conversionFailed = sourceResult.optInt("failed", 0); if (conversionFailed > 0) { int conversionScanned = sourceResult.optInt("nbtScanned", 0); @@ -339,6 +376,14 @@ public final class ExternalDataPackPipeline { summary.worldDatapacksInstalled += projectionResult.installedDatapacks(); summary.worldAssetsInstalled += projectionResult.installedAssets(); mergeResolvedLocateStructures(resolvedLocateStructuresById, request.id(), projectionResult.resolvedLocateStructures()); + 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()); } @@ -379,7 +424,9 @@ public final class ExternalDataPackPipeline { } writeLocateManifest(resolvedLocateStructuresById); + writeObjectLocateManifest(resolvedLocateStructuresByObjectKey); RESOLVED_LOCATE_STRUCTURES_BY_ID.putAll(resolvedLocateStructuresById); + RESOLVED_LOCATE_STRUCTURES_BY_OBJECT_KEY.putAll(resolvedLocateStructuresByObjectKey); return summary; } @@ -387,6 +434,10 @@ public final class ExternalDataPackPipeline { return Iris.instance.getDataFile(LOCATE_MANIFEST_PATH); } + private static File getObjectLocateManifestFile() { + return Iris.instance.getDataFile(OBJECT_LOCATE_MANIFEST_PATH); + } + private static String normalizeLocateId(String id) { if (id == null) { return ""; @@ -413,6 +464,57 @@ public final class ExternalDataPackPipeline { 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; @@ -432,6 +534,38 @@ public final class ExternalDataPackPipeline { } } + 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<>(); @@ -488,6 +622,62 @@ public final class ExternalDataPackPipeline { } } + 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 Map> readLocateManifest() { LinkedHashMap> mapped = new LinkedHashMap<>(); File input = getLocateManifestFile(); @@ -538,6 +728,56 @@ public final class ExternalDataPackPipeline { 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 List normalizeRequests(List requests) { Map deduplicated = new HashMap<>(); if (requests == null) { @@ -1885,7 +2125,19 @@ public final class ExternalDataPackPipeline { } } - private static File resolveSourceRoot(String targetPack, String sourceKey) { + 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(); @@ -1893,14 +2145,15 @@ public final class ExternalDataPackPipeline { return new File(Iris.instance.getDataFolder("packs", pack), "objects/" + IMPORT_PREFIX + "/" + sourceKey); } - private static SourceDescriptor createSourceDescriptor(File entry, String targetPack, String requiredEnvironment) { + 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 sourceHash = shortHash(entry.getAbsolutePath()); - String sourceKey = sanitized + "-" + sourceHash; + 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); @@ -1908,7 +2161,7 @@ public final class ExternalDataPackPipeline { if (pack.isEmpty()) { pack = defaultTargetPack(); } - return new SourceDescriptor(sourceKey, base, fingerprint, pack, normalizeEnvironment(requiredEnvironment)); + return new SourceDescriptor(sourceKey, base, fingerprint, pack, normalizeEnvironment(requiredEnvironment), objectRootKey); } private static String directoryFingerprint(File directory) { @@ -1939,12 +2192,14 @@ public final class ExternalDataPackPipeline { return files + ":" + size + ":" + latest; } - private static JSONObject convertSource(File entry, SourceDescriptor sourceDescriptor, File sourceRoot) { + 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.requiredEnvironment(), + sourceDescriptor.objectRootKey(), + requestId ); if (entry.isDirectory()) { convertDirectory(entry, conversion, sourceRoot); @@ -2085,6 +2340,11 @@ public final class ExternalDataPackPipeline { } private static void applyResult(SourceConversion conversion, ConversionResult result) { + if (result.skipped) { + conversion.skipped++; + return; + } + if (!result.success) { conversion.failed++; return; @@ -2107,19 +2367,21 @@ public final class ExternalDataPackPipeline { return ConversionResult.failed(); } + if (isEmptyStructure(compoundTag)) { + return ConversionResult.skipped(); + } + IrisObject object = toObject(compoundTag); if (object == null) { return ConversionResult.failed(); } String relative = objectKey; - if (relative.startsWith(IMPORT_PREFIX + "/")) { - relative = relative.substring((IMPORT_PREFIX + "/").length()); - int slash = relative.indexOf('/'); - if (slash >= 0 && slash + 1 < relative.length()) { - relative = relative.substring(slash + 1); - } + 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) { @@ -2139,6 +2401,32 @@ public final class ExternalDataPackPipeline { 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) { @@ -2548,7 +2836,11 @@ public final class ExternalDataPackPipeline { } } - deleteFolder(resolveSourceRoot(targetPack, sourceKey)); + String objectRootKey = source == null ? "" : normalizeObjectRootKey(source.optString("objectRootKey", "")); + if (!objectRootKey.isBlank()) { + deleteFolder(resolveSourceRoot(targetPack, objectRootKey)); + } + deleteFolder(resolveLegacySourceRoot(targetPack, sourceKey)); } } @@ -3363,7 +3655,14 @@ public final class ExternalDataPackPipeline { private record EntryPath(String originalPath, String namespace, String structurePath) { } - private record SourceDescriptor(String sourceKey, String sourceName, String fingerprint, String targetPack, String requiredEnvironment) { + 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) { @@ -3381,6 +3680,7 @@ public final class ExternalDataPackPipeline { 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; @@ -3390,11 +3690,20 @@ public final class ExternalDataPackPipeline { private int entitiesIgnored; private int blockEntities; - private SourceConversion(String sourceKey, String sourceName, String targetPack, String requiredEnvironment) { + 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; @@ -3411,8 +3720,12 @@ public final class ExternalDataPackPipeline { if (namespacePath.isEmpty() || structureValue.isEmpty()) { return null; } - String baseKey = IMPORT_PREFIX + "/" + sourceKey + "/" + namespacePath + "/" + structureValue; - return createUniqueKey(baseKey, usedKeys); + String baseKey = objectRootKey + "/" + structureValue; + if (usedKeys.add(baseKey)) { + return baseKey; + } + String namespacedKey = objectRootKey + "/" + namespacePath + "/" + structureValue; + return createUniqueKey(namespacedKey, usedKeys); } private JSONObject toJson(String fingerprint) { @@ -3420,6 +3733,7 @@ public final class ExternalDataPackPipeline { source.put("sourceKey", sourceKey); source.put("sourceName", sourceName); source.put("targetPack", targetPack); + source.put("objectRootKey", objectRootKey); if (requiredEnvironment != null) { source.put("requiredEnvironment", requiredEnvironment); } @@ -3480,23 +3794,29 @@ public final class ExternalDataPackPipeline { 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, JSONObject record, int blockEntities, 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, record, blockEntities, entitiesIgnored); + return new ConversionResult(true, false, record, blockEntities, entitiesIgnored); } private static ConversionResult failed() { - return new ConversionResult(false, null, 0, false); + 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/commands/CommandFind.java b/core/src/main/java/art/arcane/iris/core/commands/CommandFind.java index c2e84be64..1cc34c8ee 100644 --- a/core/src/main/java/art/arcane/iris/core/commands/CommandFind.java +++ b/core/src/main/java/art/arcane/iris/core/commands/CommandFind.java @@ -18,6 +18,7 @@ package art.arcane.iris.core.commands; +import art.arcane.iris.core.ExternalDataPackPipeline; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisRegion; @@ -27,6 +28,11 @@ 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.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 { @@ -94,6 +100,45 @@ public class CommandFind implements DirectorExecutor { return; } - e.gotoObject(object, player(), teleport); + if (e.hasObjectPlacement(object)) { + e.gotoObject(object, player(), teleport); + return; + } + + Set structures = ExternalDataPackPipeline.resolveLocateStructuresForObjectKey(object); + if (structures.isEmpty()) { + sender().sendMessage(C.RED + object + " is not configured in any region/biome object placements and has no external structure mapping."); + sender().sendMessage(C.GRAY + "Try /iris locateexternal for external structure lookups."); + return; + } + + Player target = player(); + if (target == null) { + sender().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) { + sender().sendMessage(C.RED + "Failed to dispatch: /" + command); + } else { + sender().sendMessage(C.GREEN + "Dispatched: /" + command); + dispatched++; + } + } + + if (teleport) { + sender().sendMessage(C.YELLOW + "External object lookups are structure-backed and dispatch locate commands instead of direct teleport."); + } + sender().sendMessage(C.GREEN + "External object mapping matched locateTargets=" + structures.size() + ", dispatched=" + dispatched + "."); + }; + + if (!J.runEntity(target, dispatchTask)) { + sender().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/tools/IrisCreator.java b/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java index 08dbfdfd1..9045ae490 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 @@ -20,6 +20,7 @@ package art.arcane.iris.core.tools; import com.google.common.util.concurrent.AtomicDouble; import art.arcane.iris.Iris; +import art.arcane.iris.core.IrisWorlds; import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.ServerConfigurator; import art.arcane.iris.core.link.FoliaWorldsLink; @@ -161,6 +162,9 @@ public class IrisCreator { .seed(seed) .studio(studio) .create(); + if (!studio()) { + IrisWorlds.get().put(name(), dimension()); + } boolean verifyDataPacks = !studio(); boolean includeExternalDataPacks = !studio(); if (ServerConfigurator.installDataPacks(verifyDataPacks, includeExternalDataPacks)) { 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 0f3e74435..778810b3b 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 @@ -81,6 +81,7 @@ import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.Nullable; import java.awt.Color; +import java.util.Locale; import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -944,6 +945,24 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat } } + default boolean hasObjectPlacement(String objectKey) { + String normalizedObjectKey = normalizeObjectPlacementKey(objectKey); + if (normalizedObjectKey.isBlank()) { + return false; + } + + Set biomeKeys = getDimension().getAllBiomes(this).stream() + .filter((i) -> containsObjectPlacement(i.getObjects(), normalizedObjectKey)) + .map(IrisRegistrant::getLoadKey) + .collect(Collectors.toSet()); + Set regionKeys = getDimension().getAllRegions(this).stream() + .filter((i) -> i.getAllBiomeIds().stream().anyMatch(biomeKeys::contains) + || containsObjectPlacement(i.getObjects(), normalizedObjectKey)) + .map(IrisRegistrant::getLoadKey) + .collect(Collectors.toSet()); + return !regionKeys.isEmpty(); + } + default void gotoRegion(IrisRegion r, Player player, boolean teleport) { if (!getDimension().getRegions().contains(r.getLoadKey())) { player.sendMessage(C.RED + r.getName() + " is not defined in the dimension!"); @@ -957,6 +976,45 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat Locator.poi(type).find(p, teleport, "POI " + type); } + private static boolean containsObjectPlacement(KList placements, String normalizedObjectKey) { + if (placements == null || placements.isEmpty() || normalizedObjectKey.isBlank()) { + return false; + } + + for (IrisObjectPlacement placement : placements) { + if (placement == null || placement.getPlace() == null || placement.getPlace().isEmpty()) { + continue; + } + + for (String placedObject : placement.getPlace()) { + String normalizedPlacedObject = normalizeObjectPlacementKey(placedObject); + if (!normalizedPlacedObject.isBlank() && normalizedPlacedObject.equals(normalizedObjectKey)) { + return true; + } + } + } + + return false; + } + + private static String normalizeObjectPlacementKey(String objectKey) { + if (objectKey == null) { + return ""; + } + + String normalized = objectKey.trim().replace('\\', '/'); + while (normalized.startsWith("/")) { + normalized = normalized.substring(1); + } + while (normalized.endsWith("/")) { + normalized = normalized.substring(0, normalized.length() - 1); + } + if (normalized.endsWith(".iob")) { + normalized = normalized.substring(0, normalized.length() - 4); + } + return normalized.toLowerCase(Locale.ROOT); + } + default void cleanupMantleChunk(int x, int z) { World world = getWorld().realWorld(); if (world != null && IrisToolbelt.isWorldMaintenanceActive(world)) {