From e6a8351e57128675ed55ee1bdc0adf2f602c42eb Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Fri, 17 Apr 2026 22:28:35 -0400 Subject: [PATCH] Bulk changes and fixes namely the chunk issue, and objects not wantingto place on cave tops. --- core/src/main/java/art/arcane/iris/Iris.java | 47 + .../iris/core/ExternalDataPackPipeline.java | 808 +++++++++-------- .../arcane/iris/core/ServerConfigurator.java | 72 +- .../iris/core/VanillaDatapackDumper.java | 153 ++++ .../iris/core/commands/CommandIris.java | 41 +- .../iris/core/commands/CommandPack.java | 164 ++++ .../iris/core/commands/CommandStudio.java | 2 +- .../iris/core/gui/NoiseExplorerGUI.java | 644 +++++++++----- .../art/arcane/iris/core/gui/VisionGUI.java | 815 +++++++++--------- .../core/gui/components/IrisRenderer.java | 164 ++-- .../art/arcane/iris/core/nms/INMSBinding.java | 5 + .../iris/core/pack/BrokenPackException.java | 56 ++ .../iris/core/pack/IrisPackRepository.java | 4 +- .../core/pack/PackValidationRegistry.java | 64 ++ .../iris/core/pack/PackValidationResult.java | 67 ++ .../arcane/iris/core/pack/PackValidator.java | 369 ++++++++ .../pregenerator/DeepSearchPregenerator.java | 15 +- .../core/pregenerator/IrisPregenerator.java | 65 +- .../iris/core/pregenerator/PregenTask.java | 126 +-- .../arcane/iris/core/project/IrisProject.java | 127 ++- .../runtime/WorldRuntimeControlService.java | 9 +- .../arcane/iris/core/service/StudioSVC.java | 99 ++- .../arcane/iris/core/tools/IrisCreator.java | 28 +- .../decorator/IrisCeilingDecorator.java | 12 + .../decorator/IrisSurfaceDecorator.java | 15 +- .../arcane/iris/engine/framework/Engine.java | 43 +- .../arcane/iris/engine/framework/Locator.java | 36 +- .../mantle/components/IrisCaveCarver3D.java | 128 +-- .../components/MantleCarvingComponent.java | 39 +- .../components/MantleObjectComponent.java | 132 ++- .../engine/modifier/IrisCarveModifier.java | 247 +++++- .../iris/engine/object/IrisDimension.java | 766 ++++++++-------- .../engine/object/IrisExternalDatapack.java | 30 +- .../object/IrisExternalDatapackBinding.java | 4 +- .../IrisExternalDatapackReplaceTargets.java | 54 -- .../IrisExternalDatapackStructureAlias.java | 20 - .../IrisExternalDatapackStructurePatch.java | 23 - ...IrisExternalDatapackStructureSetAlias.java | 20 - .../IrisExternalDatapackTemplateAlias.java | 23 - .../arcane/iris/engine/object/IrisObject.java | 110 ++- .../iris/engine/object/ObjectPlaceMode.java | 4 + .../engine/platform/BukkitChunkGenerator.java | 2 + .../iris/core/nms/v1_21_R7/NMSBinding.java | 103 +++ 43 files changed, 3575 insertions(+), 2180 deletions(-) create mode 100644 core/src/main/java/art/arcane/iris/core/VanillaDatapackDumper.java create mode 100644 core/src/main/java/art/arcane/iris/core/commands/CommandPack.java create mode 100644 core/src/main/java/art/arcane/iris/core/pack/BrokenPackException.java create mode 100644 core/src/main/java/art/arcane/iris/core/pack/PackValidationRegistry.java create mode 100644 core/src/main/java/art/arcane/iris/core/pack/PackValidationResult.java create mode 100644 core/src/main/java/art/arcane/iris/core/pack/PackValidator.java delete mode 100644 core/src/main/java/art/arcane/iris/engine/object/IrisExternalDatapackReplaceTargets.java delete mode 100644 core/src/main/java/art/arcane/iris/engine/object/IrisExternalDatapackStructureAlias.java delete mode 100644 core/src/main/java/art/arcane/iris/engine/object/IrisExternalDatapackStructurePatch.java delete mode 100644 core/src/main/java/art/arcane/iris/engine/object/IrisExternalDatapackStructureSetAlias.java delete mode 100644 core/src/main/java/art/arcane/iris/engine/object/IrisExternalDatapackTemplateAlias.java diff --git a/core/src/main/java/art/arcane/iris/Iris.java b/core/src/main/java/art/arcane/iris/Iris.java index dd1f26aab..4d80d9022 100644 --- a/core/src/main/java/art/arcane/iris/Iris.java +++ b/core/src/main/java/art/arcane/iris/Iris.java @@ -32,6 +32,10 @@ import art.arcane.iris.core.link.IrisPapiExpansion; import art.arcane.iris.core.link.MultiverseCoreLink; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.nms.INMS; +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.service.StudioSVC; import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.engine.EnginePanic; @@ -537,6 +541,7 @@ public class Iris extends VolmitPlugin implements Listener { IO.delete(new File("iris")); compat = IrisCompat.configured(getDataFile("compat.json")); ServerConfigurator.configure(); + validateAllPacks(); IrisSafeguard.execute(); getSender().setTag(getTag()); IrisSafeguard.splash(); @@ -1008,6 +1013,16 @@ public class Iris extends VolmitPlugin implements Listener { Iris.debug("Default World Generator Called for " + worldName + " using ID: " + id); if (id == null || id.isEmpty()) id = IrisSettings.get().getGenerator().getDefaultWorldType(); Iris.debug("Generator ID: " + id + " requested by bukkit/plugin"); + + PackValidationResult validation = PackValidationRegistry.get(id); + if (validation != null && !validation.isLoadable()) { + Iris.error("Refusing to create world '" + worldName + "' using broken pack '" + id + "':"); + for (String reason : validation.getBlockingErrors()) { + Iris.error(" - " + reason); + } + throw new BrokenPackException(id, validation.getBlockingErrors()); + } + IrisDimension dim = loadDimension(worldName, id); if (dim == null) { throw new RuntimeException("Can't find dimension " + id + "!"); @@ -1039,6 +1054,38 @@ public class Iris extends VolmitPlugin implements Listener { return new BukkitChunkGenerator(w, false, ff, dim.getLoadKey()); } + public static void validateAllPacks() { + File packsRoot = Iris.instance.getDataFolder("packs"); + File[] packDirs = packsRoot.listFiles(File::isDirectory); + if (packDirs == null || packDirs.length == 0) { + return; + } + PackValidationRegistry.clear(); + for (File packDir : packDirs) { + try { + PackValidationResult result = PackValidator.validate(packDir); + PackValidationRegistry.publish(result); + if (!result.isLoadable()) { + Iris.error("Pack '" + result.getPackName() + "' FAILED validation - world/studio creation will be refused. Reasons:"); + for (String reason : result.getBlockingErrors()) { + Iris.error(" - " + reason); + } + } else if (!result.getWarnings().isEmpty() || !result.getRemovedUnusedFiles().isEmpty()) { + Iris.info("Pack '" + result.getPackName() + "' validated (" + + result.getRemovedUnusedFiles().size() + " unused file(s) quarantined to .iris-trash/, " + + result.getWarnings().size() + " warning(s))."); + for (String warning : result.getWarnings()) { + Iris.warn(" [" + result.getPackName() + "] " + warning); + } + } else { + Iris.success("Pack '" + result.getPackName() + "' validated."); + } + } catch (Throwable e) { + Iris.reportError("Pack validation failed for '" + packDir.getName() + "'", e); + } + } + } + @Nullable public static IrisDimension loadDimension(@NonNull String worldName, @NonNull String id) { File pack = new File(Bukkit.getWorldContainer(), String.join(File.separator, worldName, "iris", "pack")); 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 b4108a53c..587a8ca69 100644 --- a/core/src/main/java/art/arcane/iris/core/ExternalDataPackPipeline.java +++ b/core/src/main/java/art/arcane/iris/core/ExternalDataPackPipeline.java @@ -8,11 +8,6 @@ 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.IrisExternalDatapackReplaceTargets; -import art.arcane.iris.engine.object.IrisExternalDatapackStructureAlias; -import art.arcane.iris.engine.object.IrisExternalDatapackStructureSetAlias; -import art.arcane.iris.engine.object.IrisExternalDatapackStructurePatch; -import art.arcane.iris.engine.object.IrisExternalDatapackTemplateAlias; import art.arcane.iris.engine.object.TileData; import art.arcane.iris.util.common.data.B; import art.arcane.iris.util.common.math.Vector3i; @@ -90,8 +85,11 @@ public final class ExternalDataPackPipeline { 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"; @@ -270,6 +268,19 @@ public final class ExternalDataPackPipeline { 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<>(); @@ -281,14 +292,34 @@ public final class ExternalDataPackPipeline { } if (request.replaceVanilla() && !request.hasReplacementTargets()) { - if (request.required()) { - summary.requiredFailures++; - } else { - summary.optionalFailures++; + 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; } - Iris.warn("Downloading datapacks [" + (requestIndex + 1) + "/" + normalizedRequests.size() + "] Failed! id=" + request.id() + " (replaceVanilla requires explicit replacement targets)."); - mergeResolvedLocateStructures(resolvedLocateStructuresById, request.id(), request.resolvedLocateStructures()); - continue; } RequestSyncResult syncResult = syncRequest(request); @@ -313,7 +344,7 @@ public final class ExternalDataPackPipeline { sourceInputs.add(new RequestedSourceInput(syncResult.source(), request)); } - if (sourceInputs.isEmpty()) { + if (sourceInputs.isEmpty() && summary.skippedExistingRequests == 0) { if (summary.requiredFailures == 0) { summary.legacyWorldCopyRemovals += pruneManagedWorldDatapacks(knownWorldDatapackFolders, Set.of()); } @@ -326,17 +357,6 @@ public final class ExternalDataPackPipeline { return summary; } - File importPackFolder = Iris.instance.getDataFolder("packs", EXTERNAL_PACK_INDEX); - File indexFile = new File(importPackFolder, "datapack-index.json"); - importPackFolder.mkdirs(); - - JSONObject oldIndex = readExistingIndex(indexFile); - Map oldSources = mapExistingSources(oldIndex); - JSONArray newSources = new JSONArray(); - Set seenSourceKeys = new HashSet<>(); - Set activeManagedWorldDatapackNames = new HashSet<>(); - ImportSummary importSummary = new ImportSummary(); - for (int sourceIndex = 0; sourceIndex < sourceInputs.size(); sourceIndex++) { RequestedSourceInput sourceInput = sourceInputs.get(sourceIndex); File entry = sourceInput.source(); @@ -373,13 +393,16 @@ public final class ExternalDataPackPipeline { 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(importSummary, cachedSource, true); + addSourceToSummary(packImportSummary, cachedSource, true); activeSource = cachedSource; } else { if (cachedTargetPack != null && cachedSource != null) { @@ -394,7 +417,7 @@ public final class ExternalDataPackPipeline { sourceRoot.mkdirs(); JSONObject sourceResult = convertSource(entry, sourceDescriptor, sourceRoot, request.id()); newSources.put(sourceResult); - addSourceToSummary(importSummary, sourceResult, false); + addSourceToSummary(packImportSummary, sourceResult, false); activeSource = sourceResult; int conversionFailed = sourceResult.optInt("failed", 0); if (conversionFailed > 0) { @@ -465,8 +488,23 @@ public final class ExternalDataPackPipeline { } pruneRemovedSourceFolders(oldSources, seenSourceKeys); - writeIndex(indexFile, newSources, importSummary); - summary.setImportSummary(importSummary); + 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); } @@ -1153,6 +1191,163 @@ public final class ExternalDataPackPipeline { 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()) { @@ -1244,13 +1439,10 @@ public final class ExternalDataPackPipeline { File managedFolder = new File(worldDatapackFolder, baseManagedName); File managedZip = new File(worldDatapackFolder, managedName); deleteFolder(managedFolder); - if (managedZip.exists() && !managedZip.delete()) { - throw new IOException("failed to replace managed external datapack zip " + managedZip.getPath()); - } int copiedAssets = writeProjectedAssets(managedZip, projectionAssetSummary.assets()); if (copiedAssets <= 0) { if (managedZip.exists() && !managedZip.delete()) { - Iris.warn("Failed to remove empty managed external datapack zip " + managedZip.getPath()); + Iris.verbose("Unable to remove empty managed external datapack zip " + managedZip.getPath() + " (likely locked by the running server)."); } continue; } @@ -1283,69 +1475,30 @@ public final class ExternalDataPackPipeline { private static ProjectionAssetSummary buildProjectedAssets(File source, SourceDescriptor sourceDescriptor, DatapackRequest request) throws IOException { ProjectionSelection projectionSelection = readProjectedEntries(source, request); - if (!projectionSelection.missingSeededTargets().isEmpty()) { - throw new IOException("Strict replace validation missing target(s): " + summarizeMissingSeededTargets(projectionSelection.missingSeededTargets())); - } List inputAssets = projectionSelection.assets(); if (inputAssets.isEmpty()) { return new ProjectionAssetSummary(List.of(), Set.copyOf(request.resolvedLocateStructures()), 0, Set.of(), 0, 0, 0); } - int selectedStructureNbtCount = 0; - for (ProjectionInputAsset inputAsset : inputAssets) { - if (inputAsset == null || inputAsset.entry() == null) { - continue; - } - if (inputAsset.entry().type() == ProjectedEntryType.STRUCTURE_NBT) { - selectedStructureNbtCount++; - } - } String scopeNamespace = buildScopeNamespace(sourceDescriptor, request); - LinkedHashMap remappedKeys = new LinkedHashMap<>(); - for (ProjectionInputAsset inputAsset : inputAssets) { - if (inputAsset.entry().namespace().equals("minecraft") && request.alongsideMode()) { - String remappedKey = scopeNamespace + ":" + extractPathFromKey(inputAsset.entry().key()); - remappedKeys.put(inputAsset.entry().key(), remappedKey); - } - } - - LinkedHashMap remapStringValues = new LinkedHashMap<>(); - for (Map.Entry entry : remappedKeys.entrySet()) { - remapStringValues.put(entry.getKey(), entry.getValue()); - remapStringValues.put("#" + entry.getKey(), "#" + entry.getValue()); - } - LinkedHashMap> scopedTagValues = new LinkedHashMap<>(); LinkedHashSet resolvedLocateStructures = new LinkedHashSet<>(); resolvedLocateStructures.addAll(request.resolvedLocateStructures()); - LinkedHashSet remappedStructureKeys = new LinkedHashSet<>(); LinkedHashSet projectedStructureKeys = new LinkedHashSet<>(); - LinkedHashSet structureSetReferences = new LinkedHashSet<>(); - int templateAliasesApplied = 0; - int emptyElementConversions = 0; - LinkedHashSet unresolvedTemplateRefs = new LinkedHashSet<>(); LinkedHashSet writtenPaths = new LinkedHashSet<>(); ArrayList outputAssets = new ArrayList<>(); int projectedCanonicalStructureNbtCount = 0; for (ProjectionInputAsset inputAsset : inputAssets) { ProjectedEntry projectedEntry = inputAsset.entry(); - String remappedKey = remappedKeys.get(projectedEntry.key()); - ProjectedEntry effectiveEntry = remappedKey == null - ? projectedEntry - : new ProjectedEntry(projectedEntry.type(), extractNamespaceFromKey(remappedKey), remappedKey); - - String outputRelativePath = buildProjectedPath(effectiveEntry); + String outputRelativePath = buildProjectedPath(projectedEntry); if (outputRelativePath == null || writtenPaths.contains(outputRelativePath)) { continue; } writtenPaths.add(outputRelativePath); byte[] outputBytes = inputAsset.bytes(); - if (projectedEntry.type() == ProjectedEntryType.STRUCTURE_NBT && !remappedKeys.isEmpty()) { - outputBytes = StructureNbtJigsawPoolRewriter.rewrite(outputBytes, remappedKeys); - } if (projectedEntry.type() == ProjectedEntryType.STRUCTURE || projectedEntry.type() == ProjectedEntryType.STRUCTURE_SET || projectedEntry.type() == ProjectedEntryType.CONFIGURED_FEATURE @@ -1354,42 +1507,20 @@ public final class ExternalDataPackPipeline { || projectedEntry.type() == ProjectedEntryType.PROCESSOR_LIST || projectedEntry.type() == ProjectedEntryType.BIOME_HAS_STRUCTURE_TAG) { JSONObject root = new JSONObject(new String(outputBytes, StandardCharsets.UTF_8)); - rewriteJsonValues(root, remapStringValues); - - if (projectedEntry.type() == ProjectedEntryType.TEMPLATE_POOL && !request.templateAliases().isEmpty()) { - TemplateAliasRewriteResult templateAliasRewriteResult = applyTemplateAliasesToTemplatePool(root, request.templateAliases()); - templateAliasesApplied += templateAliasRewriteResult.appliedCount(); - emptyElementConversions += templateAliasRewriteResult.emptyConversions(); - unresolvedTemplateRefs.addAll(templateAliasRewriteResult.unresolvedReferences()); - } if (projectedEntry.type() == ProjectedEntryType.STRUCTURE) { - Integer startHeightAbsolute = getPatchedStartHeightAbsolute(projectedEntry, request); - if (startHeightAbsolute == null && remappedKey != null) { - startHeightAbsolute = request.structureStartHeights().get(remappedKey); - } - - if (startHeightAbsolute != null) { - JSONObject startHeight = new JSONObject(); - startHeight.put("absolute", startHeightAbsolute); - root.put("start_height", startHeight); - } - if (!request.forcedBiomeKeys().isEmpty()) { - String scopeTagKey = scopeNamespace + ":has_structure/" + extractPathFromKey(effectiveEntry.key()); + String scopeTagKey = scopeNamespace + ":has_structure/" + extractPathFromKey(projectedEntry.key()); root.put("biomes", "#" + scopeTagKey); KList values = scopedTagValues.computeIfAbsent(scopeTagKey, key -> new KList<>()); values.addAll(request.forcedBiomeKeys()); } - remappedStructureKeys.add(effectiveEntry.key()); - resolvedLocateStructures.add(effectiveEntry.key()); - String normalizedProjectedStructure = normalizeLocateStructure(effectiveEntry.key()); + resolvedLocateStructures.add(projectedEntry.key()); + String normalizedProjectedStructure = normalizeLocateStructure(projectedEntry.key()); if (!normalizedProjectedStructure.isBlank()) { projectedStructureKeys.add(normalizedProjectedStructure); } - } else if (projectedEntry.type() == ProjectedEntryType.STRUCTURE_SET) { - structureSetReferences.addAll(readStructureSetReferences(root)); } outputBytes = root.toString(4).getBytes(StandardCharsets.UTF_8); @@ -1403,19 +1534,6 @@ public final class ExternalDataPackPipeline { } } - int syntheticStructureSets = 0; - if (request.alongsideMode()) { - SyntheticStructureSetResult syntheticResult = synthesizeMissingStructureSets( - remappedStructureKeys, - structureSetReferences, - remappedKeys, - scopeNamespace, - writtenPaths - ); - outputAssets.addAll(syntheticResult.assets()); - syntheticStructureSets += syntheticResult.count(); - } - for (Map.Entry> scopedEntry : scopedTagValues.entrySet()) { String tagKey = scopedEntry.getKey(); String tagPath = "data/" + extractNamespaceFromKey(tagKey) + "/tags/worldgen/biome/" + extractPathFromKey(tagKey) + ".json"; @@ -1436,52 +1554,14 @@ public final class ExternalDataPackPipeline { outputAssets.add(new ProjectionOutputAsset(tagPath, root.toString(4).getBytes(StandardCharsets.UTF_8))); } - if (request.required() && selectedStructureNbtCount > 0 && projectedCanonicalStructureNbtCount <= 0) { - throw new IOException("Required external datapack projection produced no canonical structure template outputs (data/*/structure/*.nbt)."); - } - - if (request.required() && !unresolvedTemplateRefs.isEmpty()) { - throw new IOException("Required external datapack template alias rewrite unresolved reference(s): " - + summarizeMissingSeededTargets(unresolvedTemplateRefs)); - } - - if (request.replaceVanilla() && !request.alongsideMode()) { - int directTargetCount = projectionSelection.directResolvedTargets().size(); - int aliasTargetCount = projectionSelection.aliasResolvedTargets().size(); - int projectedStructureCount = projectedStructureKeys.size(); - int projectedTemplateCount = projectedCanonicalStructureNbtCount; - int unresolvedTemplateRefCount = unresolvedTemplateRefs.size(); - if (request.required()) { - Iris.info("External datapack strict replace validation: id=" + request.id() - + ", directTargets=" + directTargetCount - + ", aliasTargets=" + aliasTargetCount - + ", projectedStructures=" + projectedStructureCount - + ", templates=" + projectedTemplateCount - + ", templateAliasesApplied=" + templateAliasesApplied - + ", emptyElementConversions=" + emptyElementConversions - + ", unresolvedTemplateRefs=" + unresolvedTemplateRefCount - + ", missingTargets=0"); - } else { - Iris.verbose("External datapack strict replace validation: id=" + request.id() - + ", directTargets=" + directTargetCount - + ", aliasTargets=" + aliasTargetCount - + ", projectedStructures=" + projectedStructureCount - + ", templates=" + projectedTemplateCount - + ", templateAliasesApplied=" + templateAliasesApplied - + ", emptyElementConversions=" + emptyElementConversions - + ", unresolvedTemplateRefs=" + unresolvedTemplateRefCount - + ", missingTargets=0"); - } - } - return new ProjectionAssetSummary( outputAssets, Set.copyOf(resolvedLocateStructures), - syntheticStructureSets, + 0, Set.copyOf(projectedStructureKeys), - templateAliasesApplied, - emptyElementConversions, - unresolvedTemplateRefs.size() + 0, + 0, + 0 ); } @@ -1567,10 +1647,6 @@ public final class ExternalDataPackPipeline { return ProjectionSelection.empty(); } - if (!request.alongsideMode() && request.replaceVanilla()) { - return selectReplaceVanillaEntries(inputAssets, request); - } - ArrayList selected = new ArrayList<>(); for (ProjectionInputAsset asset : inputAssets) { if (asset == null) { @@ -2458,12 +2534,57 @@ public final class ExternalDataPackPipeline { 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()))) { - byte[] packMetaBytes = buildManagedPackMetaBytes(); ZipEntry packMetaEntry = new ZipEntry("pack.mcmeta"); + packMetaEntry.setTime(MANAGED_ZIP_ENTRY_TIME); zipOutputStream.putNextEntry(packMetaEntry); - zipOutputStream.write(packMetaBytes); + zipOutputStream.write(buildManagedPackMetaBytes()); zipOutputStream.closeEntry(); for (ProjectionOutputAsset asset : assets) { @@ -2477,22 +2598,25 @@ public final class ExternalDataPackPipeline { } ZipEntry zipEntry = new ZipEntry(relativePath); + zipEntry.setTime(MANAGED_ZIP_ENTRY_TIME); zipOutputStream.putNextEntry(zipEntry); zipOutputStream.write(asset.bytes()); zipOutputStream.closeEntry(); copied++; } } - - try { - Files.move(temp.toPath(), managedZipFile.toPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); - } catch (IOException e) { - Files.move(temp.toPath(), managedZipFile.toPath(), StandardCopyOption.REPLACE_EXISTING); - } - 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) { @@ -2879,28 +3003,7 @@ public final class ExternalDataPackPipeline { return true; } - if (request.alongsideMode()) { - return true; - } - - if (!request.replaceVanilla()) { - return false; - } - - if (!request.hasReplacementTargets()) { - return false; - } - - return switch (entry.type()) { - case STRUCTURE -> request.structures().contains(entry.key()); - case STRUCTURE_SET -> request.structureSets().contains(entry.key()); - case CONFIGURED_FEATURE -> request.configuredFeatures().contains(entry.key()); - case PLACED_FEATURE -> request.placedFeatures().contains(entry.key()); - case TEMPLATE_POOL -> request.templatePools().contains(entry.key()); - case PROCESSOR_LIST -> request.processorLists().contains(entry.key()); - case BIOME_HAS_STRUCTURE_TAG -> request.biomeHasStructureTags().contains(entry.key()); - case STRUCTURE_NBT -> request.structures().contains(entry.key()) || !request.templatePools().isEmpty(); - }; + return request.replaceVanilla(); } private static ProjectedEntry parseProjectedEntry(String relativePath) { @@ -3819,6 +3922,126 @@ public final class ExternalDataPackPipeline { } } + 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) { @@ -4152,10 +4375,27 @@ public final class ExternalDataPackPipeline { 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.delete(); + 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; } - Files.copy(cachedZip.toPath(), managedZip.toPath(), StandardCopyOption.REPLACE_EXISTING); if (managedZip.exists() && managedZip.length() > 0) { installedDatapacks++; installedAssets += meta.optInt("installedAssets", 0); @@ -4261,50 +4501,8 @@ public final class ExternalDataPackPipeline { String requiredEnvironment, boolean required, boolean replaceVanilla, - boolean supportSmartBore, - IrisExternalDatapackReplaceTargets replaceTargets, - KList structureAliases, - KList structureSetAliases, - KList templateAliases, - KList structurePatches - ) { - this( - id, - url, - targetPack, - requiredEnvironment, - required, - replaceVanilla, - supportSmartBore, - replaceTargets, - structureAliases, - structureSetAliases, - templateAliases, - structurePatches, - Set.of(), - "dimension-root", - !replaceVanilla, - Set.of() - ); - } - - public DatapackRequest( - String id, - String url, - String targetPack, - String requiredEnvironment, - boolean required, - boolean replaceVanilla, - boolean supportSmartBore, - IrisExternalDatapackReplaceTargets replaceTargets, - KList structureAliases, - KList structureSetAliases, - KList templateAliases, - KList structurePatches, Set forcedBiomeKeys, - String scopeKey, - boolean alongsideMode, - Set resolvedLocateStructures + String scopeKey ) { this( normalizeRequestId(id, url), @@ -4313,25 +4511,22 @@ public final class ExternalDataPackPipeline { normalizeEnvironment(requiredEnvironment), required, replaceVanilla, - supportSmartBore, - normalizeTargets(replaceTargets == null ? null : replaceTargets.getStructures(), "worldgen/structure/"), - normalizeTargets(replaceTargets == null ? null : replaceTargets.getStructureSets(), "worldgen/structure_set/"), - normalizeStructureAliases(structureAliases), - normalizeStructureSetAliases(structureSetAliases), - normalizeTemplateAliases(templateAliases), - normalizeTargets(replaceTargets == null ? null : replaceTargets.getConfiguredFeatures(), "worldgen/configured_feature/"), - normalizeTargets(replaceTargets == null ? null : replaceTargets.getPlacedFeatures(), "worldgen/placed_feature/"), - normalizeTargets(replaceTargets == null ? null : replaceTargets.getTemplatePools(), "worldgen/template_pool/"), - normalizeTargets(replaceTargets == null ? null : replaceTargets.getProcessorLists(), "worldgen/processor_list/"), - normalizeTargets(replaceTargets == null ? null : replaceTargets.getBiomeHasStructureTags(), - "tags/worldgen/biome/has_structure/", - "worldgen/biome/has_structure/", - "has_structure/"), - normalizeStructureStartHeights(structurePatches), + 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), - alongsideMode, - normalizeLocateStructures(resolvedLocateStructures, replaceTargets == null ? null : replaceTargets.getStructures()) + !replaceVanilla, + Set.of() ); } @@ -4474,87 +4669,6 @@ public final class ExternalDataPackPipeline { return normalized; } - private static Map> normalizeStructureAliases(KList aliases) { - LinkedHashMap> normalized = new LinkedHashMap<>(); - if (aliases == null) { - return normalized; - } - - for (IrisExternalDatapackStructureAlias alias : aliases) { - if (alias == null) { - continue; - } - - String target = normalizeResourceKey("minecraft", alias.getTarget(), "worldgen/structure/"); - String source = normalizeResourceKey("minecraft", alias.getSource(), "worldgen/structure/"); - if (target == null || target.isBlank() || source == null || source.isBlank()) { - continue; - } - List targetSources = normalized.computeIfAbsent(target, key -> new ArrayList<>()); - if (!targetSources.contains(source)) { - targetSources.add(source); - } - } - - return normalized; - } - - private static Map normalizeStructureSetAliases(KList aliases) { - LinkedHashMap normalized = new LinkedHashMap<>(); - if (aliases == null) { - return normalized; - } - - for (IrisExternalDatapackStructureSetAlias alias : aliases) { - if (alias == null) { - continue; - } - - String target = normalizeResourceKey("minecraft", alias.getTarget(), "worldgen/structure_set/"); - String source = normalizeResourceKey("minecraft", alias.getSource(), "worldgen/structure_set/"); - if (target == null || target.isBlank() || source == null || source.isBlank()) { - continue; - } - normalized.put(target, source); - } - - return normalized; - } - - private static Map normalizeTemplateAliases(KList aliases) { - LinkedHashMap normalized = new LinkedHashMap<>(); - if (aliases == null) { - return normalized; - } - - for (IrisExternalDatapackTemplateAlias alias : aliases) { - if (alias == null || !alias.isEnabled()) { - continue; - } - - String from = normalizeResourceKey("minecraft", alias.getFrom(), "structure/", "structures/"); - if (from == null || from.isBlank()) { - continue; - } - - String toRaw = alias.getTo() == null ? "" : alias.getTo().trim(); - String to; - if ("minecraft:empty".equalsIgnoreCase(toRaw)) { - to = "minecraft:empty"; - } else { - to = normalizeResourceKey("minecraft", toRaw, "structure/", "structures/"); - } - - if (to == null || to.isBlank()) { - continue; - } - - normalized.put(from, to); - } - - return normalized; - } - private static Set immutableSet(Set values) { LinkedHashSet copy = new LinkedHashSet<>(); if (values != null) { @@ -4705,33 +4819,6 @@ public final class ExternalDataPackPipeline { return merged; } - private static Map normalizeStructureStartHeights(KList patches) { - LinkedHashMap normalized = new LinkedHashMap<>(); - if (patches == null) { - return normalized; - } - - for (IrisExternalDatapackStructurePatch patch : patches) { - if (patch == null || !patch.isEnabled()) { - continue; - } - - String structure = patch.getStructure(); - if (structure == null || structure.isBlank()) { - continue; - } - - String normalizedStructure = normalizeResourceKey("minecraft", structure, "worldgen/structure/"); - if (normalizedStructure == null || normalizedStructure.isBlank()) { - continue; - } - - normalized.put(normalizedStructure, patch.getStartHeightAbsolute()); - } - - return normalized; - } - private static Map unionStructureStartHeights(Map first, Map second) { LinkedHashMap merged = new LinkedHashMap<>(); if (first != null) { @@ -4765,6 +4852,7 @@ public final class ExternalDataPackPipeline { private int requests; private int syncedRequests; private int restoredRequests; + private int skippedExistingRequests; private int optionalFailures; private int requiredFailures; private int importedSources; @@ -4806,6 +4894,10 @@ public final class ExternalDataPackPipeline { return restoredRequests; } + public int getSkippedExistingRequests() { + return skippedExistingRequests; + } + public int getOptionalFailures() { return optionalFailures; } 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 2111dd4ea..f95f8a94e 100644 --- a/core/src/main/java/art/arcane/iris/core/ServerConfigurator.java +++ b/core/src/main/java/art/arcane/iris/core/ServerConfigurator.java @@ -185,6 +185,13 @@ public class ServerConfigurator { 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); } @@ -254,6 +261,9 @@ public class ServerConfigurator { 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) { @@ -261,6 +271,28 @@ public class ServerConfigurator { } } + 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()) { @@ -370,17 +402,9 @@ public class ServerConfigurator { targetPack, environment, definition.isRequired(), - definition.isReplaceVanilla(), - definition.isSupportSmartBore(), - definition.getReplaceTargets(), - definition.getStructureAliases(), - definition.getStructureSetAliases(), - definition.getTemplateAliases(), - definition.getStructurePatches(), + definition.isReplace(), Set.of(), - scopeKey, - !definition.isReplaceVanilla(), - Set.of() + scopeKey ); dedupeMerges += mergeDeduplicatedRequest(deduplicated, request); unscopedRequests++; @@ -389,8 +413,7 @@ public class ServerConfigurator { + ", dimension=" + dimension.getLoadKey() + ", scope=dimension-root" + ", forcedBiomes=0" - + ", replaceVanilla=" + definition.isReplaceVanilla() - + ", alongsideMode=" + (!definition.isReplaceVanilla()) + + ", replace=" + definition.isReplace() + ", required=" + definition.isRequired()); continue; } @@ -403,16 +426,8 @@ public class ServerConfigurator { environment, group.required(), group.replaceVanilla(), - definition.isSupportSmartBore(), - definition.getReplaceTargets(), - definition.getStructureAliases(), - definition.getStructureSetAliases(), - definition.getTemplateAliases(), - definition.getStructurePatches(), group.forcedBiomeKeys(), - group.scopeKey(), - !group.replaceVanilla(), - Set.of() + group.scopeKey() ); dedupeMerges += mergeDeduplicatedRequest(deduplicated, request); scopedRequests++; @@ -421,8 +436,7 @@ public class ServerConfigurator { + ", dimension=" + dimension.getLoadKey() + ", scope=" + group.source() + ", forcedBiomes=" + group.forcedBiomeKeys().size() - + ", replaceVanilla=" + group.replaceVanilla() - + ", alongsideMode=" + (!group.replaceVanilla()) + + ", replace=" + group.replaceVanilla() + ", required=" + group.required()); } } @@ -507,9 +521,9 @@ public class ServerConfigurator { continue; } - boolean replaceVanilla = binding.getReplaceVanillaOverride() == null - ? definition.isReplaceVanilla() - : binding.getReplaceVanillaOverride(); + boolean replaceVanilla = binding.getReplaceOverride() == null + ? definition.isReplace() + : binding.getReplaceOverride(); boolean required = binding.getRequiredOverride() == null ? definition.isRequired() : binding.getRequiredOverride(); @@ -551,9 +565,9 @@ public class ServerConfigurator { continue; } - boolean replaceVanilla = binding.getReplaceVanillaOverride() == null - ? definition.isReplaceVanilla() - : binding.getReplaceVanillaOverride(); + boolean replaceVanilla = binding.getReplaceOverride() == null + ? definition.isReplace() + : binding.getReplaceOverride(); boolean required = binding.getRequiredOverride() == null ? definition.isRequired() : binding.getRequiredOverride(); diff --git a/core/src/main/java/art/arcane/iris/core/VanillaDatapackDumper.java b/core/src/main/java/art/arcane/iris/core/VanillaDatapackDumper.java new file mode 100644 index 000000000..cee6c510a --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/VanillaDatapackDumper.java @@ -0,0 +1,153 @@ +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/CommandIris.java b/core/src/main/java/art/arcane/iris/core/commands/CommandIris.java index e9518d4c4..9bfcce437 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 @@ -30,7 +30,6 @@ 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.object.IrisExternalDatapackReplaceTargets; import art.arcane.iris.engine.platform.ChunkReplacementListener; import art.arcane.iris.engine.platform.ChunkReplacementOptions; import art.arcane.iris.engine.platform.PlatformChunkGenerator; @@ -99,6 +98,7 @@ public class CommandIris implements DirectorExecutor { private CommandEdit edit; private CommandFind find; private CommandDeveloper developer; + private CommandPack pack; public static boolean worldCreation = false; private static final AtomicReference mainWorld = new AtomicReference<>(); String WorldEngine; @@ -591,42 +591,7 @@ public class CommandIris implements DirectorExecutor { } private static Map> buildExternalLocateFallbackById(KList externalDatapacks) { - Map> mapped = new ConcurrentHashMap<>(); - if (externalDatapacks == null || externalDatapacks.isEmpty()) { - return mapped; - } - - for (IrisExternalDatapack externalDatapack : externalDatapacks) { - if (externalDatapack == null) { - continue; - } - - String url = externalDatapack.getUrl() == null ? "" : externalDatapack.getUrl().trim(); - String entryId = externalDatapack.getId() == null ? "" : externalDatapack.getId().trim(); - String normalizedEntryId = normalizeLocateExternalToken(entryId.isBlank() ? url : entryId); - if (normalizedEntryId.isBlank()) { - continue; - } - - IrisExternalDatapackReplaceTargets replaceTargets = externalDatapack.getReplaceTargets(); - if (replaceTargets == null || replaceTargets.getStructures() == null || replaceTargets.getStructures().isEmpty()) { - continue; - } - - LinkedHashSet structures = new LinkedHashSet<>(); - for (String structure : replaceTargets.getStructures()) { - String normalizedStructure = normalizeLocateStructureToken(structure); - if (!normalizedStructure.isBlank()) { - structures.add(normalizedStructure); - } - } - - if (!structures.isEmpty()) { - mapped.put(normalizedEntryId, Set.copyOf(structures)); - } - } - - return mapped; + return new ConcurrentHashMap<>(); } private static String normalizeLocateStructureToken(String structure) { @@ -803,7 +768,7 @@ public class CommandIris implements DirectorExecutor { 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 = "main") + @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, 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 new file mode 100644 index 000000000..888098307 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/commands/CommandPack.java @@ -0,0 +1,164 @@ +/* + * 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.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.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.io.File; + +@Director(name = "pack", aliases = {"pk"}, description = "Pack validation and maintenance") +public class CommandPack implements DirectorExecutor { + + @Director(description = "Validate a pack (or all packs) and re-publish results", aliases = {"v", "check"}) + public void validate( + @Param(description = "The pack folder name to validate (leave empty for all)", defaultValue = "") + String pack + ) { + VolmitSender s = sender(); + File packsRoot = Iris.instance.getDataFolder("packs"); + if (!packsRoot.isDirectory()) { + s.sendMessage(C.RED + "packs/ folder not found."); + return; + } + + if (pack == null || pack.isBlank()) { + File[] dirs = packsRoot.listFiles(File::isDirectory); + if (dirs == null || dirs.length == 0) { + s.sendMessage(C.YELLOW + "No packs to validate."); + return; + } + int broken = 0; + for (File dir : dirs) { + PackValidationResult result = runValidate(s, dir); + if (result != null && !result.isLoadable()) { + broken++; + } + } + s.sendMessage(C.GREEN + "Validation complete. Broken packs: " + broken + "/" + dirs.length); + return; + } + + File target = new File(packsRoot, pack); + if (!target.isDirectory()) { + s.sendMessage(C.RED + "Pack '" + pack + "' not found under packs/."); + return; + } + runValidate(s, target); + } + + @Director(description = "Restore most recent trashed files for a pack", aliases = {"r", "undelete"}) + public void restore( + @Param(description = "The pack folder name to restore") + String pack + ) { + VolmitSender s = sender(); + if (pack == null || pack.isBlank()) { + s.sendMessage(C.RED + "You must specify a pack name."); + return; + } + File packFolder = new File(Iris.instance.getDataFolder("packs"), pack); + if (!packFolder.isDirectory()) { + s.sendMessage(C.RED + "Pack '" + pack + "' not found under packs/."); + return; + } + int restored = PackValidator.restoreTrash(packFolder); + if (restored == 0) { + s.sendMessage(C.YELLOW + "Nothing to restore for pack '" + pack + "'."); + return; + } + s.sendMessage(C.GREEN + "Restored " + restored + " file(s) from the most recent trash dump for pack '" + pack + "'."); + s.sendMessage(C.GRAY + "Re-run /iris pack validate " + pack + " to re-check."); + } + + @Director(description = "Show cached validation status for a pack", aliases = {"s", "info"}) + public void status( + @Param(description = "The pack folder name", defaultValue = "") + String pack + ) { + VolmitSender s = sender(); + if (pack == null || pack.isBlank()) { + if (PackValidationRegistry.snapshot().isEmpty()) { + s.sendMessage(C.YELLOW + "No validation results recorded. Run /iris pack validate first."); + return; + } + PackValidationRegistry.snapshot().forEach((name, result) -> { + String tag = result.isLoadable() ? (C.GREEN + "OK") : (C.RED + "BROKEN"); + s.sendMessage(tag + C.RESET + " " + name + + C.GRAY + " (blocking=" + result.getBlockingErrors().size() + + ", warnings=" + result.getWarnings().size() + + ", trashed=" + result.getRemovedUnusedFiles().size() + ")"); + }); + return; + } + PackValidationResult result = PackValidationRegistry.get(pack); + if (result == null) { + s.sendMessage(C.YELLOW + "No validation result for '" + pack + "'. Run /iris pack validate " + pack + "."); + return; + } + reportResult(s, result); + } + + private PackValidationResult runValidate(VolmitSender s, File packFolder) { + try { + PackValidationResult result = PackValidator.validate(packFolder); + PackValidationRegistry.publish(result); + reportResult(s, result); + return result; + } catch (Throwable e) { + Iris.reportError("Pack validation failed for '" + packFolder.getName() + "'", e); + s.sendMessage(C.RED + "Validation of '" + packFolder.getName() + "' failed: " + e.getMessage()); + return null; + } + } + + private void reportResult(VolmitSender s, PackValidationResult result) { + if (result.isLoadable()) { + s.sendMessage(C.GREEN + "Pack '" + result.getPackName() + "' is loadable." + + C.GRAY + " (warnings=" + result.getWarnings().size() + + ", trashed=" + result.getRemovedUnusedFiles().size() + ")"); + } else { + s.sendMessage(C.RED + "Pack '" + result.getPackName() + "' is BROKEN:"); + for (String reason : result.getBlockingErrors()) { + s.sendMessage(C.RED + " - " + reason); + } + } + int wMax = Math.min(10, result.getWarnings().size()); + for (int i = 0; i < wMax; i++) { + s.sendMessage(C.YELLOW + " ! " + result.getWarnings().get(i)); + } + if (result.getWarnings().size() > wMax) { + s.sendMessage(C.GRAY + " ... and " + (result.getWarnings().size() - wMax) + " more warning(s)."); + } + int tMax = Math.min(10, result.getRemovedUnusedFiles().size()); + for (int i = 0; i < tMax; i++) { + s.sendMessage(C.GRAY + " ~ trashed " + result.getRemovedUnusedFiles().get(i)); + } + if (result.getRemovedUnusedFiles().size() > tMax) { + s.sendMessage(C.GRAY + " ... and " + (result.getRemovedUnusedFiles().size() - tMax) + " more trashed file(s)."); + } + } +} 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 806217d10..3f938ac20 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 @@ -103,7 +103,7 @@ public class CommandStudio implements DirectorExecutor { 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 = "master") + @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, diff --git a/core/src/main/java/art/arcane/iris/core/gui/NoiseExplorerGUI.java b/core/src/main/java/art/arcane/iris/core/gui/NoiseExplorerGUI.java index 4c8c38198..2878e870a 100644 --- a/core/src/main/java/art/arcane/iris/core/gui/NoiseExplorerGUI.java +++ b/core/src/main/java/art/arcane/iris/core/gui/NoiseExplorerGUI.java @@ -21,8 +21,12 @@ package art.arcane.iris.core.gui; import art.arcane.iris.Iris; import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.events.IrisEngineHotloadEvent; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.IrisGenerator; import art.arcane.iris.engine.object.NoiseStyle; -import art.arcane.volmlib.util.collection.KList; +import art.arcane.iris.engine.platform.PlatformChunkGenerator; import art.arcane.volmlib.util.function.Function2; import art.arcane.volmlib.util.math.M; import art.arcane.volmlib.util.math.RNG; @@ -32,305 +36,519 @@ import art.arcane.iris.util.common.parallel.BurstExecutor; import art.arcane.iris.util.common.parallel.MultiBurst; import art.arcane.iris.util.common.scheduling.J; import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; +import org.bukkit.Bukkit; +import org.bukkit.World; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; -import javax.imageio.ImageIO; import javax.swing.*; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; import java.awt.*; import java.awt.event.*; import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; -import java.util.concurrent.locks.ReentrantLock; +import java.awt.image.DataBufferInt; +import java.util.*; +import java.util.List; import java.util.function.Supplier; public class NoiseExplorerGUI extends JPanel implements MouseWheelListener, Listener { private static final long serialVersionUID = 2094606939770332040L; + private static final Color BG = new Color(24, 24, 28); + private static final Color SIDEBAR_BG = new Color(20, 20, 24); + private static final Color SIDEBAR_SELECTED = new Color(40, 50, 70); + private static final Color SIDEBAR_ITEM_COLOR = new Color(170, 170, 185); + private static final Color SEARCH_BG = new Color(30, 30, 38); + private static final Color SEARCH_FG = new Color(180, 180, 190); + private static final Color STATUS_BG = new Color(32, 32, 38, 230); + private static final Color STATUS_TEXT = new Color(180, 180, 190); + private static final Color ACCENT = new Color(90, 140, 255); + private static final Color SEPARATOR = new Color(40, 40, 50); + private static final Font STATUS_FONT = new Font(Font.MONOSPACED, Font.PLAIN, 12); + private static final Font SIDEBAR_HEADER_FONT = new Font(Font.SANS_SERIF, Font.BOLD, 11); + private static final Font SIDEBAR_ITEM_FONT = new Font(Font.SANS_SERIF, Font.PLAIN, 12); + private static final Font SEARCH_FONT = new Font(Font.SANS_SERIF, Font.PLAIN, 13); + private static final int SIDEBAR_WIDTH = 240; + private static final int[] HSB_LUT = new int[256]; - static JComboBox combo; - @SuppressWarnings("CanBeFinal") - static boolean hd = false; - static double ascale = 10; - static double oxp = 0; - static double ozp = 0; - static double mxx = 0; - static double mzz = 0; - @SuppressWarnings("CanBeFinal") - static boolean down = false; - @SuppressWarnings("CanBeFinal") - RollingSequence r = new RollingSequence(20); - @SuppressWarnings("CanBeFinal") - boolean colorMode = IrisSettings.get().getGui().colorMode; - double scale = 1; - CNG cng = NoiseStyle.STATIC.create(new RNG(RNG.r.nextLong())); - @SuppressWarnings("CanBeFinal") - MultiBurst gx = MultiBurst.burst; - ReentrantLock l = new ReentrantLock(); - BufferedImage img; - int w = 0; - int h = 0; - Function2 generator; - Supplier> loader; - double ox = 0; //Offset X - double oz = 0; //Offset Y - double mx = 0; - double mz = 0; - double lx = Double.MAX_VALUE; //MouseX - double lz = Double.MAX_VALUE; //MouseY - double t; - double tz; + private static final String[] CATEGORY_ORDER = { + "Pack Generators", "Simplex", "Perlin", "Cellular", "Iris", "Clover", + "Hexagon", "Vascular", "Globe", "Cubic", "Fractal", "Static", + "Nowhere", "Sierpinski", "Utility", "Other" + }; + + static { + for (int i = 0; i < 256; i++) { + float n = i / 255f; + HSB_LUT[i] = Color.HSBtoRGB(0.666f - n * 0.666f, 1f, 1f - n * 0.8f); + } + } + + private final RollingSequence fpsHistory = new RollingSequence(60); + private final boolean colorMode = IrisSettings.get().getGui().colorMode; + private final MultiBurst gx = MultiBurst.burst; + private double scale = 1; + private double animScale = 10; + private double ox = 0; + private double oz = 0; + private double animOx = 0; + private double animOz = 0; + private double lastMouseX = Double.MAX_VALUE; + private double lastMouseZ = Double.MAX_VALUE; + private double time = 0; + private double animTime = 0; + private int imgWidth = 0; + private int imgHeight = 0; + private BufferedImage img; + private CNG cng = NoiseStyle.STATIC.create(new RNG(RNG.r.nextLong())); + private Function2 generator; + private Supplier> loader; + private String currentName = "STATIC"; public NoiseExplorerGUI() { Iris.instance.registerListener(this); + setBackground(BG); addMouseWheelListener(this); addMouseMotionListener(new MouseMotionListener() { @Override public void mouseMoved(MouseEvent e) { Point cp = e.getPoint(); - - lx = (cp.getX()); - lz = (cp.getY()); - mx = lx; - mz = lz; + lastMouseX = cp.getX(); + lastMouseZ = cp.getY(); } @Override public void mouseDragged(MouseEvent e) { Point cp = e.getPoint(); - ox += (lx - cp.getX()) * scale; - oz += (lz - cp.getY()) * scale; - lx = cp.getX(); - lz = cp.getY(); + ox += (lastMouseX - cp.getX()) * scale; + oz += (lastMouseZ - cp.getY()) * scale; + lastMouseX = cp.getX(); + lastMouseZ = cp.getY(); } }); } - private static void createAndShowGUI(Supplier> loader, String genName) { - JFrame frame = new JFrame("Noise Explorer: " + genName); - NoiseExplorerGUI nv = new NoiseExplorerGUI(); - frame.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE); - JLayeredPane pane = new JLayeredPane(); - nv.setSize(new Dimension(1440, 820)); - pane.add(nv, 1, 0); - nv.loader = loader; - nv.generator = loader.get(); - frame.add(pane); - File file = Iris.getCached("Iris Icon", "https://raw.githubusercontent.com/VolmitSoftware/Iris/master/icon.png"); - - if (file != null) { - try { - frame.setIconImage(ImageIO.read(file)); - } catch (IOException e) { - Iris.reportError(e); - } - } - frame.setSize(1440, 820); - frame.setVisible(true); - } - - private static void createAndShowGUI() { - JFrame frame = new JFrame("Noise Explorer"); - NoiseExplorerGUI nv = new NoiseExplorerGUI(); - frame.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE); - KList li = new KList<>(NoiseStyle.values()).toStringList().sort(); - combo = new JComboBox<>(li.toArray(new String[0])); - combo.setSelectedItem("STATIC"); - combo.setFocusable(false); - combo.addActionListener(e -> { - @SuppressWarnings("unchecked") - String b = (String) (((JComboBox) e.getSource()).getSelectedItem()); - NoiseStyle s = NoiseStyle.valueOf(b); - nv.cng = s.create(RNG.r.nextParallelRNG(RNG.r.imax())); - }); - - combo.setSize(500, 30); - JLayeredPane pane = new JLayeredPane(); - nv.setSize(new Dimension(1440, 820)); - pane.add(nv, 1, 0); - pane.add(combo, 2, 0); - frame.add(pane); - File file = Iris.getCached("Iris Icon", "https://raw.githubusercontent.com/VolmitSoftware/Iris/master/icon.png"); - - if (file != null) { - try { - frame.setIconImage(ImageIO.read(file)); - } catch (IOException e) { - Iris.reportError(e); - } - } - frame.setSize(1440, 820); - frame.setVisible(true); - frame.addWindowListener(new java.awt.event.WindowAdapter() { - @Override - public void windowClosing(java.awt.event.WindowEvent windowEvent) { - Iris.instance.unregisterListener(nv); - } + public static void launch() { + Engine engine = findActiveEngine(); + EventQueue.invokeLater(() -> { + NoiseExplorerGUI nv = new NoiseExplorerGUI(); + buildFrame("Noise Explorer", nv, engine, null, null); }); } public static void launch(Supplier> gen, String genName) { - EventQueue.invokeLater(() -> createAndShowGUI(gen, genName)); + Engine engine = findActiveEngine(); + EventQueue.invokeLater(() -> { + NoiseExplorerGUI nv = new NoiseExplorerGUI(); + nv.loader = gen; + nv.generator = gen.get(); + nv.currentName = genName; + buildFrame("Noise Explorer: " + genName, nv, engine, gen, genName); + }); } - public static void launch() { - EventQueue.invokeLater(NoiseExplorerGUI::createAndShowGUI); + private static Engine findActiveEngine() { + try { + for (World w : new ArrayList<>(Bukkit.getWorlds())) { + try { + PlatformChunkGenerator access = IrisToolbelt.access(w); + if (access != null && access.getEngine() != null && !access.getEngine().isClosed()) { + return access.getEngine(); + } + } catch (Throwable ignored) {} + } + } catch (Throwable ignored) {} + return null; + } + + private static JFrame buildFrame(String title, NoiseExplorerGUI nv, Engine engine, + Supplier> customGen, String customName) { + JFrame frame = new JFrame(title); + frame.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE); + frame.getContentPane().setBackground(BG); + frame.setLayout(new BorderLayout()); + + JPanel sidebar = buildSidebar(nv, engine, customGen, customName); + frame.add(sidebar, BorderLayout.WEST); + frame.add(nv, BorderLayout.CENTER); + + frame.setSize(1440, 820); + frame.setMinimumSize(new Dimension(640, 480)); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + frame.addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + Iris.instance.unregisterListener(nv); + } + }); + return frame; + } + + private static JPanel buildSidebar(NoiseExplorerGUI nv, Engine engine, + Supplier> customGen, String customName) { + JPanel sidebar = new JPanel(new BorderLayout()); + sidebar.setPreferredSize(new Dimension(SIDEBAR_WIDTH, 0)); + sidebar.setBackground(SIDEBAR_BG); + sidebar.setBorder(BorderFactory.createMatteBorder(0, 0, 0, 1, SEPARATOR)); + + JTextField search = new JTextField(); + search.setBackground(SEARCH_BG); + search.setForeground(SEARCH_FG); + search.setCaretColor(SEARCH_FG); + search.setFont(SEARCH_FONT); + search.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createMatteBorder(0, 0, 1, 0, SEPARATOR), + BorderFactory.createEmptyBorder(8, 10, 8, 10) + )); + search.putClientProperty("JTextField.placeholderText", "Search..."); + + LinkedHashMap> categories = buildCategoryMap(nv, engine, customGen, customName); + DefaultListModel model = new DefaultListModel<>(); + populateModel(model, categories, ""); + + JList list = new JList<>(model); + list.setBackground(SIDEBAR_BG); + list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + list.setCellRenderer(new SidebarCellRenderer()); + list.setFixedCellHeight(-1); + + list.addListSelectionListener(e -> { + if (e.getValueIsAdjusting()) return; + ListItem selected = list.getSelectedValue(); + if (selected != null && !selected.header && selected.action != null) { + selected.action.run(); + } + }); + + search.getDocument().addDocumentListener(new DocumentListener() { + private void filter() { + String text = search.getText().trim(); + populateModel(model, categories, text); + } + + public void insertUpdate(DocumentEvent e) { filter(); } + public void removeUpdate(DocumentEvent e) { filter(); } + public void changedUpdate(DocumentEvent e) { filter(); } + }); + + JScrollPane scrollPane = new JScrollPane(list); + scrollPane.setBorder(BorderFactory.createEmptyBorder()); + scrollPane.getVerticalScrollBar().setUnitIncrement(16); + scrollPane.getVerticalScrollBar().setBackground(SIDEBAR_BG); + + sidebar.add(search, BorderLayout.NORTH); + sidebar.add(scrollPane, BorderLayout.CENTER); + return sidebar; + } + + private static void populateModel(DefaultListModel model, LinkedHashMap> categories, String filter) { + model.clear(); + String lower = filter.toLowerCase(); + for (Map.Entry> entry : categories.entrySet()) { + List matching = new ArrayList<>(); + for (ListItem item : entry.getValue()) { + if (lower.isEmpty() || item.text.toLowerCase().contains(lower) || item.rawName.toLowerCase().contains(lower)) { + matching.add(item); + } + } + if (!matching.isEmpty()) { + model.addElement(new ListItem(entry.getKey(), entry.getKey(), true, null)); + for (ListItem item : matching) { + model.addElement(item); + } + } + } + } + + private static LinkedHashMap> buildCategoryMap(NoiseExplorerGUI nv, Engine engine, + Supplier> customGen, String customName) { + LinkedHashMap> categories = new LinkedHashMap<>(); + + if (customGen != null && customName != null) { + List custom = new ArrayList<>(); + custom.add(new ListItem(customName, customName, false, () -> { + nv.generator = customGen.get(); + nv.loader = customGen; + nv.currentName = customName; + })); + categories.put("Custom", custom); + } + + Map> styleGroups = new LinkedHashMap<>(); + for (NoiseStyle style : NoiseStyle.values()) { + String cat = categorize(style); + styleGroups.computeIfAbsent(cat, k -> new ArrayList<>()).add(style); + } + + if (engine != null && !engine.isClosed()) { + List genItems = new ArrayList<>(); + try { + IrisData data = engine.getData(); + String[] keys = data.getGeneratorLoader().getPossibleKeys(); + Arrays.sort(keys); + for (String key : keys) { + IrisGenerator gen = data.getGeneratorLoader().load(key); + if (gen != null) { + long seed = new RNG(12345).nextParallelRNG(3245).lmax(); + genItems.add(new ListItem(formatName(key), key, false, () -> { + nv.generator = (x, z) -> gen.getHeight(x, z, seed); + nv.loader = null; + nv.currentName = key; + })); + } + } + } catch (Throwable ignored) {} + if (!genItems.isEmpty()) { + categories.put("Pack Generators", genItems); + } + } + + for (String cat : CATEGORY_ORDER) { + if ("Pack Generators".equals(cat)) continue; + List styles = styleGroups.get(cat); + if (styles != null && !styles.isEmpty()) { + List items = new ArrayList<>(); + for (NoiseStyle style : styles) { + items.add(new ListItem(formatName(style.name()), style.name(), false, () -> { + nv.cng = style.create(RNG.r.nextParallelRNG(RNG.r.imax())); + nv.generator = null; + nv.loader = null; + nv.currentName = style.name(); + })); + } + categories.put(cat, items); + } + } + + for (Map.Entry> entry : styleGroups.entrySet()) { + if (!categories.containsKey(entry.getKey())) { + List items = new ArrayList<>(); + for (NoiseStyle style : entry.getValue()) { + items.add(new ListItem(formatName(style.name()), style.name(), false, () -> { + nv.cng = style.create(RNG.r.nextParallelRNG(RNG.r.imax())); + nv.generator = null; + nv.loader = null; + nv.currentName = style.name(); + })); + } + categories.put(entry.getKey(), items); + } + } + + return categories; + } + + private static String categorize(NoiseStyle style) { + String n = style.name(); + if (n.startsWith("STATIC")) return "Static"; + if (n.startsWith("IRIS")) return "Iris"; + if (n.startsWith("CLOVER")) return "Clover"; + if (n.startsWith("VASCULAR")) return "Vascular"; + if (n.equals("FLAT")) return "Utility"; + if (n.startsWith("CELLULAR")) return "Cellular"; + if (n.startsWith("HEX") || n.equals("HEXAGON")) return "Hexagon"; + if (n.startsWith("SIERPINSKI")) return "Sierpinski"; + if (n.startsWith("NOWHERE")) return "Nowhere"; + if (n.startsWith("GLOB")) return "Globe"; + if (n.startsWith("PERLIN")) return "Perlin"; + if (n.startsWith("CUBIC") || (n.startsWith("FRACTAL") && n.contains("CUBIC"))) return "Cubic"; + if (n.contains("SIMPLEX") && !n.startsWith("FRACTAL")) return "Simplex"; + if (n.startsWith("FRACTAL")) return "Fractal"; + return "Other"; + } + + private static String formatName(String enumName) { + String lower = enumName.toLowerCase().replace('_', ' '); + return Character.toUpperCase(lower.charAt(0)) + lower.substring(1); } @EventHandler public void on(IrisEngineHotloadEvent e) { - if (generator != null) + if (generator != null && loader != null) { generator = loader.get(); + } } + @Override public void mouseWheelMoved(MouseWheelEvent e) { - int notches = e.getWheelRotation(); if (e.isControlDown()) { - t = t + ((0.0025 * t) * notches); + time = time + ((0.0025 * time) * notches); return; } - scale = scale + ((0.044 * scale) * notches); scale = Math.max(scale, 0.00001); } + private double lerp(double current, double target, double speed) { + double diff = target - current; + if (Math.abs(diff) < 0.001) return target; + return current + diff * speed; + } + @Override public void paint(Graphics g) { - if (scale < ascale) { - ascale -= Math.abs(scale - ascale) * 0.16; - } - - if (scale > ascale) { - ascale += Math.abs(ascale - scale) * 0.16; - } - - if (t < tz) { - tz -= Math.abs(t - tz) * 0.29; - } - - if (t > tz) { - tz += Math.abs(tz - t) * 0.29; - } - - if (ox < oxp) { - oxp -= Math.abs(ox - oxp) * 0.16; - } - - if (ox > oxp) { - oxp += Math.abs(oxp - ox) * 0.16; - } - - if (oz < ozp) { - ozp -= Math.abs(oz - ozp) * 0.16; - } - - if (oz > ozp) { - ozp += Math.abs(ozp - oz) * 0.16; - } - - if (mx < mxx) { - mxx -= Math.abs(mx - mxx) * 0.16; - } - - if (mx > mxx) { - mxx += Math.abs(mxx - mx) * 0.16; - } - - if (mz < mzz) { - mzz -= Math.abs(mz - mzz) * 0.16; - } - - if (mz > mzz) { - mzz += Math.abs(mzz - mz) * 0.16; - } + animScale = lerp(animScale, scale, 0.16); + animTime = lerp(animTime, time, 0.29); + animOx = lerp(animOx, ox, 0.16); + animOz = lerp(animOz, oz, 0.16); PrecisionStopwatch p = PrecisionStopwatch.start(); - int accuracy = hd ? 1 : M.clip((r.getAverage() / 12D), 2D, 128D).intValue(); - accuracy = down ? accuracy * 4 : accuracy; - int v = 1000; if (g instanceof Graphics2D gg) { + gg.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB); + gg.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); - if (getParent().getWidth() != w || getParent().getHeight() != h) { - w = getParent().getWidth(); - h = getParent().getHeight(); + int pw = getWidth(); + int ph = getHeight(); + + if (pw != imgWidth || ph != imgHeight || img == null) { + imgWidth = pw; + imgHeight = ph; img = null; } - if (img == null) { - img = new BufferedImage(w / accuracy, h / accuracy, BufferedImage.TYPE_INT_RGB); + int accuracy = M.clip((fpsHistory.getAverage() / 14D), 1D, 64D).intValue(); + int rw = Math.max(1, pw / accuracy); + int rh = Math.max(1, ph / accuracy); + + if (img == null || img.getWidth() != rw || img.getHeight() != rh) { + img = new BufferedImage(rw, rh, BufferedImage.TYPE_INT_RGB); } - BurstExecutor e = gx.burst(w); + int[] pixels = ((DataBufferInt) img.getRaster().getDataBuffer()).getData(); - for (int x = 0; x < w / accuracy; x++) { + BurstExecutor burst = gx.burst(rw); + for (int x = 0; x < rw; x++) { int xx = x; + burst.queue(() -> { + for (int z = 0; z < rh; z++) { + double worldX = (xx * accuracy * animScale) + animOx; + double worldZ = (z * accuracy * animScale) + animOz; + double n = generator != null + ? generator.apply(worldX, worldZ) + : cng.noise(worldX, worldZ); + n = Math.max(0, Math.min(1, n)); - int finalAccuracy = accuracy; - e.queue(() -> { - for (int z = 0; z < h / finalAccuracy; z++) { - double n = generator != null ? generator.apply(((xx * finalAccuracy) * ascale) + oxp, ((z * finalAccuracy) * ascale) + ozp) : cng.noise(((xx * finalAccuracy) * ascale) + oxp, ((z * finalAccuracy) * ascale) + ozp); - n = n > 1 ? 1 : n < 0 ? 0 : n; - - try { - //Color color = colorMode ? Color.getHSBColor((float) (n), 1f - (float) (n * n * n * n * n * n), 1f - (float) n) : Color.getHSBColor(0f, 0f, (float) n); - Color color = colorMode ? Color.getHSBColor((float) (0.666f - n * 0.666f), 1f, (float) (1f - n * 0.8f)) : Color.getHSBColor(0f, 0f, (float) n); - int rgb = color.getRGB(); - img.setRGB(xx, z, rgb); - } catch (Throwable ignored) { + int rgb; + if (colorMode) { + rgb = HSB_LUT[(int) (n * 255)]; + } else { + int v = (int) (n * 255); + rgb = (v << 16) | (v << 8) | v; } + pixels[z * rw + xx] = rgb; } }); } + burst.complete(); - e.complete(); - gg.drawImage(img, 0, 0, getParent().getWidth() * accuracy, getParent().getHeight() * accuracy, (img, infoflags, x, y, width, height) -> true); + gg.setColor(BG); + gg.fillRect(0, 0, pw, ph); + gg.drawImage(img, 0, 0, pw, ph, null); + + renderStatusBar(gg, pw, ph, p.getMilliseconds()); + renderCrosshair(gg, pw, ph); } p.end(); + time += 1D; + fpsHistory.put(p.getMilliseconds()); - t += 1D; - r.put(p.getMilliseconds()); - - if (!isVisible()) { + if (!isVisible() || !getParent().isVisible()) { return; } - if (!getParent().isVisible()) { - return; - } - - if (!getParent().getParent().isVisible()) { - return; - } - - EventQueue.invokeLater(() -> - { - J.sleep((long) Math.max(0, 32 - r.getAverage())); + long sleepMs = Math.max(1, 16 - (long) p.getMilliseconds()); + EventQueue.invokeLater(() -> { + J.sleep(sleepMs); repaint(); }); } - static class HandScrollListener extends MouseAdapter { - private static final Point pp = new Point(); + private void renderCrosshair(Graphics2D g, int w, int h) { + int cx = w / 2; + int cy = h / 2; + g.setColor(new Color(255, 255, 255, 40)); + g.drawLine(cx - 8, cy, cx + 8, cy); + g.drawLine(cx, cy - 8, cx, cy + 8); + } - @Override - public void mouseDragged(MouseEvent e) { - JViewport vport = (JViewport) e.getSource(); - JComponent label = (JComponent) vport.getView(); - Point cp = e.getPoint(); - Point vp = vport.getViewPosition(); - vp.translate(pp.x - cp.x, pp.y - cp.y); - label.scrollRectToVisible(new Rectangle(vp, vport.getSize())); + private void renderStatusBar(Graphics2D g, int w, int h, double frameMs) { + int barHeight = 28; + int y = h - barHeight; - pp.setLocation(cp); + g.setColor(STATUS_BG); + g.fillRect(0, y, w, barHeight); + g.setColor(new Color(50, 50, 60)); + g.drawLine(0, y, w, y); + + g.setFont(STATUS_FONT); + g.setColor(STATUS_TEXT); + + double worldX = (w / 2.0 * animScale) + animOx; + double worldZ = (h / 2.0 * animScale) + animOz; + double noiseVal = generator != null + ? generator.apply(worldX, worldZ) + : cng.noise(worldX, worldZ); + noiseVal = Math.max(0, Math.min(1, noiseVal)); + + int fps = frameMs > 0 ? (int) (1000.0 / frameMs) : 0; + + String status = String.format(" %s | X: %.1f Z: %.1f | Zoom: %.4f | Value: %.4f | %d FPS", + currentName, worldX, worldZ, animScale, noiseVal, fps); + g.drawString(status, 8, y + 18); + + int barW = 60; + int barX = w - barW - 12; + int barY = y + 6; + int barH = barHeight - 12; + g.setColor(new Color(40, 40, 48)); + g.fillRoundRect(barX, barY, barW, barH, 4, 4); + int fillW = (int) (noiseVal * (barW - 2)); + g.setColor(ACCENT); + g.fillRoundRect(barX + 1, barY + 1, fillW, barH - 2, 3, 3); + } + + private static final class ListItem { + final String text; + final String rawName; + final boolean header; + final Runnable action; + + ListItem(String text, String rawName, boolean header, Runnable action) { + this.text = text; + this.rawName = rawName; + this.header = header; + this.action = action; } @Override - public void mousePressed(MouseEvent e) { - pp.setLocation(e.getPoint()); + public String toString() { + return text; } } -} \ No newline at end of file + + private static final class SidebarCellRenderer extends DefaultListCellRenderer { + @Override + public Component getListCellRendererComponent(JList list, Object value, int index, boolean selected, boolean focus) { + ListItem item = (ListItem) value; + super.getListCellRendererComponent(list, item.text, index, !item.header && selected, false); + setOpaque(true); + if (item.header) { + setFont(SIDEBAR_HEADER_FONT); + setForeground(ACCENT); + setBackground(SIDEBAR_BG); + setBorder(BorderFactory.createEmptyBorder(10, 10, 4, 10)); + } else { + setFont(SIDEBAR_ITEM_FONT); + setForeground(selected ? Color.WHITE : SIDEBAR_ITEM_COLOR); + setBackground(selected ? SIDEBAR_SELECTED : SIDEBAR_BG); + setBorder(BorderFactory.createEmptyBorder(3, 20, 3, 10)); + } + return this; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/gui/VisionGUI.java b/core/src/main/java/art/arcane/iris/core/gui/VisionGUI.java index 4fe71aa3d..b9276c0fe 100644 --- a/core/src/main/java/art/arcane/iris/core/gui/VisionGUI.java +++ b/core/src/main/java/art/arcane/iris/core/gui/VisionGUI.java @@ -42,23 +42,45 @@ import org.bukkit.Location; import org.bukkit.entity.LivingEntity; import org.bukkit.entity.Player; -import javax.imageio.ImageIO; import javax.swing.*; import javax.swing.event.MouseInputListener; import java.awt.*; import java.awt.event.*; +import java.awt.geom.RoundRectangle2D; import java.awt.image.BufferedImage; import java.io.File; -import java.io.IOException; +import java.util.LinkedHashMap; import java.util.Locale; +import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.function.BiFunction; import static art.arcane.iris.util.common.data.registry.Attributes.MAX_HEALTH; public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener, MouseMotionListener, MouseInputListener { private static final long serialVersionUID = 2094606939770332040L; + + private static final Color BG = new Color(18, 18, 22); + private static final Color CARD_BG = new Color(28, 28, 36, 220); + private static final Color CARD_BORDER = new Color(60, 60, 75, 180); + private static final Color TEXT_PRIMARY = new Color(220, 220, 230); + private static final Color TEXT_SECONDARY = new Color(140, 140, 155); + private static final Color TEXT_DIM = new Color(90, 90, 105); + private static final Color ACCENT = new Color(90, 140, 255); + private static final Color ACCENT_DIM = new Color(60, 100, 200, 100); + private static final Color PLAYER_COLOR = new Color(80, 200, 120); + private static final Color MOB_COLOR = new Color(220, 80, 80); + private static final Color STATUS_BG = new Color(24, 24, 30, 240); + private static final Color GRID_COLOR = new Color(255, 255, 255, 12); + private static final Font FONT_STATUS = new Font(Font.MONOSPACED, Font.PLAIN, 12); + private static final Font FONT_CARD_TITLE = new Font(Font.SANS_SERIF, Font.BOLD, 13); + private static final Font FONT_CARD_BODY = new Font(Font.SANS_SERIF, Font.PLAIN, 12); + private static final Font FONT_HELP_KEY = new Font(Font.MONOSPACED, Font.BOLD, 12); + private static final Font FONT_NOTIFICATION = new Font(Font.SANS_SERIF, Font.BOLD, 14); + private static final int CARD_RADIUS = 8; + private static final int CARD_PAD = 12; + private static final int STATUS_HEIGHT = 26; + private final KList lastEntities = new KList<>(); private final KMap notifications = new KMap<>(); private final ChronoLatch centities = new ChronoLatch(1000); @@ -68,8 +90,7 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener private final KMap fastpositions = new KMap<>(); private final KSet working = new KSet<>(); private final KSet workingfast = new KSet<>(); - double tfps = 240D; - int ltc = 3; + private RenderType currentType = RenderType.BIOME; private boolean help = true; private boolean helpIgnored = false; @@ -81,6 +102,7 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener private boolean lowtile = false; private boolean follow = false; private boolean alt = false; + private boolean grid = false; private IrisRenderer renderer; private IrisWorld world; private double velocity = 0; @@ -99,52 +121,49 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener private double ozp = 0; private Engine engine; private int tid = 0; + private Map modeButtons; + private final ExecutorService e = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), r -> { tid++; Thread t = new Thread(r); t.setName("Iris HD Renderer " + tid); t.setPriority(Thread.MIN_PRIORITY); - t.setUncaughtExceptionHandler((et, e) -> - { + t.setUncaughtExceptionHandler((et, ex) -> { Iris.info("Exception encountered in " + et.getName()); - e.printStackTrace(); + ex.printStackTrace(); }); - return t; }); - private final ExecutorService eh = Executors.newFixedThreadPool(ltc, r -> { + private final ExecutorService eh = Executors.newFixedThreadPool(3, r -> { tid++; Thread t = new Thread(r); t.setName("Iris Renderer " + tid); t.setPriority(Thread.NORM_PRIORITY); - t.setUncaughtExceptionHandler((et, e) -> - { + t.setUncaughtExceptionHandler((et, ex) -> { Iris.info("Exception encountered in " + et.getName()); - e.printStackTrace(); + ex.printStackTrace(); }); - return t; }); - private BufferedImage texture; public VisionGUI(JFrame frame) { m.set(8); rs.put(1); + setBackground(BG); addMouseWheelListener(this); addMouseMotionListener(this); addMouseListener(this); frame.addKeyListener(this); J.a(() -> { J.sleep(10000); - if (!helpIgnored && help) { help = false; } }); - frame.addWindowListener(new java.awt.event.WindowAdapter() { + frame.addWindowListener(new WindowAdapter() { @Override - public void windowClosing(java.awt.event.WindowEvent windowEvent) { + public void windowClosing(WindowEvent windowEvent) { e.shutdown(); eh.shutdown(); } @@ -152,30 +171,98 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener } private static void createAndShowGUI(Engine r, int s, IrisWorld world) { - JFrame frame = new JFrame("Vision"); + JFrame frame = new JFrame("Iris Vision"); VisionGUI nv = new VisionGUI(frame); nv.world = world; nv.engine = r; nv.renderer = new IrisRenderer(r); - frame.add(nv); + frame.getContentPane().setBackground(BG); + frame.setLayout(new BorderLayout()); + frame.add(buildToolbar(nv), BorderLayout.NORTH); + frame.add(nv, BorderLayout.CENTER); frame.setSize(1440, 820); + frame.setMinimumSize(new Dimension(640, 480)); + frame.setLocationRelativeTo(null); frame.setVisible(true); - File file = Iris.getCached("Iris Icon", "https://raw.githubusercontent.com/VolmitSoftware/Iris/master/icon.png"); + } - if (file != null) { - try { - nv.texture = ImageIO.read(file); - frame.setIconImage(ImageIO.read(file)); - } catch (IOException e) { - Iris.reportError(e); + private static JPanel buildToolbar(VisionGUI nv) { + JPanel toolbar = new JPanel(new FlowLayout(FlowLayout.LEFT, 3, 3)); + toolbar.setBackground(new Color(22, 22, 28)); + toolbar.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, new Color(45, 45, 55))); - } + JLabel modeLabel = new JLabel("View:"); + modeLabel.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 11)); + modeLabel.setForeground(TEXT_SECONDARY); + modeLabel.setBorder(BorderFactory.createEmptyBorder(0, 6, 0, 2)); + toolbar.add(modeLabel); + + ButtonGroup modeGroup = new ButtonGroup(); + Map modeButtons = new LinkedHashMap<>(); + for (RenderType type : RenderType.values()) { + JToggleButton btn = createToolbarToggle(modeName(type), type == nv.currentType); + btn.addActionListener(e -> { + nv.setRenderType(type); + for (Map.Entry entry : modeButtons.entrySet()) { + entry.getValue().setSelected(entry.getKey() == type); + } + }); + modeGroup.add(btn); + modeButtons.put(type, btn); + toolbar.add(btn); } + nv.modeButtons = modeButtons; + + toolbar.add(createToolbarSeparator()); + + JToggleButton gridBtn = createToolbarToggle("Grid", nv.grid); + gridBtn.addActionListener(e -> { nv.toggleGrid(); gridBtn.setSelected(nv.grid); }); + toolbar.add(gridBtn); + + JToggleButton followBtn = createToolbarToggle("Follow", nv.follow); + followBtn.addActionListener(e -> { nv.toggleFollow(); followBtn.setSelected(nv.follow); }); + toolbar.add(followBtn); + + JToggleButton qualityBtn = createToolbarToggle("LQ", nv.lowtile); + qualityBtn.addActionListener(e -> { nv.toggleQuality(); qualityBtn.setSelected(nv.lowtile); }); + toolbar.add(qualityBtn); + + return toolbar; + } + + private static JToggleButton createToolbarToggle(String text, boolean selected) { + JToggleButton btn = new JToggleButton(text, selected); + btn.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 11)); + btn.setFocusable(false); + btn.setForeground(new Color(170, 170, 185)); + btn.setBackground(new Color(32, 32, 40)); + btn.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createLineBorder(new Color(50, 50, 60)), + BorderFactory.createEmptyBorder(3, 8, 3, 8) + )); + btn.setOpaque(true); + btn.addChangeListener(e -> { + if (btn.isSelected()) { + btn.setBackground(new Color(50, 60, 85)); + btn.setForeground(Color.WHITE); + } else { + btn.setBackground(new Color(32, 32, 40)); + btn.setForeground(new Color(170, 170, 185)); + } + }); + return btn; + } + + private static JSeparator createToolbarSeparator() { + JSeparator sep = new JSeparator(SwingConstants.VERTICAL); + sep.setPreferredSize(new Dimension(1, 24)); + sep.setForeground(new Color(50, 50, 60)); + sep.setBackground(new Color(22, 22, 28)); + return sep; } public static void launch(Engine g, int i) { - J.a(() -> - createAndShowGUI(g, i, g.getWorld())); + J.a(() -> createAndShowGUI(g, i, g.getWorld())); } public boolean updateEngine() { @@ -184,20 +271,18 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener try { engine = IrisToolbelt.access(world.realWorld()).getEngine(); return !engine.isClosed(); - } catch (Throwable e) { - + } catch (Throwable ignored) { } } } - return false; } @Override public void mouseMoved(MouseEvent e) { Point cp = e.getPoint(); - lx = (cp.getX()); - lz = (cp.getY()); + lx = cp.getX(); + lz = cp.getY(); } @Override @@ -209,110 +294,37 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener lz = cp.getY(); } - public int getColor(double wx, double wz) { - BiFunction colorFunction = (d, dx) -> Color.black.getRGB(); - - switch (currentType) { - case BIOME, DECORATOR_LOAD, OBJECT_LOAD, LAYER_LOAD -> - colorFunction = (x, z) -> engine.getComplex().getTrueBiomeStream().get(x, z).getColor(engine, currentType).getRGB(); - case BIOME_LAND -> - colorFunction = (x, z) -> engine.getComplex().getLandBiomeStream().get(x, z).getColor(engine, currentType).getRGB(); - case BIOME_SEA -> - colorFunction = (x, z) -> engine.getComplex().getSeaBiomeStream().get(x, z).getColor(engine, currentType).getRGB(); - case REGION -> - colorFunction = (x, z) -> engine.getComplex().getRegionStream().get(x, z).getColor(engine.getComplex(), currentType).getRGB(); - case CAVE_LAND -> - colorFunction = (x, z) -> engine.getComplex().getCaveBiomeStream().get(x, z).getColor(engine, currentType).getRGB(); - case HEIGHT -> - colorFunction = (x, z) -> Color.getHSBColor(engine.getComplex().getHeightStream().get(x, z).floatValue(), 100, 100).getRGB(); - } - - return colorFunction.apply(wx, wz); - } - public void notify(String s) { notifications.put(s, M.ms() + 2500); } @Override public void keyTyped(KeyEvent e) { - } @Override public void keyPressed(KeyEvent e) { - if (e.getKeyCode() == KeyEvent.VK_SHIFT) { - shift = true; - } - if (e.getKeyCode() == KeyEvent.VK_CONTROL) { - control = true; - } - if (e.getKeyCode() == KeyEvent.VK_SEMICOLON) { - debug = true; - } - if (e.getKeyCode() == KeyEvent.VK_SLASH) { - help = true; - helpIgnored = true; - } - if (e.getKeyCode() == KeyEvent.VK_ALT) { - alt = true; - } + if (e.getKeyCode() == KeyEvent.VK_SHIFT) shift = true; + if (e.getKeyCode() == KeyEvent.VK_CONTROL) control = true; + if (e.getKeyCode() == KeyEvent.VK_SEMICOLON) debug = true; + if (e.getKeyCode() == KeyEvent.VK_SLASH) { help = true; helpIgnored = true; } + if (e.getKeyCode() == KeyEvent.VK_ALT) alt = true; } @Override public void keyReleased(KeyEvent e) { - if (e.getKeyCode() == KeyEvent.VK_SEMICOLON) { - debug = false; - } - if (e.getKeyCode() == KeyEvent.VK_SHIFT) { - shift = false; - } + if (e.getKeyCode() == KeyEvent.VK_SEMICOLON) debug = false; + if (e.getKeyCode() == KeyEvent.VK_SHIFT) shift = false; + if (e.getKeyCode() == KeyEvent.VK_CONTROL) control = false; + if (e.getKeyCode() == KeyEvent.VK_SLASH) { help = false; helpIgnored = true; } + if (e.getKeyCode() == KeyEvent.VK_ALT) alt = false; - if (e.getKeyCode() == KeyEvent.VK_CONTROL) { - control = false; - } - if (e.getKeyCode() == KeyEvent.VK_SLASH) { - help = false; - helpIgnored = true; - } - if (e.getKeyCode() == KeyEvent.VK_ALT) { - alt = false; - } + if (e.getKeyCode() == KeyEvent.VK_F) { toggleFollow(); return; } + if (e.getKeyCode() == KeyEvent.VK_R) { dump(); notify("Refreshing"); return; } + if (e.getKeyCode() == KeyEvent.VK_P) { toggleQuality(); return; } + if (e.getKeyCode() == KeyEvent.VK_E) { eco = !eco; dump(); notify((eco ? "30" : "60") + " FPS"); return; } + if (e.getKeyCode() == KeyEvent.VK_G) { toggleGrid(); return; } - // Pushes - if (e.getKeyCode() == KeyEvent.VK_F) { - follow = !follow; - - if (player != null && follow) { - notify("Following " + player.getName() + ". Press F to disable"); - } else if (follow) { - notify("Can't follow, no one is in the world"); - follow = false; - } else { - notify("Follow Off"); - } - - return; - } - - if (e.getKeyCode() == KeyEvent.VK_R) { - dump(); - notify("Refreshing Chunks"); - return; - } - - if (e.getKeyCode() == KeyEvent.VK_P) { - lowtile = !lowtile; - dump(); - notify("Rendering " + (lowtile ? "Low" : "High") + " Quality Tiles"); - return; - } - if (e.getKeyCode() == KeyEvent.VK_E) { - eco = !eco; - dump(); - notify("Using " + (eco ? "60" : "Uncapped") + " FPS Limit"); - return; - } if (e.getKeyCode() == KeyEvent.VK_EQUALS) { mscale = mscale + ((0.044 * mscale) * -3); mscale = Math.max(mscale, 0.00001); @@ -325,7 +337,6 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener dump(); return; } - if (e.getKeyCode() == KeyEvent.VK_BACK_SLASH) { mscale = 1D; dump(); @@ -334,22 +345,59 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener } int currentMode = currentType.ordinal(); - for (RenderType i : RenderType.values()) { if (e.getKeyChar() == String.valueOf(i.ordinal() + 1).charAt(0)) { if (i.ordinal() != currentMode) { - currentType = i; - dump(); - notify("Rendering " + Form.capitalizeWords(currentType.name().toLowerCase().replaceAll("\\Q_\\E", " "))); + setRenderType(i); + syncModeButtons(); return; } } } if (e.getKeyCode() == KeyEvent.VK_M) { - currentType = RenderType.values()[(currentMode + 1) % RenderType.values().length]; - notify("Rendering " + Form.capitalizeWords(currentType.name().toLowerCase().replaceAll("\\Q_\\E", " "))); - dump(); + setRenderType(RenderType.values()[(currentMode + 1) % RenderType.values().length]); + syncModeButtons(); + } + } + + private static String modeName(RenderType type) { + return Form.capitalizeWords(type.name().toLowerCase().replaceAll("\\Q_\\E", " ")); + } + + void setRenderType(RenderType type) { + currentType = type; + dump(); + notify(modeName(type)); + } + + void toggleGrid() { + grid = !grid; + notify("Grid " + (grid ? "On" : "Off")); + } + + void toggleFollow() { + follow = !follow; + if (player != null && follow) { + notify("Following " + player.getName()); + } else if (follow) { + notify("No player in world"); + follow = false; + } else { + notify("Follow disabled"); + } + } + + void toggleQuality() { + lowtile = !lowtile; + dump(); + notify((lowtile ? "Low" : "High") + " Quality"); + } + + private void syncModeButtons() { + if (modeButtons == null) return; + for (Map.Entry entry : modeButtons.entrySet()) { + entry.getValue().setSelected(entry.getKey() == currentType); } } @@ -369,25 +417,21 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener if (fastpositions.containsKey(key)) { if (!working.contains(key) && working.size() < 9) { m.set(m.get() - 1); - if (m.get() >= 0 && velocity < 50) { working.add(key); double mk = mscale; double mkd = scale; - e.submit(() -> - { + e.submit(() -> { PrecisionStopwatch ps = PrecisionStopwatch.start(); BufferedImage b = renderer.render(x * mscale, z * mscale, div * mscale, div / (lowtile ? 3 : 1), currentType); rs.put(ps.getMilliseconds()); working.remove(key); - if (mk == mscale && mkd == scale) { positions.put(key, b); } }); } } - return fastpositions.get(key); } @@ -398,13 +442,11 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener workingfast.add(key); double mk = mscale; double mkd = scale; - eh.submit(() -> - { + eh.submit(() -> { PrecisionStopwatch ps = PrecisionStopwatch.start(); BufferedImage b = renderer.render(x * mscale, z * mscale, div * mscale, div / lowq, currentType); rs.put(ps.getMilliseconds()); workingfast.remove(key); - if (mk == mscale && mkd == scale) { fastpositions.put(key, b); } @@ -421,25 +463,25 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener } private double getScreenX(double x) { - return (x / mscale) - ((oxp / scale)); + return (x / mscale) - (oxp / scale); } private double getScreenZ(double z) { - return (z / mscale) - ((ozp / scale)); + return (z / mscale) - (ozp / scale); + } + + private double lerp(double current, double target, double speed) { + double diff = target - current; + if (Math.abs(diff) < 0.5) return target; + return current + diff * speed; } @Override public void paint(Graphics gx) { - if (engine.isClosed()) { EventQueue.invokeLater(() -> { - try { - setVisible(false); - } catch (Throwable e) { - - } + try { setVisible(false); } catch (Throwable ignored) { } }); - return; } @@ -447,48 +489,11 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener dump(); } - if (ox < oxp) { - velocity = Math.abs(ox - oxp) * 0.36; - oxp -= velocity; - } - - if (ox > oxp) { - velocity = Math.abs(oxp - ox) * 0.36; - oxp += velocity; - } - - if (oz < ozp) { - velocity = Math.abs(oz - ozp) * 0.36; - ozp -= velocity; - } - - if (oz > ozp) { - velocity = Math.abs(ozp - oz) * 0.36; - ozp += velocity; - } - - if (lx < hx) { - hx -= Math.abs(lx - hx) * 0.36; - } - - if (lx > hx) { - hx += Math.abs(hx - lx) * 0.36; - } - - if (lz < hz) { - hz -= Math.abs(lz - hz) * 0.36; - } - - if (lz > hz) { - hz += Math.abs(hz - lz) * 0.36; - } - - if (Math.abs(lx - hx) < 0.5) { - hx = lx; - } - if (Math.abs(lz - hz) < 0.5) { - hz = lz; - } + velocity = Math.abs(ox - oxp) * 0.36 + Math.abs(oz - ozp) * 0.36; + oxp = lerp(oxp, ox, 0.36); + ozp = lerp(ozp, oz, 0.36); + hx = lerp(hx, lx, 0.36); + hz = lerp(hz, lz, 0.36); if (centities.flip()) { J.s(() -> { @@ -498,9 +503,14 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener } }); } + lowq = Math.max(Math.min((int) M.lerp(8, 28, velocity / 1000D), 28), 8); PrecisionStopwatch p = PrecisionStopwatch.start(); Graphics2D g = (Graphics2D) gx; + g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); + w = getWidth(); h = getHeight(); double vscale = scale; @@ -512,8 +522,8 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener KSet gg = new KSet<>(); int iscale = (int) scale; - g.setColor(Color.white); - g.clearRect(0, 0, w, h); + g.setColor(BG); + g.fillRect(0, 0, w, h); double offsetX = oxp / scale; double offsetZ = ozp / scale; m.set(3); @@ -531,13 +541,17 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener if (t != null) { int rx = Math.floorMod((int) Math.floor(offsetX), iscale); int rz = Math.floorMod((int) Math.floor(offsetZ), iscale); - g.drawImage(t, i - rx, j - rz, iscale, iscale, (img, infoflags, x, y, width, height) -> true); + g.drawImage(t, i - rx, j - rz, iscale, iscale, null); } } } } } + if (grid) { + renderGrid(g, iscale, offsetX, offsetZ); + } + p.end(); for (BlockPosition i : positions.k()) { @@ -546,36 +560,41 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener } } - hanleFollow(); - renderOverlays(g); + handleFollow(); + renderOverlays(g, p.getMilliseconds()); - if (!isVisible()) { + if (!isVisible() || !getParent().isVisible()) { return; } - if (!getParent().isVisible()) { - return; - } - - if (!getParent().getParent().isVisible()) { - return; - } - - J.a(() -> - { - J.sleep(eco ? 15 : 1); + long targetMs = eco ? 32 : 16; + long sleepMs = Math.max(1, targetMs - (long) p.getMilliseconds()); + J.a(() -> { + J.sleep(sleepMs); repaint(); }); } - private void hanleFollow() { + private void renderGrid(Graphics2D g, int tileSize, double offsetX, double offsetZ) { + g.setColor(GRID_COLOR); + int rx = Math.floorMod((int) Math.floor(offsetX), tileSize); + int rz = Math.floorMod((int) Math.floor(offsetZ), tileSize); + for (int i = -tileSize; i < w + tileSize; i += tileSize) { + g.drawLine(i - rx, 0, i - rx, h); + } + for (int j = -tileSize; j < h + tileSize; j += tileSize) { + g.drawLine(0, j - rz, w, j - rz); + } + } + + private void handleFollow() { if (follow && player != null) { animateTo(player.getLocation().getX(), player.getLocation().getZ()); } } - private void renderOverlays(Graphics2D g) { - renderPlayer(g); + private void renderOverlays(Graphics2D g, double frameMs) { + renderEntities(g); if (help) { renderOverlayHelp(g); @@ -583,108 +602,123 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener renderOverlayDebug(g); } - renderOverlayLegend(g); - + renderStatusBar(g, frameMs); renderHoverOverlay(g, shift); + if (!notifications.isEmpty()) { renderNotification(g); } } - private void renderOverlayLegend(Graphics2D g) { - KList l = new KList<>(); - l.add("Zoom: " + Form.pc(mscale, 0)); - l.add("Blocks: " + Form.f((int) mscale * w) + " by " + Form.f((int) mscale * h)); - l.add("BPP: " + Form.f(mscale, 1)); - l.add("Render Mode: " + Form.capitalizeWords(currentType.name().toLowerCase().replaceAll("\\Q_\\E", " "))); + private void renderStatusBar(Graphics2D g, double frameMs) { + int y = h - STATUS_HEIGHT; + g.setColor(STATUS_BG); + g.fillRect(0, y, w, STATUS_HEIGHT); + g.setColor(CARD_BORDER); + g.drawLine(0, y, w, y); - drawCardBR(g, l); + g.setFont(FONT_STATUS); + g.setColor(TEXT_SECONDARY); + + double wx = getWorldX(w / 2.0); + double wz = getWorldZ(h / 2.0); + int fps = frameMs > 0 ? (int) (1000.0 / frameMs) : 0; + + String left = String.format(" %s | %.1f bpp | %s x %s blocks", + modeName(currentType), mscale, + Form.f((int) (mscale * w)), Form.f((int) (mscale * h))); + g.drawString(left, 8, y + 17); + + String right = String.format("X: %s Z: %s | %d FPS ", + Form.f((int) wx), Form.f((int) wz), fps); + int rw = g.getFontMetrics().stringWidth(right); + g.drawString(right, w - rw - 8, y + 17); + + g.setColor(ACCENT); + int modeW = g.getFontMetrics().stringWidth(" " + modeName(currentType)); + g.fillRect(0, y + 1, 3, STATUS_HEIGHT - 1); } - private void renderNotification(Graphics2D g) { - drawCardCB(g, notifications.k()); - - for (String i : notifications.k()) { - if (M.ms() > notifications.get(i)) { - notifications.remove(i); - } - } - } - - private void renderPlayer(Graphics2D g) { + private void renderEntities(Graphics2D g) { Player b = null; for (Player i : world.getPlayers()) { b = i; - renderPosition(g, i.getLocation().getX(), i.getLocation().getZ()); + renderPlayerMarker(g, i.getLocation().getX(), i.getLocation().getZ(), i.getName()); } synchronized (lastEntities) { double dist = Double.MAX_VALUE; - LivingEntity h = null; + LivingEntity nearest = null; for (LivingEntity i : lastEntities) { - if (i instanceof Player) { - continue; - } - - renderMobPosition(g, i, i.getLocation().getX(), i.getLocation().getZ()); + if (i instanceof Player) continue; + renderMobMarker(g, i.getLocation().getX(), i.getLocation().getZ()); if (shift) { - double d = i.getLocation().distanceSquared(new Location(i.getWorld(), getWorldX(hx), i.getLocation().getY(), getWorldZ(hz))); - + double d = i.getLocation().distanceSquared( + new Location(i.getWorld(), getWorldX(hx), i.getLocation().getY(), getWorldZ(hz))); if (d < dist) { dist = d; - h = i; + nearest = i; } } } - if (h != null && shift) { - g.setColor(Color.red); - g.fillRoundRect((int) getScreenX(h.getLocation().getX()) - 10, (int) getScreenZ(h.getLocation().getZ()) - 10, 20, 20, 20, 20); + if (nearest != null && shift) { + double sx = getScreenX(nearest.getLocation().getX()); + double sz = getScreenZ(nearest.getLocation().getZ()); + g.setColor(MOB_COLOR); + g.fillOval((int) sx - 6, (int) sz - 6, 12, 12); + g.setColor(new Color(220, 80, 80, 60)); + g.fillOval((int) sx - 10, (int) sz - 10, 20, 20); + KList k = new KList<>(); - k.add(Form.capitalizeWords(h.getType().name().toLowerCase(Locale.ROOT).replaceAll("\\Q_\\E", " ")) + h.getEntityId()); - - k.add("Pos: " + h.getLocation().getBlockX() + ", " + h.getLocation().getBlockY() + ", " + h.getLocation().getBlockZ()); - k.add("UUID: " + h.getUniqueId()); - k.add("HP: " + h.getHealth() + " / " + h.getAttribute(MAX_HEALTH).getValue()); - - drawCardTR(g, k); + k.add(Form.capitalizeWords(nearest.getType().name().toLowerCase(Locale.ROOT).replaceAll("\\Q_\\E", " "))); + k.add("Pos: " + nearest.getLocation().getBlockX() + ", " + nearest.getLocation().getBlockY() + ", " + nearest.getLocation().getBlockZ()); + k.add("HP: " + Form.f(nearest.getHealth(), 1) + " / " + Form.f(nearest.getAttribute(MAX_HEALTH).getValue(), 1)); + drawCard(w - CARD_PAD, CARD_PAD, 1, 0, g, k); } } player = b; } + private void renderPlayerMarker(Graphics2D g, double x, double z, String name) { + int sx = (int) getScreenX(x); + int sz = (int) getScreenZ(z); + g.setColor(new Color(80, 200, 120, 40)); + g.fillOval(sx - 12, sz - 12, 24, 24); + g.setColor(PLAYER_COLOR); + g.fillOval(sx - 5, sz - 5, 10, 10); + g.setColor(new Color(40, 160, 80)); + g.drawOval(sx - 5, sz - 5, 10, 10); + + g.setFont(FONT_CARD_BODY); + g.setColor(TEXT_PRIMARY); + int nw = g.getFontMetrics().stringWidth(name); + g.drawString(name, sx - nw / 2, sz - 14); + } + + private void renderMobMarker(Graphics2D g, double x, double z) { + int sx = (int) getScreenX(x); + int sz = (int) getScreenZ(z); + g.setColor(MOB_COLOR); + g.fillRect(sx - 2, sz - 2, 4, 4); + } + private void animateTo(double wx, double wz) { - double cx = getWorldX(getWidth() / 2); - double cz = getWorldZ(getHeight() / 2); + double cx = getWorldX(getWidth() / 2.0); + double cz = getWorldZ(getHeight() / 2.0); ox += ((wx - cx) / mscale) * scale; oz += ((wz - cz) / mscale) * scale; } - private void renderPosition(Graphics2D g, double x, double z) { - if (texture != null) { - g.drawImage(texture, (int) getScreenX(x), (int) getScreenZ(z), 66, 66, (img, infoflags, xx, xy, width, height) -> true); - } else { - g.setColor(Color.darkGray); - g.fillRoundRect((int) getScreenX(x) - 15, (int) getScreenZ(z) - 15, 30, 30, 15, 15); - g.setColor(Color.cyan.darker().darker()); - g.fillRoundRect((int) getScreenX(x) - 10, (int) getScreenZ(z) - 10, 20, 20, 10, 10); - } - } - - private void renderMobPosition(Graphics2D g, LivingEntity e, double x, double z) { - g.setColor(Color.red.darker().darker()); - g.fillRoundRect((int) getScreenX(x) - 2, (int) getScreenZ(z) - 2, 4, 4, 4, 4); - } - private void renderHoverOverlay(Graphics2D g, boolean detailed) { IrisBiome biome = engine.getComplex().getTrueBiomeStream().get(getWorldX(hx), getWorldZ(hz)); IrisRegion region = engine.getComplex().getRegionStream().get(getWorldX(hx), getWorldZ(hz)); KList l = new KList<>(); - l.add("Biome: " + biome.getName()); - l.add("Region: " + region.getName() + "(" + region.getLoadKey() + ")"); + l.add(biome.getName()); + l.add(region.getName()); l.add("Block " + (int) getWorldX(hx) + ", " + (int) getWorldZ(hz)); if (detailed) { l.add("Chunk " + ((int) getWorldX(hx) >> 4) + ", " + ((int) getWorldZ(hz) >> 4)); @@ -692,135 +726,134 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener l.add("Key: " + biome.getLoadKey()); l.add("File: " + biome.getLoadFile()); } - - drawCardAt((float) hx, (float) hz, 0, 0, g, l); + drawCard((float) hx + 16, (float) hz, 0, 0, g, l); } private void renderOverlayDebug(Graphics2D g) { KList l = new KList<>(); l.add("Velocity: " + (int) velocity); - l.add("Center Pos: " + Form.f((int) getWorldX(getWidth() / 2)) + ", " + Form.f((int) getWorldZ(getHeight() / 2))); - drawCardBL(g, l); + l.add("Tiles: " + positions.size() + " HD / " + fastpositions.size() + " LQ"); + l.add("Workers: " + working.size() + " HD / " + workingfast.size() + " LQ"); + l.add("Center: " + Form.f((int) getWorldX(getWidth() / 2.0)) + ", " + Form.f((int) getWorldZ(getHeight() / 2.0))); + drawCard(CARD_PAD, h - STATUS_HEIGHT - CARD_PAD, 0, 1, g, l); } private void renderOverlayHelp(Graphics2D g) { - KList l = new KList<>(); - l.add("/ to show this help screen"); - l.add("R to repaint the screen"); - l.add("F to follow first player"); - l.add("+/- to Change Zoom"); - l.add("\\ to reset zoom to 1"); - l.add("M to cycle render modes"); - l.add("P to toggle Tile Quality Mode"); - l.add("E to toggle Eco FPS Mode"); + KList keys = new KList<>(); + KList descs = new KList<>(); + keys.add("/"); descs.add("Toggle help"); + keys.add("R"); descs.add("Refresh tiles"); + keys.add("F"); descs.add("Follow player"); + keys.add("+/-"); descs.add("Zoom in/out"); + keys.add("\\"); descs.add("Reset zoom"); + keys.add("M"); descs.add("Cycle render mode"); + keys.add("P"); descs.add("Toggle tile quality"); + keys.add("E"); descs.add("Toggle 30/60 FPS"); + keys.add("G"); descs.add("Toggle grid"); int ff = 0; for (RenderType i : RenderType.values()) { ff++; - l.add(ff + " to view " + Form.capitalizeWords(i.name().toLowerCase().replaceAll("\\Q_\\E", " "))); + keys.add(String.valueOf(ff)); + descs.add(modeName(i)); } - l.add("Shift for additional biome details (at cursor)"); - l.add("CTRL + Click to teleport to location"); - l.add("ALT + Click to open biome in VSCode"); - drawCardTL(g, l); - } + keys.add("Shift"); descs.add("Detailed biome info"); + keys.add("Ctrl+Click"); descs.add("Teleport to cursor"); + keys.add("Alt+Click"); descs.add("Open biome in editor"); - private void drawCardTL(Graphics2D g, KList text) { - drawCardAt(0, 0, 0, 0, g, text); - } - - private void drawCardBR(Graphics2D g, KList text) { - drawCardAt(getWidth(), getHeight(), 1, 1, g, text); - } - - private void drawCardBL(Graphics2D g, KList text) { - drawCardAt(0, getHeight(), 0, 1, g, text); - } - - private void drawCardTR(Graphics2D g, KList text) { - drawCardAt(getWidth(), 0, 1, 0, g, text); - } - - private void open() { - IrisComplex complex = engine.getComplex(); - File r = null; - switch (currentType) { - case BIOME, LAYER_LOAD, DECORATOR_LOAD, OBJECT_LOAD, HEIGHT -> - r = complex.getTrueBiomeStream().get(getWorldX(hx), getWorldZ(hz)).openInVSCode(); - case BIOME_LAND -> r = complex.getLandBiomeStream().get(getWorldX(hx), getWorldZ(hz)).openInVSCode(); - case BIOME_SEA -> r = complex.getSeaBiomeStream().get(getWorldX(hx), getWorldZ(hz)).openInVSCode(); - case REGION -> r = complex.getRegionStream().get(getWorldX(hx), getWorldZ(hz)).openInVSCode(); - case CAVE_LAND -> r = complex.getCaveBiomeStream().get(getWorldX(hx), getWorldZ(hz)).openInVSCode(); + int maxKeyW = 0; + g.setFont(FONT_HELP_KEY); + for (String k : keys) { + maxKeyW = Math.max(maxKeyW, g.getFontMetrics().stringWidth(k)); } - notify("Opening " + r.getPath() + " in VSCode"); + int lineH = 20; + int totalH = keys.size() * lineH + CARD_PAD * 2 + 4; + int totalW = maxKeyW + 180 + CARD_PAD * 2; + + drawCardBackground(g, CARD_PAD, CARD_PAD, totalW, totalH); + + for (int i = 0; i < keys.size(); i++) { + int y = CARD_PAD + 16 + i * lineH; + + g.setFont(FONT_HELP_KEY); + g.setColor(ACCENT); + g.drawString(keys.get(i), CARD_PAD * 2, y); + + g.setFont(FONT_CARD_BODY); + g.setColor(TEXT_SECONDARY); + g.drawString(descs.get(i), CARD_PAD * 2 + maxKeyW + 16, y); + } } - private void teleport() { - J.s(() -> { - if (player != null) { - int xx = (int) getWorldX(hx); - int zz = (int) getWorldZ(hz); - int h = engine.getComplex().getRoundedHeighteightStream().get(xx, zz); - player.teleport(new Location(player.getWorld(), xx, h, zz)); - notify("Teleporting to " + xx + ", " + h + ", " + zz); + private void renderNotification(Graphics2D g) { + int y = h - STATUS_HEIGHT - 50; + g.setFont(FONT_NOTIFICATION); + + KList active = new KList<>(); + for (String i : notifications.k()) { + if (M.ms() > notifications.get(i)) { + notifications.remove(i); } else { - notify("No player in world, can't teleport."); + active.add(i); } - }); - } - - private void drawCardCB(Graphics2D g, KList text) { - drawCardAt(getWidth() / 2, getHeight(), 0.5, 1, g, text); - } - - private void drawCardCT(Graphics2D g, KList text) { - drawCardAt(getWidth() / 2, 0, 0.5, 0, g, text); - } - - private void drawCardAt(float x, float y, double pushX, double pushZ, Graphics2D g, KList text) { - g.setFont(new Font("Hevetica", Font.BOLD, 16)); - int h = 0; - int w = 0; - - for (String i : text) { - h += g.getFontMetrics().getHeight(); - w = Math.max(w, g.getFontMetrics().stringWidth(i)); } - w += 28; - h += 14; + if (active.isEmpty()) return; - int cw = (int) ((w + 26) * pushX); - int ch = (int) ((h + 26) * pushZ); + String text = String.join(" | ", active); + int tw = g.getFontMetrics().stringWidth(text); + int th = g.getFontMetrics().getHeight(); + int px = (w - tw) / 2 - 16; + int py = y - th / 2 - 8; + int bw = tw + 32; + int bh = th + 16; - g.setColor(Color.darkGray); - g.fillRect((int) x + 7 + 2 - cw, (int) y + 12 + 2 - ch, w + 7, h); // Shadow - g.setColor(Color.gray); - g.fillRect((int) x + 7 + 1 - cw, (int) y + 12 + 1 - ch, w + 7, h); // Shadow - g.setColor(Color.white); - g.fillRect((int) x + 7 - cw, (int) y + 12 - ch, w + 7, h); + drawCardBackground(g, px, py, bw, bh); + g.setColor(TEXT_PRIMARY); + g.drawString(text, px + 16, py + th + 4); + } - g.setColor(Color.black); - int m = 0; + private void drawCardBackground(Graphics2D g, int x, int y, int w, int h) { + RoundRectangle2D rect = new RoundRectangle2D.Double(x, y, w, h, CARD_RADIUS, CARD_RADIUS); + g.setColor(CARD_BG); + g.fill(rect); + g.setColor(CARD_BORDER); + g.draw(rect); + } + + private void drawCard(float x, float y, double pushX, double pushZ, Graphics2D g, KList text) { + g.setFont(FONT_CARD_BODY); + int lineH = g.getFontMetrics().getHeight(); + int cardW = 0; for (String i : text) { - g.drawString(i, x + 14 - cw, y + 14 - ch + (++m * g.getFontMetrics().getHeight())); + cardW = Math.max(cardW, g.getFontMetrics().stringWidth(i)); + } + cardW += CARD_PAD * 2; + int cardH = text.size() * lineH + CARD_PAD * 2 - 4; + + int cx = (int) (x - cardW * pushX); + int cy = (int) (y - cardH * pushZ); + + drawCardBackground(g, cx, cy, cardW, cardH); + + int ty = cy + CARD_PAD + lineH - 4; + for (int i = 0; i < text.size(); i++) { + g.setColor(i == 0 ? TEXT_PRIMARY : TEXT_SECONDARY); + g.setFont(i == 0 ? FONT_CARD_TITLE : FONT_CARD_BODY); + g.drawString(text.get(i), cx + CARD_PAD, ty + i * lineH); } } public void mouseWheelMoved(MouseWheelEvent e) { int notches = e.getWheelRotation(); - if (e.isControlDown()) { - return; - } + if (e.isControlDown()) return; double m0 = mscale; double m1 = m0 + ((0.25 * m0) * notches); m1 = Math.max(m1, 0.00001); - if (m1 == m0) { - return; - } + if (m1 == m0) return; positions.clear(); fastpositions.clear(); @@ -841,30 +874,42 @@ public class VisionGUI extends JPanel implements MouseWheelListener, KeyListener @Override public void mouseClicked(MouseEvent e) { - if (control) { - teleport(); - } else if (alt) { - open(); + if (control) teleport(); + else if (alt) open(); + } + + @Override public void mousePressed(MouseEvent e) { } + @Override public void mouseReleased(MouseEvent e) { } + @Override public void mouseEntered(MouseEvent e) { } + @Override public void mouseExited(MouseEvent e) { } + + private void open() { + IrisComplex complex = engine.getComplex(); + File r = null; + switch (currentType) { + case BIOME, LAYER_LOAD, DECORATOR_LOAD, OBJECT_LOAD, HEIGHT -> + r = complex.getTrueBiomeStream().get(getWorldX(hx), getWorldZ(hz)).openInVSCode(); + case BIOME_LAND -> r = complex.getLandBiomeStream().get(getWorldX(hx), getWorldZ(hz)).openInVSCode(); + case BIOME_SEA -> r = complex.getSeaBiomeStream().get(getWorldX(hx), getWorldZ(hz)).openInVSCode(); + case REGION -> r = complex.getRegionStream().get(getWorldX(hx), getWorldZ(hz)).openInVSCode(); + case CAVE_LAND -> r = complex.getCaveBiomeStream().get(getWorldX(hx), getWorldZ(hz)).openInVSCode(); + } + if (r != null) { + notify("Opened " + r.getName()); } } - @Override - public void mousePressed(MouseEvent e) { - - } - - @Override - public void mouseReleased(MouseEvent e) { - - } - - @Override - public void mouseEntered(MouseEvent e) { - - } - - @Override - public void mouseExited(MouseEvent e) { - + private void teleport() { + J.s(() -> { + if (player != null) { + int xx = (int) getWorldX(hx); + int zz = (int) getWorldZ(hz); + int yy = player.getWorld().getHighestBlockYAt(xx, zz) + 1; + player.teleport(new Location(player.getWorld(), xx, yy, zz)); + notify("Teleported to " + xx + ", " + yy + ", " + zz); + } else { + notify("No player in world"); + } + }); } } diff --git a/core/src/main/java/art/arcane/iris/core/gui/components/IrisRenderer.java b/core/src/main/java/art/arcane/iris/core/gui/components/IrisRenderer.java index 7316357d9..d69c9ecfd 100644 --- a/core/src/main/java/art/arcane/iris/core/gui/components/IrisRenderer.java +++ b/core/src/main/java/art/arcane/iris/core/gui/components/IrisRenderer.java @@ -1,84 +1,80 @@ -/* - * 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.gui.components; - -import art.arcane.iris.engine.framework.Engine; -import art.arcane.iris.engine.object.IrisBiome; -import art.arcane.iris.engine.object.IrisBiomeGeneratorLink; -import art.arcane.iris.util.project.interpolation.IrisInterpolation; -import java.awt.*; -import java.awt.image.BufferedImage; -import java.util.function.BiFunction; - -public class IrisRenderer { - private final Engine renderer; - - public IrisRenderer(Engine renderer) { - this.renderer = renderer; - } - - public BufferedImage render(double sx, double sz, double size, int resolution, RenderType currentType) { - BufferedImage image = new BufferedImage(resolution, resolution, BufferedImage.TYPE_INT_RGB); - BiFunction colorFunction = (d, dx) -> Color.black.getRGB(); - - switch (currentType) { - case BIOME, DECORATOR_LOAD, OBJECT_LOAD, LAYER_LOAD -> - colorFunction = (x, z) -> renderer.getComplex().getTrueBiomeStream().get(x, z).getColor(renderer, currentType).getRGB(); - case BIOME_LAND -> - colorFunction = (x, z) -> renderer.getComplex().getLandBiomeStream().get(x, z).getColor(renderer, currentType).getRGB(); - case BIOME_SEA -> - colorFunction = (x, z) -> renderer.getComplex().getSeaBiomeStream().get(x, z).getColor(renderer, currentType).getRGB(); - case REGION -> - colorFunction = (x, z) -> renderer.getComplex().getRegionStream().get(x, z).getColor(renderer.getComplex(), currentType).getRGB(); - case CAVE_LAND -> - colorFunction = (x, z) -> renderer.getComplex().getCaveBiomeStream().get(x, z).getColor(renderer, currentType).getRGB(); - case HEIGHT -> - colorFunction = (x, z) -> Color.getHSBColor(renderer.getComplex().getHeightStream().get(x, z).floatValue(), 100, 100).getRGB(); - case CONTINENT -> colorFunction = (x, z) -> { - IrisBiome b = renderer.getBiome((int) Math.round(x), renderer.getMaxHeight() - 1, (int) Math.round(z)); - IrisBiomeGeneratorLink g = b.getGenerators().get(0); - Color c; - if (g.getMax() <= 0) { - // Max is below water level, so it is most likely an ocean biome - c = Color.BLUE; - } else if (g.getMin() < 0) { - // Min is below water level, but max is not, so it is most likely a shore biome - c = Color.YELLOW; - } else { - // Both min and max are above water level, so it is most likely a land biome - c = Color.GREEN; - } - return c.getRGB(); - }; - } - - double x, z; - int i, j; - for (i = 0; i < resolution; i++) { - x = IrisInterpolation.lerp(sx, sx + size, (double) i / (double) (resolution)); - - for (j = 0; j < resolution; j++) { - z = IrisInterpolation.lerp(sz, sz + size, (double) j / (double) (resolution)); - image.setRGB(i, j, colorFunction.apply(x, z)); - } - } - - return image; - } -} +/* + * 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.gui.components; + +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisBiomeGeneratorLink; +import art.arcane.iris.util.project.interpolation.IrisInterpolation; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.util.function.BiFunction; + +public class IrisRenderer { + private static final int BLUE = Color.BLUE.getRGB(); + private static final int YELLOW = Color.YELLOW.getRGB(); + private static final int GREEN = Color.GREEN.getRGB(); + + private final Engine renderer; + + public IrisRenderer(Engine renderer) { + this.renderer = renderer; + } + + public BufferedImage render(double sx, double sz, double size, int resolution, RenderType currentType) { + BufferedImage image = new BufferedImage(resolution, resolution, BufferedImage.TYPE_INT_RGB); + int[] pixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); + BiFunction colorFunction = (d, dx) -> 0; + + switch (currentType) { + case BIOME, DECORATOR_LOAD, OBJECT_LOAD, LAYER_LOAD -> + colorFunction = (x, z) -> renderer.getComplex().getTrueBiomeStream().get(x, z).getColor(renderer, currentType).getRGB(); + case BIOME_LAND -> + colorFunction = (x, z) -> renderer.getComplex().getLandBiomeStream().get(x, z).getColor(renderer, currentType).getRGB(); + case BIOME_SEA -> + colorFunction = (x, z) -> renderer.getComplex().getSeaBiomeStream().get(x, z).getColor(renderer, currentType).getRGB(); + case REGION -> + colorFunction = (x, z) -> renderer.getComplex().getRegionStream().get(x, z).getColor(renderer.getComplex(), currentType).getRGB(); + case CAVE_LAND -> + colorFunction = (x, z) -> renderer.getComplex().getCaveBiomeStream().get(x, z).getColor(renderer, currentType).getRGB(); + case HEIGHT -> + colorFunction = (x, z) -> Color.getHSBColor(renderer.getComplex().getHeightStream().get(x, z).floatValue(), 1f, 1f).getRGB(); + case CONTINENT -> colorFunction = (x, z) -> { + IrisBiome b = renderer.getBiome((int) Math.round(x), renderer.getMaxHeight() - 1, (int) Math.round(z)); + IrisBiomeGeneratorLink g = b.getGenerators().get(0); + if (g.getMax() <= 0) return BLUE; + if (g.getMin() < 0) return YELLOW; + return GREEN; + }; + } + + double x, z; + for (int i = 0; i < resolution; i++) { + x = IrisInterpolation.lerp(sx, sx + size, (double) i / (double) resolution); + for (int j = 0; j < resolution; j++) { + z = IrisInterpolation.lerp(sz, sz + size, (double) j / (double) resolution); + pixels[j * resolution + i] = colorFunction.apply(x, z); + } + } + + return image; + } +} 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 69fea385e..6e3200f75 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 @@ -47,6 +47,7 @@ import org.bukkit.inventory.ItemStack; import java.awt.Color; import java.util.List; +import java.util.Map; import java.util.concurrent.CompletableFuture; public interface INMSBinding { @@ -172,6 +173,10 @@ public interface INMSBinding { KMap collectStructures(); + default Map extractVanillaDatapack() { + return Map.of(); + } + private void validateDimensionTypes(WorldCreator c) { if (c.generator() instanceof PlatformChunkGenerator gen && missingDimensionTypes(gen.getTarget().getDimension().getDimensionTypeKey())) { diff --git a/core/src/main/java/art/arcane/iris/core/pack/BrokenPackException.java b/core/src/main/java/art/arcane/iris/core/pack/BrokenPackException.java new file mode 100644 index 000000000..898aed374 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pack/BrokenPackException.java @@ -0,0 +1,56 @@ +/* + * 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.pack; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public final class BrokenPackException extends RuntimeException { + private final String packName; + private final List reasons; + + public BrokenPackException(String packName, List reasons) { + super(buildMessage(packName, reasons)); + this.packName = packName; + this.reasons = reasons == null ? new ArrayList<>() : new ArrayList<>(reasons); + } + + public String getPackName() { + return packName; + } + + public List getReasons() { + return Collections.unmodifiableList(reasons); + } + + private static String buildMessage(String packName, List reasons) { + StringBuilder sb = new StringBuilder(); + sb.append("Iris pack '").append(packName).append("' is broken and cannot be used for world or studio creation."); + if (reasons != null) { + for (String reason : reasons) { + if (reason == null || reason.isBlank()) { + continue; + } + sb.append(System.lineSeparator()).append(" - ").append(reason); + } + } + return sb.toString(); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pack/IrisPackRepository.java b/core/src/main/java/art/arcane/iris/core/pack/IrisPackRepository.java index ba4119756..3a6f41803 100644 --- a/core/src/main/java/art/arcane/iris/core/pack/IrisPackRepository.java +++ b/core/src/main/java/art/arcane/iris/core/pack/IrisPackRepository.java @@ -45,7 +45,7 @@ public class IrisPackRepository { private String repo = "overworld"; @Builder.Default - private String branch = "master"; + private String branch = "stable"; @Builder.Default private String tag = ""; @@ -94,7 +94,7 @@ public class IrisPackRepository { return IrisPackRepository.builder() .user("IrisDimensions") .repo(g) - .branch(g.equals("overworld") ? "stable" : "master") + .branch("stable") .build(); } diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackValidationRegistry.java b/core/src/main/java/art/arcane/iris/core/pack/PackValidationRegistry.java new file mode 100644 index 000000000..9d4dcac10 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pack/PackValidationRegistry.java @@ -0,0 +1,64 @@ +/* + * 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.pack; + +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public final class PackValidationRegistry { + private static final Map RESULTS = new ConcurrentHashMap<>(); + + private PackValidationRegistry() { + } + + public static void publish(PackValidationResult result) { + if (result == null || result.getPackName() == null || result.getPackName().isBlank()) { + return; + } + RESULTS.put(result.getPackName(), result); + } + + public static PackValidationResult get(String packName) { + if (packName == null || packName.isBlank()) { + return null; + } + return RESULTS.get(packName); + } + + public static boolean isBroken(String packName) { + PackValidationResult result = get(packName); + return result != null && !result.isLoadable(); + } + + public static Map snapshot() { + return Collections.unmodifiableMap(RESULTS); + } + + public static void remove(String packName) { + if (packName == null || packName.isBlank()) { + return; + } + RESULTS.remove(packName); + } + + public static void clear() { + RESULTS.clear(); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackValidationResult.java b/core/src/main/java/art/arcane/iris/core/pack/PackValidationResult.java new file mode 100644 index 000000000..b979bdfaf --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pack/PackValidationResult.java @@ -0,0 +1,67 @@ +/* + * 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.pack; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public final class PackValidationResult { + private final String packName; + private final List blockingErrors; + private final List warnings; + private final List removedUnusedFiles; + private final long validatedAtMillis; + + public PackValidationResult(String packName, + List blockingErrors, + List warnings, + List removedUnusedFiles, + long validatedAtMillis) { + this.packName = packName; + this.blockingErrors = blockingErrors == null ? new ArrayList<>() : new ArrayList<>(blockingErrors); + this.warnings = warnings == null ? new ArrayList<>() : new ArrayList<>(warnings); + this.removedUnusedFiles = removedUnusedFiles == null ? new ArrayList<>() : new ArrayList<>(removedUnusedFiles); + this.validatedAtMillis = validatedAtMillis; + } + + public String getPackName() { + return packName; + } + + public boolean isLoadable() { + return blockingErrors.isEmpty(); + } + + public List getBlockingErrors() { + return Collections.unmodifiableList(blockingErrors); + } + + public List getWarnings() { + return Collections.unmodifiableList(warnings); + } + + public List getRemovedUnusedFiles() { + return Collections.unmodifiableList(removedUnusedFiles); + } + + public long getValidatedAtMillis() { + return validatedAtMillis; + } +} 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 new file mode 100644 index 000000000..83a8e7d66 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pack/PackValidator.java @@ -0,0 +1,369 @@ +/* + * 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.pack; + +import art.arcane.iris.Iris; +import art.arcane.volmlib.util.json.JSONArray; +import art.arcane.volmlib.util.json.JSONObject; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +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 OBJECTS_FOLDER = "objects"; + private static final String DIMENSIONS_FOLDER = "dimensions"; + private static final List MANAGED_RESOURCE_FOLDERS = List.of( + "biomes", + "regions", + "entities", + "spawners", + "loot", + "generators", + "expressions", + "markers", + "blocks", + "mods" + ); + private static final DateTimeFormatter TRASH_STAMP = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss"); + + private PackValidator() { + } + + public static PackValidationResult validate(File packFolder) { + String packName = packFolder == null ? "" : packFolder.getName(); + List blockingErrors = new ArrayList<>(); + List warnings = new ArrayList<>(); + List removedUnusedFiles = new ArrayList<>(); + long validatedAt = System.currentTimeMillis(); + + if (packFolder == null || !packFolder.isDirectory()) { + blockingErrors.add("Pack folder does not exist or is not a directory."); + return new PackValidationResult(packName, blockingErrors, warnings, removedUnusedFiles, validatedAt); + } + + File dimensionsFolder = new File(packFolder, DIMENSIONS_FOLDER); + if (!dimensionsFolder.isDirectory()) { + blockingErrors.add("Missing dimensions/ folder."); + return new PackValidationResult(packName, blockingErrors, warnings, removedUnusedFiles, validatedAt); + } + + File[] dimensionFiles = dimensionsFolder.listFiles(f -> f.isFile() && f.getName().endsWith(".json")); + if (dimensionFiles == null || dimensionFiles.length == 0) { + blockingErrors.add("No dimension JSON files under dimensions/."); + return new PackValidationResult(packName, blockingErrors, warnings, removedUnusedFiles, validatedAt); + } + + validateDimensions(packFolder, dimensionFiles, blockingErrors, warnings); + + try { + String packTextCorpus = buildPackTextCorpus(packFolder); + runUnusedResourceGc(packFolder, packTextCorpus, removedUnusedFiles, warnings); + } catch (Throwable e) { + Iris.reportError("PackValidator GC pass failed for pack '" + packName + "'", e); + warnings.add("Unused-resource GC pass failed: " + e.getMessage()); + } + + return new PackValidationResult(packName, blockingErrors, warnings, removedUnusedFiles, validatedAt); + } + + private static void validateDimensions(File packFolder, File[] dimensionFiles, List blockingErrors, List warnings) { + File regionsFolder = new File(packFolder, "regions"); + File biomesFolder = new File(packFolder, "biomes"); + + for (File dimFile : dimensionFiles) { + String dimensionKey = stripExtension(dimFile.getName()); + JSONObject dimJson; + try { + dimJson = new JSONObject(Files.readString(dimFile.toPath(), StandardCharsets.UTF_8)); + } catch (Throwable e) { + blockingErrors.add("Dimension '" + dimensionKey + "' has invalid JSON: " + e.getMessage()); + continue; + } + + JSONArray regionsArray = dimJson.optJSONArray("regions"); + if (regionsArray == null || regionsArray.length() == 0) { + blockingErrors.add("Dimension '" + dimensionKey + "' declares no regions."); + continue; + } + + int resolvedRegions = 0; + for (int i = 0; i < regionsArray.length(); i++) { + String regionKey = regionsArray.optString(i, null); + if (regionKey == null || regionKey.isBlank()) { + warnings.add("Dimension '" + dimensionKey + "' has a blank region entry at index " + i + "."); + continue; + } + File regionFile = new File(regionsFolder, regionKey + ".json"); + if (!regionFile.isFile()) { + blockingErrors.add("Dimension '" + dimensionKey + "' references missing region '" + regionKey + "'."); + continue; + } + + JSONObject regionJson; + try { + regionJson = new JSONObject(Files.readString(regionFile.toPath(), StandardCharsets.UTF_8)); + } catch (Throwable e) { + blockingErrors.add("Region '" + regionKey + "' has invalid JSON: " + e.getMessage()); + continue; + } + + int anyBiome = countBiomeRefs(regionJson, "landBiomes", biomesFolder, regionKey, warnings) + + countBiomeRefs(regionJson, "seaBiomes", biomesFolder, regionKey, warnings) + + countBiomeRefs(regionJson, "shoreBiomes", biomesFolder, regionKey, warnings) + + countBiomeRefs(regionJson, "caveBiomes", biomesFolder, regionKey, warnings); + if (anyBiome == 0) { + blockingErrors.add("Region '" + regionKey + "' has no resolvable biomes."); + } + resolvedRegions++; + } + + if (resolvedRegions == 0) { + blockingErrors.add("Dimension '" + dimensionKey + "' has no resolvable regions."); + } + } + } + + private static int countBiomeRefs(JSONObject regionJson, String field, File biomesFolder, String regionKey, List warnings) { + JSONArray arr = regionJson.optJSONArray(field); + if (arr == null) { + return 0; + } + int resolved = 0; + for (int i = 0; i < arr.length(); i++) { + String biomeKey = arr.optString(i, null); + if (biomeKey == null || biomeKey.isBlank()) { + continue; + } + File biomeFile = new File(biomesFolder, biomeKey + ".json"); + if (!biomeFile.isFile()) { + warnings.add("Region '" + regionKey + "' references missing biome '" + biomeKey + "' in " + field + "."); + continue; + } + resolved++; + } + return resolved; + } + + private static String buildPackTextCorpus(File packFolder) { + StringBuilder sb = new StringBuilder(1 << 16); + try (Stream stream = Files.walk(packFolder.toPath())) { + stream.filter(Files::isRegularFile) + .filter(PackValidator::isScannableJsonPath) + .forEach(p -> { + try { + sb.append(Files.readString(p, StandardCharsets.UTF_8)); + sb.append('\n'); + } catch (Throwable ignored) { + } + }); + } catch (Throwable e) { + Iris.reportError("PackValidator failed to walk pack folder for corpus scan", e); + } + return sb.toString(); + } + + private static boolean isScannableJsonPath(Path path) { + String name = path.getFileName().toString(); + if (!name.endsWith(".json")) { + return false; + } + String str = path.toString().replace(File.separatorChar, '/'); + if (str.contains("/" + TRASH_ROOT + "/")) { + return false; + } + if (str.contains("/" + DATAPACK_IMPORTS + "/")) { + return false; + } + if (str.contains("/" + EXTERNAL_DATAPACKS + "/")) { + return false; + } + if (str.contains("/" + OBJECTS_FOLDER + "/")) { + return false; + } + if (str.contains("/.iris/")) { + return false; + } + return true; + } + + private static void runUnusedResourceGc(File packFolder, String corpus, List removedUnusedFiles, List warnings) { + if (corpus == null || corpus.isEmpty()) { + return; + } + File trashRoot = new File(packFolder, TRASH_ROOT + File.separator + LocalDateTime.now().format(TRASH_STAMP)); + Set scheduledForTrash = new LinkedHashSet<>(); + + for (String folderName : MANAGED_RESOURCE_FOLDERS) { + File resourceFolder = new File(packFolder, folderName); + if (!resourceFolder.isDirectory()) { + continue; + } + + List files = listJsonRecursive(resourceFolder); + for (File resourceFile : files) { + String key = deriveKey(resourceFolder, resourceFile); + if (key == null || key.isBlank()) { + continue; + } + if (isReferenced(corpus, key)) { + continue; + } + scheduledForTrash.add(resourceFile); + } + } + + if (scheduledForTrash.isEmpty()) { + return; + } + + for (File file : scheduledForTrash) { + try { + Path src = file.toPath(); + Path relative = packFolder.toPath().relativize(src); + Path dest = trashRoot.toPath().resolve(relative); + Files.createDirectories(dest.getParent()); + Files.move(src, dest, StandardCopyOption.REPLACE_EXISTING); + removedUnusedFiles.add(relative.toString().replace(File.separatorChar, '/')); + } catch (Throwable e) { + Iris.reportError("PackValidator failed to move unused file " + file.getPath() + " to trash", e); + warnings.add("Failed to quarantine unused file " + file.getName() + ": " + e.getMessage()); + } + } + } + + private static boolean isReferenced(String corpus, String key) { + String needleQuoted = "\"" + key + "\""; + if (corpus.contains(needleQuoted)) { + return true; + } + int slash = key.indexOf('/'); + if (slash > 0) { + String tail = key.substring(slash + 1); + if (!tail.isBlank() && corpus.contains("\"" + tail + "\"")) { + return true; + } + } + return false; + } + + private static List listJsonRecursive(File root) { + List out = new ArrayList<>(); + try (Stream stream = Files.walk(root.toPath())) { + stream.filter(Files::isRegularFile) + .filter(p -> p.getFileName().toString().endsWith(".json")) + .forEach(p -> out.add(p.toFile())); + } catch (Throwable ignored) { + } + return out; + } + + private static String deriveKey(File resourceFolder, File resourceFile) { + Path relative = resourceFolder.toPath().relativize(resourceFile.toPath()); + String str = relative.toString().replace(File.separatorChar, '/'); + if (!str.endsWith(".json")) { + return null; + } + return str.substring(0, str.length() - ".json".length()); + } + + private static String stripExtension(String name) { + int dot = name.lastIndexOf('.'); + return dot <= 0 ? name : name.substring(0, dot); + } + + public static int restoreTrash(File packFolder) { + if (packFolder == null || !packFolder.isDirectory()) { + return 0; + } + File trashRoot = new File(packFolder, TRASH_ROOT); + if (!trashRoot.isDirectory()) { + return 0; + } + File[] dumps = trashRoot.listFiles(File::isDirectory); + if (dumps == null || dumps.length == 0) { + return 0; + } + Arrays.sort(dumps, Comparator.comparing(File::getName)); + File latestDump = dumps[dumps.length - 1]; + int restored = 0; + try (Stream stream = Files.walk(latestDump.toPath())) { + List files = stream.filter(Files::isRegularFile).toList(); + for (Path src : files) { + Path relative = latestDump.toPath().relativize(src); + Path dest = packFolder.toPath().resolve(relative); + Files.createDirectories(dest.getParent()); + Files.move(src, dest, StandardCopyOption.REPLACE_EXISTING); + restored++; + } + } catch (Throwable e) { + Iris.reportError("PackValidator failed to restore trash for pack " + packFolder.getName(), e); + } + deleteFolderQuiet(latestDump); + return restored; + } + + private static void deleteFolderQuiet(File folder) { + if (folder == null || !folder.exists()) { + return; + } + try (Stream stream = Files.walk(folder.toPath())) { + stream.sorted(Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(File::delete); + } catch (Throwable ignored) { + } + } + + public static Set listReferencedKeysFromCorpus(String corpus) { + Set keys = new HashSet<>(); + if (corpus == null) { + return keys; + } + int i = 0; + while (i < corpus.length()) { + int start = corpus.indexOf('"', i); + if (start < 0) { + break; + } + int end = corpus.indexOf('"', start + 1); + if (end < 0) { + break; + } + keys.add(corpus.substring(start + 1, end)); + i = end + 1; + } + return keys; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/DeepSearchPregenerator.java b/core/src/main/java/art/arcane/iris/core/pregenerator/DeepSearchPregenerator.java index 55d241e3e..5f18c617a 100644 --- a/core/src/main/java/art/arcane/iris/core/pregenerator/DeepSearchPregenerator.java +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/DeepSearchPregenerator.java @@ -157,16 +157,15 @@ public class DeepSearchPregenerator extends Thread implements Listener { for (int j = 0; j < 16; j++) { int height = engine.getHeight(xx + i, zz + j); if (height > 300) { - File found = new File("plugins" + "iris" + "found.txt"); - FileWriter writer = new FileWriter(found); - if (!found.exists()) { - found.createNewFile(); - } + File found = new File("plugins", "iris" + File.separator + "found.txt"); + found.getParentFile().mkdirs(); IrisBiome biome = engine.getBiome(xx, engine.getHeight(), zz); - Iris.info("Found at! " + xx + ", " + zz + "Biome ID: " + biome.getName() + ", "); - writer.write("Biome at: X: " + xx + " Z: " + zz + "Biome ID: " + biome.getName() + ", "); + Iris.info("Found at! " + xx + ", " + zz + " Biome ID: " + biome.getName()); + try (FileWriter writer = new FileWriter(found, true)) { + writer.write("Biome at: X: " + xx + " Z: " + zz + " Biome ID: " + biome.getName() + "\n"); + } return; - } + } } } } diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/IrisPregenerator.java b/core/src/main/java/art/arcane/iris/core/pregenerator/IrisPregenerator.java index 82903f819..6275b18ee 100644 --- a/core/src/main/java/art/arcane/iris/core/pregenerator/IrisPregenerator.java +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/IrisPregenerator.java @@ -156,11 +156,16 @@ public class IrisPregenerator { } private long computeETA() { - double d = (long) (generated.get() > 1024 ? // Generated chunks exceed 1/8th of total? - // If yes, use smooth function (which gets more accurate over time since its less sensitive to outliers) - ((totalChunks.get() - generated.get()) * ((double) (M.ms() - startTime.get()) / (double) generated.get())) : - // If no, use quick function (which is less accurate over time but responds better to the initial delay) - ((totalChunks.get() - generated.get()) / chunksPerSecond.getAverage()) * 1000); + long gen = generated.get(); + long total = totalChunks.get(); + long remaining = total - gen; + double d; + if (gen > 1024) { + d = remaining * ((double) (M.ms() - startTime.get()) / (double) gen); + } else { + double cps = chunksPerSecond.getAverage(); + d = cps > 0 ? (remaining / cps) * 1000 : 0; + } return Double.isFinite(d) && d != INVALID ? (long) d : 0; } @@ -175,11 +180,7 @@ public class IrisPregenerator { checkRegions(); PrecisionStopwatch p = PrecisionStopwatch.start(); task.iterateRegions((x, z) -> visitRegion(x, z, true)); - if (generator.isAsyncChunkMode()) { - visitChunksInterleaved(); - } else { - task.iterateRegions((x, z) -> visitRegion(x, z, false)); - } + task.iterateRegions((x, z) -> visitRegion(x, z, false)); Iris.info("Pregen took " + Form.duration((long) p.getMilliseconds())); shutdown(); if (benchmarking == null) { @@ -248,6 +249,10 @@ public class IrisPregenerator { if (saveLatch.flip()) { listener.onSaving(); generator.save(); + Mantle mantle = getMantle(); + if (mantle != null) { + mantle.trim(0, 0); + } } generatedRegions.add(pos); @@ -263,46 +268,6 @@ public class IrisPregenerator { generator.supportsRegions(x, z, listener); } - private void visitChunksInterleaved() { - task.iterateAllChunksInterleaved((regionX, regionZ, chunkX, chunkZ, firstChunkInRegion, lastChunkInRegion) -> { - while (paused.get() && !shutdown.get()) { - J.sleep(50); - } - - Position2 regionPos = new Position2(regionX, regionZ); - if (shutdown.get()) { - if (!generatedRegions.contains(regionPos)) { - listener.onRegionSkipped(regionX, regionZ); - generatedRegions.add(regionPos); - } - return false; - } - - if (generatedRegions.contains(regionPos)) { - return true; - } - - if (firstChunkInRegion) { - currentGeneratorMethod.set(generator.getMethod(regionX, regionZ)); - listener.onRegionGenerating(regionX, regionZ); - } - - generator.generateChunk(chunkX, chunkZ, listener); - - if (lastChunkInRegion) { - listener.onRegionGenerated(regionX, regionZ); - if (saveLatch.flip()) { - listener.onSaving(); - generator.save(); - } - generatedRegions.add(regionPos); - checkRegions(); - } - - return true; - }); - } - public void pause() { paused.set(true); } diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/PregenTask.java b/core/src/main/java/art/arcane/iris/core/pregenerator/PregenTask.java index 1ded599f8..1a29140f8 100644 --- a/core/src/main/java/art/arcane/iris/core/pregenerator/PregenTask.java +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/PregenTask.java @@ -121,56 +121,6 @@ public class PregenTask { iterateRegions(((rX, rZ) -> iterateChunks(rX, rZ, s))); } - public void iterateAllChunksInterleaved(InterleavedChunkSpiraled spiraled) { - if (spiraled == null) { - return; - } - - KList cursors = new KList<>(); - Bound bound = bounds.chunk(); - iterateRegions((regionX, regionZ) -> { - RegionChunkCursor cursor = new RegionChunkCursor(regionX, regionZ, bound); - if (cursor.hasNext()) { - cursors.add(cursor); - } - }); - - boolean hasProgress = true; - while (hasProgress) { - hasProgress = false; - for (RegionChunkCursor cursor : cursors) { - if (!cursor.hasNext()) { - continue; - } - - hasProgress = true; - long chunk = cursor.next(); - if (chunk == Long.MIN_VALUE) { - continue; - } - int chunkX = (int) (chunk >> 32); - int chunkZ = (int) chunk; - - boolean shouldContinue = spiraled.on( - cursor.getRegionX(), - cursor.getRegionZ(), - chunkX, - chunkZ, - cursor.getIndex() == 1, - !cursor.hasNext() - ); - if (!shouldContinue) { - return; - } - } - } - } - - @FunctionalInterface - public interface InterleavedChunkSpiraled { - boolean on(int regionX, int regionZ, int chunkX, int chunkZ, boolean firstChunkInRegion, boolean lastChunkInRegion); - } - private class Bounds { private Bound chunk = null; private Bound region = null; @@ -206,9 +156,9 @@ public class PregenTask { } } - private record Bound(int minX, int maxX, int minZ, int maxZ, int sizeX, int sizeZ) { + private record Bound(int minX, int minZ, int maxX, int maxZ, int sizeX, int sizeZ) { private Bound(int minX, int minZ, int maxX, int maxZ) { - this(minX, maxX, minZ, maxZ, maxZ - minZ + 1, maxZ - minZ + 1); + this(minX, minZ, maxX, maxZ, maxX - minX + 1, maxZ - minZ + 1); } boolean check(int x, int z) { @@ -231,76 +181,4 @@ public class PregenTask { throw new IllegalStateException("This Position2 may not be modified"); } } - - private static final class RegionChunkCursor { - private final int regionX; - private final int regionZ; - private final Bound bound; - private final int[] order; - private final int chunkOffsetX; - private final int chunkOffsetZ; - private int scanIndex; - private int emittedIndex; - private int currentChunkX; - private int currentChunkZ; - private boolean hasCurrent; - - private RegionChunkCursor(int regionX, int regionZ, Bound bound) { - this.regionX = regionX; - this.regionZ = regionZ; - this.bound = bound; - this.chunkOffsetX = PowerOfTwoCoordinates.regionToChunk(regionX); - this.chunkOffsetZ = PowerOfTwoCoordinates.regionToChunk(regionZ); - this.order = orderForPull(-chunkOffsetX, -chunkOffsetZ); - this.scanIndex = 0; - this.emittedIndex = 0; - advance(); - } - - private boolean hasNext() { - return hasCurrent; - } - - private long next() { - if (!hasNext()) { - return Long.MIN_VALUE; - } - - long high = (long) currentChunkX << 32; - long low = currentChunkZ & 0xFFFFFFFFL; - emittedIndex++; - advance(); - return high | low; - } - - private void advance() { - hasCurrent = false; - while (scanIndex < order.length) { - int local = order[scanIndex]; - scanIndex++; - int chunkX = chunkOffsetX + PowerOfTwoCoordinates.unpackLocal32X(local); - int chunkZ = chunkOffsetZ + PowerOfTwoCoordinates.unpackLocal32Z(local); - if (!bound.check(chunkX, chunkZ)) { - continue; - } - - currentChunkX = chunkX; - currentChunkZ = chunkZ; - hasCurrent = true; - return; - } - } - - private int getRegionX() { - return regionX; - } - - private int getRegionZ() { - return regionZ; - } - - private int getIndex() { - return emittedIndex; - } - } } diff --git a/core/src/main/java/art/arcane/iris/core/project/IrisProject.java b/core/src/main/java/art/arcane/iris/core/project/IrisProject.java index 53c422ef8..9b58bf485 100644 --- a/core/src/main/java/art/arcane/iris/core/project/IrisProject.java +++ b/core/src/main/java/art/arcane/iris/core/project/IrisProject.java @@ -293,49 +293,138 @@ public class IrisProject { return future; } + private static final int STUDIO_PROGRESS_BAR_WIDTH = 44; + private void startStudioOpenReporter(VolmitSender sender, AtomicReference stage, AtomicReference progress, AtomicBoolean complete, AtomicBoolean failed) { - String[] spinner = {"|", "/", "-", "\\"}; - AtomicInteger spinIndex = new AtomicInteger(0); AtomicLong nextConsoleUpdate = new AtomicLong(0L); + AtomicLong startMs = new AtomicLong(System.currentTimeMillis()); AtomicInteger taskId = new AtomicInteger(-1); + org.bukkit.boss.BossBar bossBar; + + if (sender.isPlayer() && sender.player() != null) { + bossBar = Bukkit.createBossBar( + C.GOLD + "Studio " + C.AQUA + "OPENING", + org.bukkit.boss.BarColor.BLUE, + org.bukkit.boss.BarStyle.SEGMENTED_20 + ); + bossBar.setProgress(0.0D); + bossBar.addPlayer(sender.player()); + bossBar.setVisible(true); + } else { + bossBar = null; + } int scheduledTaskId = J.ar(() -> { + double currentProgress = Math.max(0D, Math.min(0.99D, progress.get())); + String currentStage = describeStage(stage.get()); + int percent = (int) Math.round(currentProgress * 100.0D); + long elapsed = System.currentTimeMillis() - startMs.get(); + if (complete.get()) { J.car(taskId.get()); + if (failed.get()) { + if (bossBar != null) { + bossBar.setProgress(Math.max(0.0D, Math.min(1.0D, currentProgress))); + bossBar.setColor(org.bukkit.boss.BarColor.RED); + bossBar.setTitle(C.GOLD + "Studio " + C.RED + "FAILED" + C.GRAY + " " + C.YELLOW + percent + "%"); + J.a(() -> { bossBar.removeAll(); bossBar.setVisible(false); }, 60); + } if (sender.isPlayer()) { - sender.sendProgress(1D, "Studio open failed"); + String action = buildStudioProgressBar(currentProgress) + + C.GRAY + " " + C.RED + "FAILED" + + C.GRAY + " | " + C.WHITE + currentStage; + sender.sendAction(action); } else { sender.sendMessage(C.RED + "Studio open failed."); } - } else if (sender.isPlayer()) { - sender.sendProgress(1D, "Studio ready"); } else { - sender.sendMessage(C.GREEN + "Studio ready."); + if (bossBar != null) { + bossBar.setProgress(1.0D); + bossBar.setColor(org.bukkit.boss.BarColor.GREEN); + bossBar.setTitle(C.GOLD + "Studio " + C.GREEN + "READY" + C.GRAY + " " + C.YELLOW + "100%"); + J.a(() -> { bossBar.removeAll(); bossBar.setVisible(false); }, 60); + } + if (sender.isPlayer()) { + String action = buildStudioProgressBar(1.0D) + + C.GRAY + " " + C.GREEN + "100%" + + C.GRAY + " | " + C.GREEN + "Studio ready" + + C.DARK_GRAY + " " + Form.duration(elapsed, 1); + sender.sendAction(action); + } else { + sender.sendMessage(C.GREEN + "Studio ready " + C.GRAY + "(" + Form.duration(elapsed, 1) + ")"); + } } return; } - double currentProgress = Math.max(0D, Math.min(0.97D, progress.get())); - String currentStage = stage.get(); - String currentSpinner = spinner[Math.floorMod(spinIndex.getAndIncrement(), spinner.length)]; + if (sender.isPlayer() && sender.player() != null) { + if (bossBar != null) { + bossBar.setProgress(Math.max(0.0D, Math.min(1.0D, currentProgress))); + bossBar.setTitle(C.GOLD + "Studio " + C.AQUA + "OPENING" + C.GRAY + " " + C.YELLOW + percent + "%"); + } - if (sender.isPlayer()) { - sender.sendProgress(currentProgress, "Studio " + currentSpinner + " " + currentStage); - return; - } - - long now = System.currentTimeMillis(); - long nextUpdate = nextConsoleUpdate.get(); - if (now >= nextUpdate) { - sender.sendMessage(C.WHITE + "Studio " + Form.pc(currentProgress, 0) + C.GRAY + " - " + currentStage); - nextConsoleUpdate.set(now + 1500L); + String action = buildStudioProgressBar(currentProgress) + + C.GRAY + " " + C.YELLOW + percent + "%" + + C.GRAY + " | " + C.WHITE + currentStage + + C.DARK_GRAY + " " + Form.duration(elapsed, 0); + sender.sendAction(action); + } else { + long now = System.currentTimeMillis(); + long nextUpdate = nextConsoleUpdate.get(); + if (now >= nextUpdate) { + String bar = buildStudioConsoleBar(currentProgress); + sender.sendMessage(C.GOLD + "Studio " + C.AQUA + bar + " " + C.YELLOW + percent + "%" + C.GRAY + " " + currentStage + C.DARK_GRAY + " (" + Form.duration(elapsed, 0) + ")"); + nextConsoleUpdate.set(now + 1500L); + } } }, 3); taskId.set(scheduledTaskId); } + private static String buildStudioProgressBar(double progress) { + int filled = (int) Math.round(Math.max(0.0D, Math.min(1.0D, progress)) * STUDIO_PROGRESS_BAR_WIDTH); + StringBuilder bar = new StringBuilder(STUDIO_PROGRESS_BAR_WIDTH * 3 + 4); + bar.append(C.DARK_GRAY).append("["); + for (int i = 0; i < STUDIO_PROGRESS_BAR_WIDTH; i++) { + bar.append(i < filled ? C.GREEN : C.DARK_GRAY).append("|"); + } + bar.append(C.DARK_GRAY).append("]"); + return bar.toString(); + } + + private static String buildStudioConsoleBar(double progress) { + int width = 20; + int filled = (int) Math.round(Math.max(0.0D, Math.min(1.0D, progress)) * width); + StringBuilder bar = new StringBuilder(); + bar.append("["); + for (int i = 0; i < width; i++) { + bar.append(i < filled ? "#" : "-"); + } + bar.append("]"); + return bar.toString(); + } + + private static String describeStage(String stage) { + if (stage == null || stage.isBlank()) return "Initializing"; + return switch (stage) { + case "Queued" -> "Queued"; + case "resolve_dimension" -> "Resolving dimension"; + case "prepare_world_pack" -> "Preparing world pack"; + case "install_datapacks" -> "Installing datapacks"; + case "create_world" -> "Creating world"; + case "apply_world_rules" -> "Applying world rules"; + case "prepare_generator" -> "Preparing generator"; + case "request_entry_chunk" -> "Loading entry chunk"; + case "resolve_safe_entry" -> "Finding safe spawn"; + case "teleport_player" -> "Teleporting"; + case "finalize_open" -> "Finalizing"; + case "cleanup" -> "Cleaning up"; + default -> Form.capitalizeWords(stage.replace('_', ' ')); + }; + } + public CompletableFuture close() { if (activeProvider == null) { return CompletableFuture.completedFuture(new StudioOpenCoordinator.StudioCloseResult(null, true, true, false, null)); diff --git a/core/src/main/java/art/arcane/iris/core/runtime/WorldRuntimeControlService.java b/core/src/main/java/art/arcane/iris/core/runtime/WorldRuntimeControlService.java index 61cde3b0c..4d9067519 100644 --- a/core/src/main/java/art/arcane/iris/core/runtime/WorldRuntimeControlService.java +++ b/core/src/main/java/art/arcane/iris/core/runtime/WorldRuntimeControlService.java @@ -294,19 +294,12 @@ public final class WorldRuntimeControlService { } static int[] buildSafeLocationScanOrder(World world, Location source) { - int x = source.getBlockX(); - int z = source.getBlockZ(); int minY = world.getMinHeight() + 1; int maxY = world.getMaxHeight() - 2; - int highestY = Math.max(minY, Math.min(maxY, world.getHighestBlockYAt(x, z) + 1)); int[] scanOrder = new int[maxY - minY + 1]; int index = 0; - for (int y = highestY; y >= minY; y--) { - scanOrder[index++] = y; - } - - for (int y = highestY + 1; y <= maxY; y++) { + for (int y = maxY; y >= minY; y--) { scanOrder[index++] = y; } 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 2edecc780..0d9467074 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 @@ -26,6 +26,8 @@ import art.arcane.iris.core.lifecycle.WorldLifecycleService; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.nms.INMS; import art.arcane.iris.core.pack.IrisPack; +import art.arcane.iris.core.pack.PackValidationRegistry; +import art.arcane.iris.core.pack.PackValidationResult; import art.arcane.iris.core.project.IrisProject; import art.arcane.iris.core.runtime.TransientWorldCleanupSupport; import art.arcane.iris.core.tools.IrisToolbelt; @@ -69,12 +71,12 @@ public class StudioSVC implements IrisService { File f = IrisPack.packsPack(pack); if (!f.exists()) { - Iris.info("Downloading Default Pack " + pack); 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); } else { - downloadSearch(Iris.getSender(), pack, false); + Iris.warn("Default pack '" + pack + "' is not installed. Please download it manually with /iris download"); } } }); @@ -130,11 +132,14 @@ public class StudioSVC implements IrisService { IrisDimension dim = IrisData.loadAnyDimension(type, null); if (dim == null) { - for (File i : getWorkspaceFolder().listFiles()) { - if (i.isFile() && i.getName().equals(type + ".iris")) { - sender.sendMessage("Found " + type + ".iris in " + WORKSPACE_NAME + " folder"); - ZipUtil.unpack(i, folder); - break; + File[] workspaceFiles = getWorkspaceFolder().listFiles(); + if (workspaceFiles != null) { + for (File i : workspaceFiles) { + if (i.isFile() && i.getName().equals(type + ".iris")) { + sender.sendMessage("Found " + type + ".iris in " + WORKSPACE_NAME + " folder"); + ZipUtil.unpack(i, folder); + break; + } } } } else { @@ -153,26 +158,29 @@ public class StudioSVC implements IrisService { if (!dimensionFile.exists() || !dimensionFile.isFile()) { downloadSearch(sender, type, false); File downloaded = getWorkspaceFolder(type); + File[] files = downloaded.listFiles(); - for (File i : downloaded.listFiles()) { - if (i.isFile()) { - try { - FileUtils.copyFile(i, new File(folder, i.getName())); - } catch (IOException e) { - e.printStackTrace(); - Iris.reportError(e); - } - } else { - try { - FileUtils.copyDirectory(i, new File(folder, i.getName())); - } catch (IOException e) { - e.printStackTrace(); - Iris.reportError(e); + if (files != null) { + for (File i : files) { + if (i.isFile()) { + try { + FileUtils.copyFile(i, new File(folder, i.getName())); + } catch (IOException e) { + e.printStackTrace(); + Iris.reportError(e); + } + } else { + try { + FileUtils.copyDirectory(i, new File(folder, i.getName())); + } catch (IOException e) { + e.printStackTrace(); + Iris.reportError(e); + } } } - } - IO.delete(downloaded); + IO.delete(downloaded); + } } if (!dimensionFile.exists() || !dimensionFile.isFile()) { @@ -198,26 +206,24 @@ public class StudioSVC implements IrisService { } public void downloadSearch(VolmitSender sender, String key, boolean trim, boolean forceOverwrite) { - String url = "?"; - try { - url = getListing(false).get(key); + String url = getListing(false).get(key); if (url == null) { - Iris.warn("ITS ULL for " + key); + sender.sendMessage("Pack '" + key + "' was not found in the pack listing."); + sender.sendMessage("Use /iris download to download manually."); + return; } - url = url == null ? key : url; - Iris.info("Assuming URL " + url); - String branch = "master"; + Iris.info("Resolved pack '" + key + "' to " + url); String[] nodes = url.split("\\Q/\\E"); String repo = nodes.length == 1 ? "IrisDimensions/" + nodes[0] : nodes[0] + "/" + nodes[1]; - branch = nodes.length > 2 ? nodes[2] : branch; + String branch = nodes.length > 2 ? nodes[2] : "stable"; download(sender, repo, branch, trim, forceOverwrite, false); } catch (Throwable e) { Iris.reportError(e); e.printStackTrace(); - sender.sendMessage("Failed to download '" + key + "' from " + url + "."); + sender.sendMessage("Failed to download '" + key + "'."); } } @@ -246,7 +252,7 @@ public class StudioSVC implements IrisService { if (zip == null || !zip.exists()) { sender.sendMessage("Failed to find pack at " + url); sender.sendMessage("Make sure you specified the correct repo and branch!"); - sender.sendMessage("For example: /iris download IrisDimensions/overworld branch=master"); + sender.sendMessage("For example: /iris download IrisDimensions/overworld branch=stable"); return; } sender.sendMessage("Unpacking " + repo); @@ -355,9 +361,6 @@ public class StudioSVC implements IrisService { l.put(i, a.getString(i)); } - // TEMP FIX - l.put("IrisDimensions/overworld/master", "IrisDimensions/overworld/stable"); - l.put("overworld", "IrisDimensions/overworld/stable"); return l; } @@ -379,7 +382,23 @@ public class StudioSVC implements IrisService { } } + private static boolean blockIfPackBroken(VolmitSender sender, String dimm) { + PackValidationResult validation = PackValidationRegistry.get(dimm); + if (validation == null || validation.isLoadable()) { + return false; + } + sender.sendMessage("Cannot open studio '" + dimm + "' - pack has blocking errors:"); + for (String reason : validation.getBlockingErrors()) { + sender.sendMessage(" - " + reason); + } + sender.sendMessage("Fix the pack and run /iris pack validate " + dimm + " to revalidate."); + return true; + } + public void open(VolmitSender sender, long seed, String dimm, Consumer onDone) throws IrisException { + if (blockIfPackBroken(sender, dimm)) { + return; + } CompletableFuture pendingClose = close(); pendingClose.whenComplete((closeResult, closeThrowable) -> { if (closeThrowable != null) { @@ -572,16 +591,18 @@ public class StudioSVC implements IrisService { public void create(VolmitSender sender, String s, String downloadable) { boolean shouldDelete = false; File importPack = getWorkspaceFolder(downloadable); + File[] packFiles = importPack.listFiles(); - if (importPack.listFiles().length == 0) { + if (packFiles == null || packFiles.length == 0) { downloadSearch(sender, downloadable, false); + packFiles = importPack.listFiles(); - if (importPack.listFiles().length > 0) { + if (packFiles != null && packFiles.length > 0) { shouldDelete = true; } } - if (importPack.listFiles().length == 0) { + if (packFiles == null || packFiles.length == 0) { sender.sendMessage("Couldn't find the pack to create a new dimension from."); return; } 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 dfae20b5b..4c1ae3beb 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 @@ -336,12 +336,22 @@ public class IrisCreator { return; } + int percent = (int) Math.round(progress * 100.0D); + int remaining = required - generated; if (sender.isPlayer()) { - sender.sendProgress(progress, "Generating"); + int barWidth = 44; + int filled = (int) Math.round(Math.max(0.0D, Math.min(1.0D, progress)) * barWidth); + StringBuilder bar = new StringBuilder(barWidth * 3 + 4); + bar.append(C.DARK_GRAY).append("["); + for (int bi = 0; bi < barWidth; bi++) { + bar.append(bi < filled ? C.GREEN : C.DARK_GRAY).append("|"); + } + bar.append(C.DARK_GRAY).append("]"); + sender.sendAction(bar.toString() + C.GRAY + " " + C.YELLOW + percent + "%" + C.DARK_GRAY + " " + Form.f(generated) + "/" + Form.f(required) + " chunks"); return; } - sender.sendMessage(C.WHITE + "Generating " + Form.pc(progress) + ((C.GRAY + " (" + (required - generated) + " Left)"))); + sender.sendMessage(C.GOLD + "Generating " + C.YELLOW + percent + "%" + C.GRAY + " " + Form.f(generated) + "/" + Form.f(required) + " chunks" + C.DARK_GRAY + " (" + remaining + " left)"); }, interval)); }); return taskId; @@ -356,12 +366,22 @@ public class IrisCreator { return; } + double p = progress.get(); + int percent = (int) Math.round(p * 100.0D); if (sender.isPlayer()) { - sender.sendProgress(progress.get(), "Pregenerating"); + int barWidth = 44; + int filled = (int) Math.round(Math.max(0.0D, Math.min(1.0D, p)) * barWidth); + StringBuilder bar = new StringBuilder(barWidth * 3 + 4); + bar.append(C.DARK_GRAY).append("["); + for (int bi = 0; bi < barWidth; bi++) { + bar.append(bi < filled ? C.GREEN : C.DARK_GRAY).append("|"); + } + bar.append(C.DARK_GRAY).append("]"); + sender.sendAction(bar.toString() + C.GRAY + " " + C.YELLOW + percent + "%" + C.GRAY + " | " + C.WHITE + "Pregenerating"); return; } - sender.sendMessage(C.WHITE + "Pregenerating " + Form.pc(progress.get())); + sender.sendMessage(C.GOLD + "Pregenerating " + C.YELLOW + percent + "%"); }, interval)); return taskId; } diff --git a/core/src/main/java/art/arcane/iris/engine/decorator/IrisCeilingDecorator.java b/core/src/main/java/art/arcane/iris/engine/decorator/IrisCeilingDecorator.java index abbbe7cc3..728c76d89 100644 --- a/core/src/main/java/art/arcane/iris/engine/decorator/IrisCeilingDecorator.java +++ b/core/src/main/java/art/arcane/iris/engine/decorator/IrisCeilingDecorator.java @@ -19,9 +19,11 @@ package art.arcane.iris.engine.decorator; import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.object.InferredType; import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisDecorationPart; import art.arcane.iris.engine.object.IrisDecorator; +import art.arcane.iris.util.common.data.B; import art.arcane.volmlib.util.documentation.BlockCoordinates; import art.arcane.iris.util.project.hunk.Hunk; import art.arcane.volmlib.util.math.RNG; @@ -40,9 +42,13 @@ public class IrisCeilingDecorator extends IrisEngineDecorator { 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) { RNG rng = getRNG(realX, realZ); IrisDecorator decorator = getDecorator(rng, biome, realX, realZ); + boolean caveSkipFluid = biome.getInferredType() == InferredType.CAVE; if (decorator != null) { if (!decorator.isStacking()) { + if (caveSkipFluid && B.isFluid(data.get(x, height, z))) { + return; + } data.set(x, height, z, fixFaces(decorator.getBlockData100(biome, rng, realX, height, realZ, getData()), data, x, z, realX, height, realZ)); } else { int stack = decorator.getHeight(rng, realX, realZ, getData()); @@ -53,6 +59,9 @@ public class IrisCeilingDecorator extends IrisEngineDecorator { } if (stack == 1) { + if (caveSkipFluid && B.isFluid(data.get(x, height, z))) { + return; + } data.set(x, height, z, decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData())); return; } @@ -92,6 +101,9 @@ public class IrisCeilingDecorator extends IrisEngineDecorator { ((PointedDripstone) bd).setVerticalDirection(BlockFace.DOWN); } + if (caveSkipFluid && B.isFluid(data.get(x, h, z))) { + break; + } data.set(x, h, z, bd); } } 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 8fdcf9b13..5eec3fdf9 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 @@ -51,6 +51,7 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator { IrisDecorator decorator = getDecorator(rng, biome, realX, realZ); bdx = data.get(x, height, z); boolean underwater = height < getDimension().getFluidHeight() && biome.getInferredType() != InferredType.CAVE; + boolean caveSkipFluid = biome.getInferredType() == InferredType.CAVE; if (decorator != null) { if (!decorator.isForcePlace() && !decorator.getSlopeCondition().isDefault() @@ -68,6 +69,9 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator { } if (decorator.getForceBlock() != null) { + if (caveSkipFluid && B.isFluid(data.get(x, height, z))) { + return; + } data.set(x, height, z, fixFaces(decorator.getForceBlock().getBlockData(getData()), data, x, z, realX, height, realZ)); } else if (!decorator.isForcePlace()) { if (decorator.getWhitelist() != null && decorator.getWhitelist().stream().noneMatch(d -> d.getBlockData(getData()).equals(bdx))) { @@ -82,7 +86,9 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator { bd = bd.clone(); ((Bisected) bd).setHalf(Bisected.Half.TOP); try { - data.set(x, height + 2, z, bd); + if (!caveSkipFluid || !B.isFluid(data.get(x, height + 2, z))) { + data.set(x, height + 2, z, bd); + } } catch (Throwable e) { Iris.reportError(e); } @@ -107,6 +113,9 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator { } if (stack == 1) { + if (caveSkipFluid && B.isFluid(data.get(x, height, z))) { + return; + } data.set(x, height, z, decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData())); return; } @@ -130,6 +139,10 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator { break; } + if (caveSkipFluid && B.isFluid(data.get(x, height + 1 + i, z))) { + break; + } + if (bd instanceof PointedDripstone) { PointedDripstone.Thickness th = PointedDripstone.Thickness.BASE; 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 225057891..2f3c637d0 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 @@ -960,50 +960,11 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat } default void gotoBiome(IrisBiome biome, Player player, boolean teleport) { - Set regionKeys = getDimension() - .getAllRegions(this).stream() - .filter((i) -> i.getAllBiomeIds().contains(biome.getLoadKey())) - .map(IrisRegistrant::getLoadKey) - .collect(Collectors.toSet()); - Locator lb = Locator.surfaceBiome(biome.getLoadKey()); - Locator locator = (engine, chunk) - -> regionKeys.contains(getRegion((chunk.getX() << 4) + 8, (chunk.getZ() << 4) + 8).getLoadKey()) - && lb.matches(engine, chunk); - - if (!regionKeys.isEmpty()) { - locator.find(player, teleport, "Biome " + biome.getName()); - } else { - player.sendMessage(C.RED + biome.getName() + " is not in any defined regions!"); - } + Locator.surfaceBiome(biome.getLoadKey()).find(player, teleport, "Biome " + biome.getName()); } default void gotoObject(String s, Player player, boolean teleport) { - Set biomeKeys = getDimension().getAllBiomes(this).stream() - .filter((i) -> i.getObjects().stream().anyMatch((f) -> f.getPlace().contains(s))) - .map(IrisRegistrant::getLoadKey) - .collect(Collectors.toSet()); - Set regionKeys = getDimension().getAllRegions(this).stream() - .filter((i) -> i.getAllBiomeIds().stream().anyMatch(biomeKeys::contains) - || i.getObjects().stream().anyMatch((f) -> f.getPlace().contains(s))) - .map(IrisRegistrant::getLoadKey) - .collect(Collectors.toSet()); - - Locator sl = Locator.object(s); - Locator locator = (engine, chunk) -> { - if (biomeKeys.contains(getSurfaceBiome((chunk.getX() << 4) + 8, (chunk.getZ() << 4) + 8).getLoadKey())) { - return sl.matches(engine, chunk); - } else if (regionKeys.contains(getRegion((chunk.getX() << 4) + 8, (chunk.getZ() << 4) + 8).getLoadKey())) { - return sl.matches(engine, chunk); - } - - return false; - }; - - if (!regionKeys.isEmpty()) { - locator.find(player, teleport, "Object " + s); - } else { - player.sendMessage(C.RED + s + " is not in any defined regions or biomes!"); - } + Locator.object(s).find(player, teleport, "Object " + s); } default boolean hasObjectPlacement(String objectKey) { diff --git a/core/src/main/java/art/arcane/iris/engine/framework/Locator.java b/core/src/main/java/art/arcane/iris/engine/framework/Locator.java index 35cd18ab9..d2b849f6f 100644 --- a/core/src/main/java/art/arcane/iris/engine/framework/Locator.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/Locator.java @@ -114,9 +114,14 @@ public interface Locator { boolean matches(Engine engine, Position2 chunk); default void find(Player player, boolean teleport, String message) { - find(player, location -> { + find(player, 120_000, location -> { + if (location == null) { + player.sendMessage(C.RED + "Could not find " + message + " within search range."); + return; + } if (teleport) { J.runEntity(player, () -> teleportAsyncSafely(player, location)); + player.sendMessage(C.GREEN + "Teleporting to " + message + "..."); } else { player.sendMessage(C.GREEN + message + " at: " + location.getBlockX() + " " + location.getBlockY() + " " + location.getBlockZ()); } @@ -124,7 +129,7 @@ public interface Locator { } default void find(Player player, Consumer consumer) { - find(player, 30_000, consumer); + find(player, 120_000, consumer); } default void find(Player player, long timeout, Consumer consumer) { @@ -137,11 +142,13 @@ public interface Locator { Position2 at = find(engine, new Position2(player.getLocation().getBlockX() >> 4, player.getLocation().getBlockZ() >> 4), timeout, checks::set).get(); if (at != null) { - consumer.accept(new Location(world, (at.getX() << 4) + 8, - engine.getHeight( - (at.getX() << 4) + 8, - (at.getZ() << 4) + 8, false), - (at.getZ() << 4) + 8)); + int bx = (at.getX() << 4) + 8; + int bz = (at.getZ() << 4) + 8; + consumer.accept(new Location(world, bx, + world.getHighestBlockYAt(bx, bz) + 2, + bz)); + } else { + consumer.accept(null); } } catch (WrongEngineBroException | InterruptedException | ExecutionException e) { e.printStackTrace(); @@ -172,18 +179,17 @@ public interface Locator { cancelSearch(); return MultiBurst.burst.completeValue(() -> { - int tc = IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism()) * 17; + int tc = IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism()) * 32; MultiBurst burst = MultiBurst.burst; AtomicBoolean found = new AtomicBoolean(false); - Position2 cursor = pos; AtomicInteger searched = new AtomicInteger(); AtomicBoolean stop = new AtomicBoolean(false); AtomicReference foundPos = new AtomicReference<>(); PrecisionStopwatch px = PrecisionStopwatch.start(); LocatorCanceller.cancel = () -> stop.set(true); - AtomicReference next = new AtomicReference<>(cursor); + AtomicReference next = new AtomicReference<>(pos); Spiraler s = new Spiraler(100000, 100000, (x, z) -> next.set(new Position2(x, z))); - s.setOffset(cursor.getX(), cursor.getZ()); + s.setOffset(pos.getX(), pos.getZ()); s.next(); while (!found.get() && !stop.get() && px.getMilliseconds() < timeout) { BurstExecutor e = burst.burst(tc); @@ -192,11 +198,11 @@ public interface Locator { Position2 p = next.get(); s.next(); e.queue(() -> { + if (found.get()) { + return; + } if (matches(engine, p)) { - if (foundPos.get() == null) { - foundPos.set(p); - } - + foundPos.compareAndSet(null, p); found.set(true); } searched.incrementAndGet(); diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java index 6eb3bcb63..2909bc15f 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java @@ -42,10 +42,8 @@ public class IrisCaveCarver3D { private static final byte LIQUID_LAVA = 2; private static final byte LIQUID_FORCED_AIR = 3; private static final int ADAPTIVE_MIN_PLANE_COLUMNS = 32; - private static final int ADAPTIVE_DEEP_MIN_PLANE_COLUMNS = 64; private static final int ADAPTIVE_DEEP_SAMPLE_STEP = 8; private static final int ADAPTIVE_DEEP_SURFACE_MARGIN = 12; - private static final int ADAPTIVE_DEEP_NEAR_SURFACE_DIVISOR = 4; private static final double ADAPTIVE_LOCAL_RANGE_SCALE = 0.25D; private static final double ADAPTIVE_DEEP_MARGIN_BOOST = 0.015D; private static final ThreadLocal SCRATCH = ThreadLocal.withInitial(Scratch::new); @@ -232,8 +230,6 @@ public class IrisCaveCarver3D { } } - int minCarveCells = Math.max(0, profile.getMinCarveCells()); - double recoveryThresholdBoost = Math.max(0, profile.getRecoveryThresholdBoost()); int carved; if (exactSampling) { if (adaptiveSampling) { @@ -258,29 +254,6 @@ public class IrisCaveCarver3D { 0D, false ); - if (carved < minCarveCells && recoveryThresholdBoost > 0D) { - carved += carvePassAdaptive( - chunk, - x0, - z0, - minY, - maxY, - adaptiveSampleStep, - adaptiveThresholdMargin, - surfaceBreakThresholdBoost, - columnMaxY, - surfaceBreakFloorY, - surfaceBreakColumn, - columnThreshold, - clampedWeights, - verticalEdgeFade, - matterByY, - resolvedMinWeight, - resolvedThresholdPenalty, - recoveryThresholdBoost, - true - ); - } } else { carved = carvePassExact( chunk, @@ -301,27 +274,6 @@ public class IrisCaveCarver3D { 0D, false ); - if (carved < minCarveCells && recoveryThresholdBoost > 0D) { - carved += carvePassExact( - chunk, - x0, - z0, - minY, - maxY, - surfaceBreakThresholdBoost, - columnMaxY, - surfaceBreakFloorY, - surfaceBreakColumn, - columnThreshold, - clampedWeights, - verticalEdgeFade, - matterByY, - resolvedMinWeight, - resolvedThresholdPenalty, - recoveryThresholdBoost, - true - ); - } } } else { int latticeStep = sampleStep; @@ -345,28 +297,6 @@ public class IrisCaveCarver3D { 0D, false ); - if (carved < minCarveCells && recoveryThresholdBoost > 0D) { - carved += carvePassLattice( - chunk, - x0, - z0, - minY, - maxY, - latticeStep, - surfaceBreakThresholdBoost, - columnMaxY, - surfaceBreakFloorY, - surfaceBreakColumn, - columnThreshold, - clampedWeights, - verticalEdgeFade, - matterByY, - resolvedMinWeight, - resolvedThresholdPenalty, - recoveryThresholdBoost, - true - ); - } if (carved == 0 && hasFallbackCandidates(columnMaxY, clampedWeights, minY, resolvedMinWeight)) { carved += carvePassFallback( chunk, @@ -388,28 +318,6 @@ public class IrisCaveCarver3D { 0D, false ); - if (carved < minCarveCells && recoveryThresholdBoost > 0D) { - carved += carvePassFallback( - chunk, - x0, - z0, - minY, - maxY, - sampleStep, - surfaceBreakThresholdBoost, - columnMaxY, - surfaceBreakFloorY, - surfaceBreakColumn, - columnThreshold, - clampedWeights, - verticalEdgeFade, - matterByY, - resolvedMinWeight, - resolvedThresholdPenalty, - recoveryThresholdBoost, - true - ); - } } } @@ -614,14 +522,7 @@ public class IrisCaveCarver3D { continue; } - int effectiveAdaptiveSampleStep = resolveAdaptivePlaneSampleStep( - y, - planeColumnIndices, - planeCount, - adaptiveSampleStep, - surfaceBreakColumn, - surfaceBreakFloorY - ); + int effectiveAdaptiveSampleStep = resolveAdaptivePlaneSampleStep(y, adaptiveSampleStep); double effectiveAdaptiveThresholdMargin = resolveAdaptivePlaneThresholdMargin( adaptiveThresholdMargin, adaptiveSampleStep, @@ -678,31 +579,14 @@ public class IrisCaveCarver3D { return carved; } - private int resolveAdaptivePlaneSampleStep( - int y, - int[] planeColumnIndices, - int planeCount, - int adaptiveSampleStep, - boolean[] surfaceBreakColumn, - int[] surfaceBreakFloorY - ) { - if (adaptiveSampleStep >= ADAPTIVE_DEEP_SAMPLE_STEP || planeCount < ADAPTIVE_DEEP_MIN_PLANE_COLUMNS) { + private int resolveAdaptivePlaneSampleStep(int y, int adaptiveSampleStep) { + if (adaptiveSampleStep >= ADAPTIVE_DEEP_SAMPLE_STEP) { return adaptiveSampleStep; } - int nearSurfaceColumns = 0; - int allowedNearSurfaceColumns = Math.max(8, planeCount / ADAPTIVE_DEEP_NEAR_SURFACE_DIVISOR); - for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) { - int columnIndex = planeColumnIndices[planeIndex]; - if (surfaceBreakColumn[columnIndex] || y > (surfaceBreakFloorY[columnIndex] - ADAPTIVE_DEEP_SURFACE_MARGIN)) { - nearSurfaceColumns++; - if (nearSurfaceColumns > allowedNearSurfaceColumns) { - return adaptiveSampleStep; - } - } - } - - return ADAPTIVE_DEEP_SAMPLE_STEP; + int profileMaxY = (int) Math.ceil(profile.getVerticalRange().getMax()); + int fineBandFloorY = profileMaxY - profile.getSurfaceBreakDepth() - ADAPTIVE_DEEP_SURFACE_MARGIN; + return y >= fineBandFloorY ? adaptiveSampleStep : ADAPTIVE_DEEP_SAMPLE_STEP; } private double resolveAdaptivePlaneThresholdMargin( diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java index c1c700206..963abde14 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java @@ -140,37 +140,28 @@ public class MantleCarvingComponent extends IrisMantleComponent { continue; } - IrisCaveProfile dominantProfile = null; - double dominantKernelWeight = Double.NEGATIVE_INFINITY; + int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ); for (int profileIndex = 0; profileIndex < profileCount; profileIndex++) { IrisCaveProfile profile = kernelProfiles[profileIndex]; double kernelWeight = kernelProfileWeights[profileIndex]; - if (kernelWeight > dominantKernelWeight) { - dominantProfile = profile; - dominantKernelWeight = kernelWeight; - } else if (kernelWeight == dominantKernelWeight - && profileSortKey(profile) < profileSortKey(dominantProfile)) { - dominantProfile = profile; - } kernelProfiles[profileIndex] = null; kernelProfileWeights[profileIndex] = 0D; - } - if (dominantProfile == null) { - continue; - } + double columnWeight = clampWeight(kernelWeight / totalKernelWeight); + if (columnWeight < MIN_WEIGHT) { + continue; + } - int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ); - double dominantWeight = clampWeight(dominantKernelWeight / totalKernelWeight); - double[] weights = columnProfileWeights.get(dominantProfile); - if (weights == null) { - weights = new double[CHUNK_AREA]; - columnProfileWeights.put(dominantProfile, weights); - } else if (!activeProfiles.containsKey(dominantProfile)) { - Arrays.fill(weights, 0D); + double[] weights = columnProfileWeights.get(profile); + if (weights == null) { + weights = new double[CHUNK_AREA]; + columnProfileWeights.put(profile, weights); + } else if (!activeProfiles.containsKey(profile)) { + Arrays.fill(weights, 0D); + } + activeProfiles.put(profile, Boolean.TRUE); + weights[columnIndex] = columnWeight; } - activeProfiles.put(dominantProfile, Boolean.TRUE); - weights[columnIndex] = dominantWeight; } } @@ -440,7 +431,7 @@ public class MantleCarvingComponent extends IrisMantleComponent { continue; } if (cachedChunkHeights != null) { - surfaceHeights[columnIndex] = (int) Math.round(cachedChunkHeights[columnIndex]); + surfaceHeights[columnIndex] = (int) Math.round(cachedChunkHeights[(localZ << 4) + localX]); continue; } surfaceHeights[columnIndex] = getEngineMantle().getEngine().getHeight(worldX, worldZ); 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 4d7db4103..dc3d7623a 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 @@ -37,10 +37,8 @@ import art.arcane.iris.util.project.stream.ProceduralStream; import art.arcane.volmlib.util.documentation.BlockCoordinates; import art.arcane.volmlib.util.documentation.ChunkCoordinates; import art.arcane.volmlib.util.format.Form; -import art.arcane.volmlib.util.mantle.runtime.MantleChunk; import art.arcane.volmlib.util.mantle.flag.ReservedFlag; import art.arcane.volmlib.util.math.RNG; -import art.arcane.volmlib.util.matter.Matter; import art.arcane.volmlib.util.matter.MatterStructurePOI; import art.arcane.iris.util.project.noise.CNG; import art.arcane.iris.util.project.noise.NoiseType; @@ -373,19 +371,46 @@ public class MantleObjectComponent extends IrisMantleComponent { int zz = rng.i(z, z + 15); int surfaceObjectExclusionDepth = resolveSurfaceObjectExclusionDepth(surfaceObjectExclusionBaseDepth, v); int surfaceObjectExclusionRadius = resolveSurfaceObjectExclusionRadius(v); - if (surfaceObjectExclusionDepth > 0 && hasSurfaceCarveExposure(writer, surfaceHeightLookup, xx, zz, surfaceObjectExclusionDepth, surfaceObjectExclusionRadius)) { - rejected++; - continue; - } + boolean overCave = surfaceObjectExclusionDepth > 0 && hasSurfaceCarveExposure(writer, surfaceHeightLookup, xx, zz, surfaceObjectExclusionDepth, surfaceObjectExclusionRadius); int id = rng.i(0, Integer.MAX_VALUE); IrisObjectPlacement effectivePlacement = resolveEffectivePlacement(objectPlacement, v); try { - int result = v.place(xx, -1, zz, writer, effectivePlacement, rng, (b, data) -> { - writer.setData(b.getX(), b.getY(), b.getZ(), v.getLoadKey() + "@" + id); - if (effectivePlacement.isDolphinTarget() && effectivePlacement.isUnderwater() && B.isStorageChest(data)) { - writer.setData(b.getX(), b.getY(), b.getZ(), MatterStructurePOI.BURIED_TREASURE); + int result = -1; + String fallbackPath = "surface"; + + if (overCave) { + int caveFloorY = findNearestCaveFloor(writer, xx, zz); + if (caveFloorY > 0) { + 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); + if (effectivePlacement.isDolphinTarget() && effectivePlacement.isUnderwater() && B.isStorageChest(data)) { + writer.setData(b.getX(), b.getY(), b.getZ(), MatterStructurePOI.BURIED_TREASURE); + } + }, null, getData()); + fallbackPath = "cave-floor"; } - }, null, getData()); + + if (result < 0) { + 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); + if (effectivePlacement.isDolphinTarget() && effectivePlacement.isUnderwater() && B.isStorageChest(data)) { + writer.setData(b.getX(), b.getY(), b.getZ(), MatterStructurePOI.BURIED_TREASURE); + } + }, null, getData()); + fallbackPath = "stilt"; + } + } else { + result = v.place(xx, -1, zz, writer, effectivePlacement, rng, (b, data) -> { + writer.setData(b.getX(), b.getY(), b.getZ(), v.getLoadKey() + "@" + id); + if (effectivePlacement.isDolphinTarget() && effectivePlacement.isUnderwater() && B.isStorageChest(data)) { + writer.setData(b.getX(), b.getY(), b.getZ(), MatterStructurePOI.BURIED_TREASURE); + } + }, null, getData()); + } if (result >= 0) { placed++; @@ -400,6 +425,8 @@ public class MantleObjectComponent extends IrisMantleComponent { + " resultY=" + result + " px=" + xx + " pz=" + zz + + " overCave=" + overCave + + " fallback=" + fallbackPath + " densityIndex=" + i + " density=" + density); } @@ -729,6 +756,14 @@ public class MantleObjectComponent extends IrisMantleComponent { return null; } + private int findNearestCaveFloor(MantleWriter writer, int x, int z) { + KList anchors = scanCaveAnchorColumn(writer, IrisCaveAnchorMode.FLOOR, 1, 0, x, z); + if (anchors.isEmpty()) { + return -1; + } + return anchors.get(anchors.size() - 1); + } + private int findCaveAnchorY(MantleWriter writer, RNG rng, int x, int z, IrisCaveAnchorMode anchorMode, int anchorScanStep, int objectMinDepthBelowSurface, KMap> anchorCache) { long key = Cache.key(x, z); KList anchors = anchorCache.computeIfAbsent(key, (k) -> scanCaveAnchorColumn(writer, anchorMode, anchorScanStep, objectMinDepthBelowSurface, x, z)); @@ -744,55 +779,58 @@ public class MantleObjectComponent extends IrisMantleComponent { } private KList scanCaveAnchorColumn(MantleWriter writer, IrisCaveAnchorMode anchorMode, int anchorScanStep, int objectMinDepthBelowSurface, int x, int z) { - KList anchors = new KList<>(); int height = getEngineMantle().getEngine().getHeight(); int step = Math.max(1, anchorScanStep); int surfaceY = getEngineMantle().getEngine().getHeight(x, z); - int maxAnchorY = Math.min(height - 1, surfaceY - Math.max(0, objectMinDepthBelowSurface)); - if (maxAnchorY <= 1) { - logCaveAnchorDiag(writer, x, z, surfaceY, maxAnchorY, height, objectMinDepthBelowSurface, 0, 0); + int baseMaxAnchorY = Math.min(height - 1, surfaceY - Math.max(0, objectMinDepthBelowSurface)); + if (baseMaxAnchorY <= 1) { + return new KList<>(); + } + + KList anchors = scanCaveAnchorRange(writer, anchorMode, step, x, z, height, baseMaxAnchorY); + if (!anchors.isEmpty()) { return anchors; } - int carvedCount = 0; - for (int y = 1; y < maxAnchorY; y += step) { - if (!writer.isCarved(x, y, z)) { - continue; + int widenedMaxAnchorY = Math.min(height - 1, surfaceY - 3); + widenedMaxAnchorY = Math.min(widenedMaxAnchorY, baseMaxAnchorY + Math.max(0, objectMinDepthBelowSurface) / 2); + if (widenedMaxAnchorY > baseMaxAnchorY) { + anchors = scanCaveAnchorRange(writer, anchorMode, step, x, z, height, widenedMaxAnchorY); + if (!anchors.isEmpty()) { + return anchors; } - - carvedCount++; - boolean solidBelow = y <= 0 || !writer.isCarved(x, y - 1, z); - boolean solidAbove = y >= (height - 1) || !writer.isCarved(x, y + 1, z); - if (matchesCaveAnchor(anchorMode, solidBelow, solidAbove)) { - anchors.add(y); - } - } - - if (anchors.isEmpty()) { - logCaveAnchorDiag(writer, x, z, surfaceY, maxAnchorY, height, objectMinDepthBelowSurface, carvedCount, 0); } return anchors; } - private void logCaveAnchorDiag(MantleWriter writer, int x, int z, int surfaceY, int maxAnchorY, int height, int minDepth, int carvedCount, int anchorCount) { - long now = System.currentTimeMillis(); - CaveRejectLogState state = CAVE_REJECT_LOG_STATE.computeIfAbsent("anchor-diag-" + (x >> 4) + "," + (z >> 4), k -> new CaveRejectLogState()); - if (now - state.lastLogMs.get() < CAVE_REJECT_LOG_THROTTLE_MS) { - return; + private KList scanCaveAnchorRange(MantleWriter writer, IrisCaveAnchorMode anchorMode, int step, int x, int z, int height, int maxAnchorY) { + KList anchors = new KList<>(); + for (int y = 1; y < maxAnchorY; y += step) { + if (!writer.isCarved(x, y, z)) { + continue; + } + + boolean solidBelow = hasSolidNeighbor(writer, x, y, z, height, -1); + boolean solidAbove = hasSolidNeighbor(writer, x, y, z, height, 1); + if (matchesCaveAnchor(anchorMode, solidBelow, solidAbove)) { + anchors.add(y); + } } - state.lastLogMs.set(now); - MantleChunk chunk = writer.acquireChunk(x >> 4, z >> 4); - Iris.info("Cave anchor diag: block=" + x + "," + z - + " chunk=" + (x >> 4) + "," + (z >> 4) - + " surfaceY=" + surfaceY - + " maxAnchorY=" + maxAnchorY - + " worldHeight=" + height - + " minDepth=" + minDepth - + " carvedInColumn=" + carvedCount - + " anchorsFound=" + anchorCount - + " chunkRef=" + (chunk == null ? "null" : System.identityHashCode(chunk)) - + " writerRef=" + System.identityHashCode(writer)); + return anchors; + } + + private boolean hasSolidNeighbor(MantleWriter writer, int x, int y, int z, int height, int direction) { + for (int d = 1; d <= 3; d++) { + int ny = y + (direction * d); + if (ny < 0 || ny >= height) { + return true; + } + if (!writer.isCarved(x, ny, z)) { + return true; + } + } + return false; } private boolean matchesCaveAnchor(IrisCaveAnchorMode anchorMode, boolean solidBelow, boolean solidAbove) { diff --git a/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveModifier.java b/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveModifier.java index fa0002fe9..b23ec8368 100644 --- a/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveModifier.java +++ b/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveModifier.java @@ -52,6 +52,9 @@ import java.util.Map; public class IrisCarveModifier extends EngineAssignedModifier { private static final ThreadLocal SCRATCH = ThreadLocal.withInitial(CarveScratch::new); + private static final int CAVE_BIOME_BLEND_RADIUS = 3; + private static final int CAVE_BIOME_BLEND_CENTER_WEIGHT = 4; + private static final int CAVE_BIOME_BLEND_TOTAL_WEIGHT = 8; private final RNG rng; private final BlockData AIR = Material.CAVE_AIR.createBlockData(); private final BlockData LAVA = Material.LAVA.createBlockData(); @@ -75,6 +78,8 @@ public class IrisCarveModifier extends EngineAssignedModifier { scratch.reset(); PackedWallBuffer walls = scratch.walls; ColumnMask[] columnMasks = scratch.columnMasks; + ColumnMask[] boundaryMasks = scratch.boundaryMasks; + MatterCavern[] boundaryCaverns = scratch.boundaryCaverns; int[] surfaceHeights = scratch.surfaceHeights; Map customBiomeCache = scratch.customBiomeCache; for (int columnIndex = 0; columnIndex < 256; columnIndex++) { @@ -137,6 +142,7 @@ public class IrisCarveModifier extends EngineAssignedModifier { output.setRaw(rx, yy, rz, AIR); } }); + addCrossChunkBoundaryWalls(mantle, mc, walls, boundaryMasks, boundaryCaverns, x, z, surfaceHeights); getEngine().getMetrics().getCarveResolve().put(resolveStopwatch.getMilliseconds()); PrecisionStopwatch applyStopwatch = PrecisionStopwatch.start(); @@ -154,7 +160,7 @@ public class IrisCarveModifier extends EngineAssignedModifier { BlockData data = biome.getWall().get(rng, worldX, yy, worldZ, getData()); int columnIndex = PowerOfTwoCoordinates.packLocal16(rx, rz); - if (data != null && B.isSolid(output.getRaw(rx, yy, rz)) && yy <= surfaceHeights[columnIndex]) { + if (data != null && B.isSolid(output.getRaw(rx, yy, rz)) && yy < surfaceHeights[columnIndex]) { output.setRaw(rx, yy, rz, data); } } @@ -163,6 +169,17 @@ public class IrisCarveModifier extends EngineAssignedModifier { for (int columnIndex = 0; columnIndex < 256; columnIndex++) { processColumnFromMask(output, mc, mantle, columnMasks[columnIndex], columnIndex, x, z, resolverState, caveBiomeCache); } + + for (int columnIndex = 0; columnIndex < 256; columnIndex++) { + if (boundaryMasks[columnIndex].isEmpty() || !columnMasks[columnIndex].isEmpty()) { + continue; + } + MatterCavern cavern = boundaryCaverns[columnIndex]; + if (cavern == null) { + continue; + } + processBoundaryColumnFromMask(output, boundaryMasks[columnIndex], cavern, columnIndex, x, z, resolverState, caveBiomeCache, customBiomeCache); + } } finally { getEngine().getMetrics().getCarveApply().put(applyStopwatch.getMilliseconds()); } @@ -172,6 +189,86 @@ public class IrisCarveModifier extends EngineAssignedModifier { } } + private void addCrossChunkBoundaryWalls( + Mantle mantle, + MantleChunk mc, + PackedWallBuffer walls, + ColumnMask[] boundaryMasks, + MatterCavern[] boundaryCaverns, + int chunkX, + int chunkZ, + int[] surfaceHeights + ) { + int baseX = PowerOfTwoCoordinates.chunkToBlock(chunkX); + int baseZ = PowerOfTwoCoordinates.chunkToBlock(chunkZ); + int maxSurfaceY = 0; + for (int index = 0; index < surfaceHeights.length; index++) { + if (surfaceHeights[index] > maxSurfaceY) { + maxSurfaceY = surfaceHeights[index]; + } + } + int maxY = Math.min(getEngine().getWorld().maxHeight() - getEngine().getWorld().minHeight() - 1, maxSurfaceY + 1); + if (maxY < 1) { + return; + } + + boolean westLoaded = mantle.hasTectonicPlate(((chunkX - 1) >> 5), (chunkZ >> 5)); + boolean eastLoaded = mantle.hasTectonicPlate(((chunkX + 1) >> 5), (chunkZ >> 5)); + boolean northLoaded = mantle.hasTectonicPlate((chunkX >> 5), ((chunkZ - 1) >> 5)); + boolean southLoaded = mantle.hasTectonicPlate((chunkX >> 5), ((chunkZ + 1) >> 5)); + if (!westLoaded && !eastLoaded && !northLoaded && !southLoaded) { + return; + } + + for (int yy = 1; yy <= maxY; yy++) { + for (int offset = 0; offset < 16; offset++) { + if (westLoaded) { + tryAddBoundaryWall(mantle, mc, walls, boundaryMasks, boundaryCaverns, 0, yy, offset, baseX, baseZ, -1, 0); + } + if (eastLoaded) { + tryAddBoundaryWall(mantle, mc, walls, boundaryMasks, boundaryCaverns, 15, yy, offset, baseX, baseZ, 1, 0); + } + if (northLoaded) { + tryAddBoundaryWall(mantle, mc, walls, boundaryMasks, boundaryCaverns, offset, yy, 0, baseX, baseZ, 0, -1); + } + if (southLoaded) { + tryAddBoundaryWall(mantle, mc, walls, boundaryMasks, boundaryCaverns, offset, yy, 15, baseX, baseZ, 0, 1); + } + } + } + } + + private void tryAddBoundaryWall( + Mantle mantle, + MantleChunk mc, + PackedWallBuffer walls, + ColumnMask[] boundaryMasks, + MatterCavern[] boundaryCaverns, + int localX, + int yy, + int localZ, + int baseX, + int baseZ, + int dx, + int dz + ) { + int worldX = baseX + localX; + int worldZ = baseZ + localZ; + if (mc.get(worldX, yy, worldZ, MatterCavern.class) != null) { + return; + } + MatterCavern neighbor = mantle.get(worldX + dx, yy, worldZ + dz, MatterCavern.class); + if (neighbor == null) { + return; + } + walls.put(localX, yy, localZ, neighbor); + int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ); + boundaryMasks[columnIndex].add(yy); + if (boundaryCaverns[columnIndex] == null) { + boundaryCaverns[columnIndex] = neighbor; + } + } + private void processColumnFromMask( Hunk output, MantleChunk mc, @@ -226,6 +323,117 @@ public class IrisCarveModifier extends EngineAssignedModifier { } } + private void processBoundaryColumnFromMask( + Hunk output, + ColumnMask boundaryMask, + MatterCavern cavern, + int columnIndex, + int chunkX, + int chunkZ, + IrisDimensionCarvingResolver.State resolverState, + Long2ObjectOpenHashMap caveBiomeCache, + Map customBiomeCache + ) { + int firstHeight = boundaryMask.nextSetBit(0); + if (firstHeight < 0) { + return; + } + + int rx = PowerOfTwoCoordinates.unpackLocal16X(columnIndex); + int rz = columnIndex & 15; + int worldX = rx + PowerOfTwoCoordinates.chunkToBlock(chunkX); + int worldZ = rz + PowerOfTwoCoordinates.chunkToBlock(chunkZ); + int zoneFloor = firstHeight; + int zoneCeiling = firstHeight; + int y = boundaryMask.nextSetBit(firstHeight + 1); + + while (y >= 0) { + if (y == zoneCeiling + 1) { + zoneCeiling = y; + } else { + paintBoundaryZone(output, cavern, rx, rz, worldX, worldZ, zoneFloor, zoneCeiling, resolverState, caveBiomeCache, customBiomeCache); + zoneFloor = y; + zoneCeiling = y; + } + y = boundaryMask.nextSetBit(y + 1); + } + + paintBoundaryZone(output, cavern, rx, rz, worldX, worldZ, zoneFloor, zoneCeiling, resolverState, caveBiomeCache, customBiomeCache); + } + + private void paintBoundaryZone( + Hunk output, + MatterCavern cavern, + int rx, + int rz, + int worldX, + int worldZ, + int zoneFloor, + int zoneCeiling, + IrisDimensionCarvingResolver.State resolverState, + Long2ObjectOpenHashMap caveBiomeCache, + Map customBiomeCache + ) { + int center = (zoneFloor + zoneCeiling) / 2; + String customBiome = cavern.getCustomBiome(); + IrisBiome biome = customBiome.isEmpty() + ? resolveCaveBiome(caveBiomeCache, worldX, center, worldZ, resolverState) + : resolveCustomBiome(customBiomeCache, customBiome); + + if (biome == null) { + return; + } + + biome.setInferredType(InferredType.CAVE); + + KList floorLayers = biome.generateLayers(getDimension(), worldX, worldZ, rng, 3, zoneFloor, getData(), getComplex()); + for (int i = 0; i < zoneFloor - 1; i++) { + if (!floorLayers.hasIndex(i)) { + break; + } + + int fy = zoneFloor - i - 1; + if (fy < 0) { + break; + } + + BlockData down = output.getRaw(rx, fy, rz); + if (!B.isSolid(down)) { + break; + } + + BlockData layer = floorLayers.get(i); + if (B.isOre(down)) { + output.setRaw(rx, fy, rz, B.toDeepSlateOre(down, layer)); + continue; + } + + output.setRaw(rx, fy, rz, layer); + } + + int worldMaxY = getEngine().getWorld().maxHeight() - getEngine().getWorld().minHeight(); + KList ceilingLayers = biome.generateCeilingLayers(getDimension(), worldX, worldZ, rng, 3, zoneCeiling, getData(), getComplex()); + for (int i = 0; i < ceilingLayers.size(); i++) { + int cy = zoneCeiling + i + 1; + if (cy >= worldMaxY) { + break; + } + + BlockData up = output.getRaw(rx, cy, rz); + if (!B.isSolid(up)) { + continue; + } + + BlockData layer = ceilingLayers.get(i); + if (B.isOre(up)) { + output.setRaw(rx, cy, rz, B.toDeepSlateOre(up, layer)); + continue; + } + + output.setRaw(rx, cy, rz, layer); + } + } + private void processZone(Hunk output, MantleChunk mc, Mantle mantle, CaveZone zone, int rx, int rz, int xx, int zz, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap caveBiomeCache) { int center = (zone.floor + zone.ceiling) / 2; String customBiome = ""; @@ -323,6 +531,38 @@ public class IrisCarveModifier extends EngineAssignedModifier { } private IrisBiome resolveCaveBiome(Long2ObjectOpenHashMap caveBiomeCache, int x, int y, int z, IrisDimensionCarvingResolver.State resolverState) { + IrisBiome center = sampleCaveBiome(caveBiomeCache, x, y, z, resolverState); + if (center == null) { + return null; + } + + IrisBiome xPos = sampleCaveBiome(caveBiomeCache, x + CAVE_BIOME_BLEND_RADIUS, y, z, resolverState); + IrisBiome xNeg = sampleCaveBiome(caveBiomeCache, x - CAVE_BIOME_BLEND_RADIUS, y, z, resolverState); + IrisBiome zPos = sampleCaveBiome(caveBiomeCache, x, y, z + CAVE_BIOME_BLEND_RADIUS, resolverState); + IrisBiome zNeg = sampleCaveBiome(caveBiomeCache, x, y, z - CAVE_BIOME_BLEND_RADIUS, resolverState); + + if (xPos == center && xNeg == center && zPos == center && zNeg == center) { + return center; + } + + int roll = Math.floorMod(rng.nextParallelRNG(BlockPosition.toLong(x, y, z)).nextInt(), CAVE_BIOME_BLEND_TOTAL_WEIGHT); + if (roll < CAVE_BIOME_BLEND_CENTER_WEIGHT) { + return center; + } + roll -= CAVE_BIOME_BLEND_CENTER_WEIGHT; + if (roll == 0) { + return xPos != null ? xPos : center; + } + if (roll == 1) { + return xNeg != null ? xNeg : center; + } + if (roll == 2) { + return zPos != null ? zPos : center; + } + return zNeg != null ? zNeg : center; + } + + private IrisBiome sampleCaveBiome(Long2ObjectOpenHashMap caveBiomeCache, int x, int y, int z, IrisDimensionCarvingResolver.State resolverState) { long key = BlockPosition.toLong(x, y, z); IrisBiome cachedBiome = caveBiomeCache.get(key); if (cachedBiome != null) { @@ -478,6 +718,8 @@ public class IrisCarveModifier extends EngineAssignedModifier { private static final class CarveScratch { private final ColumnMask[] columnMasks = new ColumnMask[256]; + private final ColumnMask[] boundaryMasks = new ColumnMask[256]; + private final MatterCavern[] boundaryCaverns = new MatterCavern[256]; private final int[] surfaceHeights = new int[256]; private final PackedWallBuffer walls = new PackedWallBuffer(512); private final Map customBiomeCache = new HashMap<>(); @@ -485,12 +727,15 @@ public class IrisCarveModifier extends EngineAssignedModifier { private CarveScratch() { for (int index = 0; index < columnMasks.length; index++) { columnMasks[index] = new ColumnMask(); + boundaryMasks[index] = new ColumnMask(); } } private void reset() { for (int index = 0; index < columnMasks.length; index++) { columnMasks[index].clear(); + boundaryMasks[index].clear(); + boundaryCaverns[index] = null; } walls.clear(); customBiomeCache.clear(); 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 4bf55e5b9..be2886877 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,37 +29,37 @@ 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 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.data.DataProvider; -import art.arcane.volmlib.util.io.IO; -import art.arcane.volmlib.util.json.JSONArray; -import art.arcane.volmlib.util.json.JSONObject; -import art.arcane.volmlib.util.mantle.flag.MantleFlag; -import art.arcane.volmlib.util.math.Position2; -import art.arcane.volmlib.util.math.RNG; -import art.arcane.iris.util.project.noise.CNG; -import art.arcane.iris.util.common.plugin.VolmitSender; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; -import org.bukkit.Material; -import org.bukkit.NamespacedKey; -import org.bukkit.World.Environment; -import org.bukkit.block.Biome; -import org.bukkit.block.data.BlockData; - -import java.io.*; -import java.nio.file.AtomicMoveNotSupportedException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardCopyOption; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; +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.data.DataProvider; +import art.arcane.volmlib.util.io.IO; +import art.arcane.volmlib.util.json.JSONArray; +import art.arcane.volmlib.util.json.JSONObject; +import art.arcane.volmlib.util.mantle.flag.MantleFlag; +import art.arcane.volmlib.util.math.Position2; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.iris.util.project.noise.CNG; +import art.arcane.iris.util.common.plugin.VolmitSender; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.World.Environment; +import org.bukkit.block.Biome; +import org.bukkit.block.data.BlockData; + +import java.io.*; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; @Accessors(chain = true) @AllArgsConstructor @@ -74,21 +74,21 @@ public class IrisDimension extends IrisRegistrant { private final transient AtomicCache rockLayerGenerator = new AtomicCache<>(); private final transient AtomicCache fluidLayerGenerator = new AtomicCache<>(); private final transient AtomicCache coordFracture = new AtomicCache<>(); - private final transient AtomicCache sinr = new AtomicCache<>(); - private final transient AtomicCache cosr = new AtomicCache<>(); - private final transient AtomicCache rad = new AtomicCache<>(); - private final transient AtomicCache featuresUsed = new AtomicCache<>(); - private final transient AtomicCache> carvingEntryIndex = new AtomicCache<>(); - private final transient AtomicCache> surfaceOreCache = new AtomicCache<>(); - private final transient AtomicCache> undergroundOreCache = new AtomicCache<>(); + private final transient AtomicCache sinr = new AtomicCache<>(); + private final transient AtomicCache cosr = new AtomicCache<>(); + private final transient AtomicCache rad = new AtomicCache<>(); + private final transient AtomicCache featuresUsed = new AtomicCache<>(); + private final transient AtomicCache> carvingEntryIndex = new AtomicCache<>(); + private final transient AtomicCache> surfaceOreCache = new AtomicCache<>(); + private final transient AtomicCache> undergroundOreCache = new AtomicCache<>(); @MinNumber(2) @Required @Desc("The human readable name of this dimension") private String name = "A Dimension"; - @MinNumber(1) - @MaxNumber(2032) - @Desc("Maximum height at which players can be teleported to through gameplay.") - private int logicalHeight = 256; + @MinNumber(1) + @MaxNumber(2032) + @Desc("Maximum height at which players can be teleported to through gameplay.") + private int logicalHeight = 256; @Desc("If set to true, Iris will remove chunks to allow visualizing cross sections of chunks easily") private boolean debugChunkCrossSections = false; @Desc("Vertically split up the biome palettes with 3 air blocks in between to visualize them") @@ -99,10 +99,10 @@ public class IrisDimension extends IrisRegistrant { @MaxNumber(16) @Desc("Customize the palette height explosion") private int explodeBiomePaletteSize = 3; - @MinNumber(2) - @MaxNumber(16) - @Desc("Every X/Z % debugCrossSectionsMod == 0 cuts the chunk") - private int debugCrossSectionsMod = 3; + @MinNumber(2) + @MaxNumber(16) + @Desc("Every X/Z % debugCrossSectionsMod == 0 cuts the chunk") + private int debugCrossSectionsMod = 3; @Desc("Tree growth override settings") private IrisTreeSettings treeSettings = new IrisTreeSettings(); @Desc("Spawn Entities in this dimension over time. Iris will continually replenish these mobs just like vanilla does.") @@ -143,29 +143,31 @@ public class IrisDimension extends IrisRegistrant { private boolean postProcessing = true; @Desc("Add slabs in post processing") private boolean postProcessingSlabs = true; - @Desc("Add painted walls in post processing") - private boolean postProcessingWalls = true; - @Desc("Enable or disable all carving for this dimension") - private boolean carvingEnabled = true; - @ArrayType(type = IrisDimensionCarvingEntry.class, min = 1) - @Desc("Dimension-level cave biome carving overrides with absolute world Y ranges") - private KList carving = new KList<>(); - @Desc("Profile-driven 3D cave configuration") - private IrisCaveProfile caveProfile = new IrisCaveProfile(); - @Desc("Configuration of fluid bodies such as rivers & lakes") - private IrisFluidBodies fluidBodies = new IrisFluidBodies(); - @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("Add painted walls in post processing") + private boolean postProcessingWalls = true; + @Desc("Enable or disable all carving for this dimension") + private boolean carvingEnabled = true; + @ArrayType(type = IrisDimensionCarvingEntry.class, min = 1) + @Desc("Dimension-level cave biome carving overrides with absolute world Y ranges") + private KList carving = new KList<>(); + @Desc("Profile-driven 3D cave configuration") + 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") private Environment environment = Environment.NORMAL; - @RegistryListResource(IrisRegion.class) - @Required - @ArrayType(min = 1, type = String.class) - @Desc("Define all of the regions to include in this dimension. Dimensions -> Regions -> Biomes -> Objects etc") - private KList regions = new KList<>(); + @RegistryListResource(IrisRegion.class) + @Required + @ArrayType(min = 1, type = String.class) + @Desc("Define all of the regions to include in this dimension. Dimensions -> Regions -> Biomes -> Objects etc") + private KList regions = new KList<>(); @Required @MinNumber(0) @MaxNumber(1024) @@ -244,121 +246,121 @@ public class IrisDimension extends IrisRegistrant { private boolean disableExplorerMaps = false; @Desc("Collection of ores to be generated") @ArrayType(type = IrisOreGenerator.class, min = 1) - private KList ores = new KList<>(); + private KList ores = new KList<>(); @MinNumber(0) @MaxNumber(318) @Desc("The Subterrain Fluid Layer Height") private int caveLavaHeight = 8; - @RegistryListFunction(ComponentFlagFunction.class) - @ArrayType(type = String.class) - @Desc("Collection of disabled components") - private KList disabledComponents = new KList<>(); + @RegistryListFunction(ComponentFlagFunction.class) + @ArrayType(type = String.class) + @Desc("Collection of disabled components") + private KList disabledComponents = new KList<>(); public int getMaxHeight() { return (int) getDimensionHeight().getMax(); } - public int getMinHeight() { - return (int) getDimensionHeight().getMin(); - } - - public Map getCarvingEntryIndex() { - return carvingEntryIndex.aquire(() -> { - Map index = new HashMap<>(); - KList entries = getCarving(); - if (entries == null || entries.isEmpty()) { - return index; - } - - for (IrisDimensionCarvingEntry entry : entries) { - if (entry == null) { - continue; - } - - String entryId = entry.getId(); - if (entryId == null || entryId.isBlank()) { - continue; - } - - index.put(entryId.trim(), entry); - } - - return index; - }); - } - - public void setCarving(KList carving) { - this.carving = carving == null ? new KList<>() : carving; - carvingEntryIndex.reset(); - } + public int getMinHeight() { + return (int) getDimensionHeight().getMin(); + } - public BlockData generateOres(int x, int y, int z, RNG rng, IrisData data, boolean surface) { - KList localOres = surface ? getSurfaceOres() : getUndergroundOres(); - return generateOres(localOres, x, y, z, rng, data); - } - - public BlockData generateSurfaceOres(int x, int y, int z, RNG rng, IrisData data) { - return generateOres(getSurfaceOres(), x, y, z, rng, data); - } - - public BlockData generateUndergroundOres(int x, int y, int z, RNG rng, IrisData data) { - return generateOres(getUndergroundOres(), x, y, z, rng, data); - } - - public boolean hasSurfaceOres() { - return !getSurfaceOres().isEmpty(); - } - - public boolean hasUndergroundOres() { - return !getUndergroundOres().isEmpty(); - } - - private BlockData generateOres(KList localOres, int x, int y, int z, RNG rng, IrisData data) { - if (localOres.isEmpty()) { - return null; - } - - int oreCount = localOres.size(); - for (int oreIndex = 0; oreIndex < oreCount; oreIndex++) { - IrisOreGenerator oreGenerator = localOres.get(oreIndex); - BlockData ore = oreGenerator.generate(x, y, z, rng, data); - if (ore != null) { - return ore; - } - } - return null; - } - - public void setOres(KList ores) { - this.ores = ores == null ? new KList<>() : ores; - surfaceOreCache.reset(); - undergroundOreCache.reset(); - } - - private KList getSurfaceOres() { - return getOres(true); - } - - private KList getUndergroundOres() { - return getOres(false); - } - - private KList getOres(boolean surface) { - AtomicCache> oreCache = surface ? surfaceOreCache : undergroundOreCache; - return oreCache.aquire(() -> { - KList filtered = new KList<>(); - KList localOres = ores; - int oreCount = localOres.size(); - for (int oreIndex = 0; oreIndex < oreCount; oreIndex++) { - IrisOreGenerator oreGenerator = localOres.get(oreIndex); - if (oreGenerator.isGenerateSurface() == surface) { - filtered.add(oreGenerator); - } - } - - return filtered; - }); - } + public Map getCarvingEntryIndex() { + return carvingEntryIndex.aquire(() -> { + Map index = new HashMap<>(); + KList entries = getCarving(); + if (entries == null || entries.isEmpty()) { + return index; + } + + for (IrisDimensionCarvingEntry entry : entries) { + if (entry == null) { + continue; + } + + String entryId = entry.getId(); + if (entryId == null || entryId.isBlank()) { + continue; + } + + index.put(entryId.trim(), entry); + } + + return index; + }); + } + + public void setCarving(KList carving) { + this.carving = carving == null ? new KList<>() : carving; + carvingEntryIndex.reset(); + } + + public BlockData generateOres(int x, int y, int z, RNG rng, IrisData data, boolean surface) { + KList localOres = surface ? getSurfaceOres() : getUndergroundOres(); + return generateOres(localOres, x, y, z, rng, data); + } + + public BlockData generateSurfaceOres(int x, int y, int z, RNG rng, IrisData data) { + return generateOres(getSurfaceOres(), x, y, z, rng, data); + } + + public BlockData generateUndergroundOres(int x, int y, int z, RNG rng, IrisData data) { + return generateOres(getUndergroundOres(), x, y, z, rng, data); + } + + public boolean hasSurfaceOres() { + return !getSurfaceOres().isEmpty(); + } + + public boolean hasUndergroundOres() { + return !getUndergroundOres().isEmpty(); + } + + private BlockData generateOres(KList localOres, int x, int y, int z, RNG rng, IrisData data) { + if (localOres.isEmpty()) { + return null; + } + + int oreCount = localOres.size(); + for (int oreIndex = 0; oreIndex < oreCount; oreIndex++) { + IrisOreGenerator oreGenerator = localOres.get(oreIndex); + BlockData ore = oreGenerator.generate(x, y, z, rng, data); + if (ore != null) { + return ore; + } + } + return null; + } + + public void setOres(KList ores) { + this.ores = ores == null ? new KList<>() : ores; + surfaceOreCache.reset(); + undergroundOreCache.reset(); + } + + private KList getSurfaceOres() { + return getOres(true); + } + + private KList getUndergroundOres() { + return getOres(false); + } + + private KList getOres(boolean surface) { + AtomicCache> oreCache = surface ? surfaceOreCache : undergroundOreCache; + return oreCache.aquire(() -> { + KList filtered = new KList<>(); + KList localOres = ores; + int oreCount = localOres.size(); + for (int oreIndex = 0; oreIndex < oreCount; oreIndex++) { + IrisOreGenerator oreGenerator = localOres.get(oreIndex); + if (oreGenerator.isGenerateSurface() == surface) { + filtered.add(oreGenerator); + } + } + + return filtered; + }); + } public int getFluidHeight() { return fluidHeight - (int) dimensionHeight.getMin(); @@ -452,217 +454,217 @@ public class IrisDimension extends IrisRegistrant { return landBiomeStyle; } - 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)) { - if (!irisBiome.isCustom()) { - 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) { - if (!biomes.add(customBiomeId)) { - Iris.verbose("Duplicate Data Pack Biome: " + getLoadKey() + "/" + customBiomeId); - continue; - } - } - - if (vanillaBiomeKey != null) { - customBiomeToVanillaBiome.put(customBiomeKey, vanillaBiomeKey); - } - - for (File datapacks : folders) { - File output = new File(datapacks, "iris/data/" + namespace + "/worldgen/biome/" + customBiomeId + ".json"); - - Iris.verbose(" Installing Data Pack Biome: " + output.getPath()); - output.getParentFile().mkdirs(); - try { - IO.writeAll(output, json); - } catch (IOException e) { - Iris.reportError(e); - e.printStackTrace(); - } - } - } - } - - 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 void installBiomes(IDataFixer fixer, DataProvider data, KList folders, KSet biomes) { + KMap customBiomeToVanillaBiome = new KMap<>(); + String namespace = getLoadKey().toLowerCase(Locale.ROOT); - public Dimension getBaseDimension() { - return switch (getEnvironment()) { - case NETHER -> Dimension.NETHER; - case THE_END -> Dimension.END; - default -> Dimension.OVERWORLD; - }; - } - - public String getDimensionTypeKey() { - return sanitizeDimensionTypeKeyValue(getLoadKey()); - } - - public static String sanitizeDimensionTypeKeyValue(String value) { - if (value == null || value.isBlank()) { - return "dimension"; - } - - String sanitized = value.trim().toLowerCase(Locale.ROOT).replace("\\", "/"); - sanitized = sanitized.replaceAll("[^a-z0-9_\\-./]", "_"); - sanitized = sanitized.replaceAll("/+", "/"); - sanitized = sanitized.replaceAll("^/+", ""); - sanitized = sanitized.replaceAll("/+$", ""); - if (sanitized.contains("..")) { - sanitized = sanitized.replace("..", "_"); - } - - sanitized = sanitized.replace("/", "_"); - return sanitized.isBlank() ? "dimension" : sanitized; - } - - public IrisDimensionType getDimensionType() { - return new IrisDimensionType(getBaseDimension(), getDimensionOptions(), getLogicalHeight(), getMaxHeight() - getMinHeight(), getMinHeight()); - } - - public void installDimensionType(IDataFixer fixer, KList folders) { - IrisDimensionType type = getDimensionType(); - String json = type.toJson(fixer); - String dimensionTypeKey = getDimensionTypeKey(); - - Iris.verbose(" Installing Data Pack Dimension Type: \"iris:" + dimensionTypeKey + '"'); - for (File datapacks : folders) { - File output = new File(datapacks, "iris/data/iris/dimension_type/" + dimensionTypeKey + ".json"); - output.getParentFile().mkdirs(); - try { - IO.writeAll(output, json); - } catch (IOException e) { + for (IrisBiome irisBiome : getAllBiomes(data)) { + if (!irisBiome.isCustom()) { + 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) { + if (!biomes.add(customBiomeId)) { + Iris.verbose("Duplicate Data Pack Biome: " + getLoadKey() + "/" + customBiomeId); + continue; + } + } + + if (vanillaBiomeKey != null) { + customBiomeToVanillaBiome.put(customBiomeKey, vanillaBiomeKey); + } + + for (File datapacks : folders) { + File output = new File(datapacks, "iris/data/" + namespace + "/worldgen/biome/" + customBiomeId + ".json"); + + Iris.verbose(" Installing Data Pack Biome: " + output.getPath()); + output.getParentFile().mkdirs(); + try { + IO.writeAll(output, json); + } catch (IOException e) { + Iris.reportError(e); + e.printStackTrace(); + } + } + } + } + + 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() { + return switch (getEnvironment()) { + case NETHER -> Dimension.NETHER; + case THE_END -> Dimension.END; + default -> Dimension.OVERWORLD; + }; + } + + public String getDimensionTypeKey() { + return sanitizeDimensionTypeKeyValue(getLoadKey()); + } + + public static String sanitizeDimensionTypeKeyValue(String value) { + if (value == null || value.isBlank()) { + return "dimension"; + } + + String sanitized = value.trim().toLowerCase(Locale.ROOT).replace("\\", "/"); + sanitized = sanitized.replaceAll("[^a-z0-9_\\-./]", "_"); + sanitized = sanitized.replaceAll("/+", "/"); + sanitized = sanitized.replaceAll("^/+", ""); + sanitized = sanitized.replaceAll("/+$", ""); + if (sanitized.contains("..")) { + sanitized = sanitized.replace("..", "_"); + } + + sanitized = sanitized.replace("/", "_"); + return sanitized.isBlank() ? "dimension" : sanitized; + } + + public IrisDimensionType getDimensionType() { + return new IrisDimensionType(getBaseDimension(), getDimensionOptions(), getLogicalHeight(), getMaxHeight() - getMinHeight(), getMinHeight()); + } + + public void installDimensionType(IDataFixer fixer, KList folders) { + IrisDimensionType type = getDimensionType(); + String json = type.toJson(fixer); + String dimensionTypeKey = getDimensionTypeKey(); + + Iris.verbose(" Installing Data Pack Dimension Type: \"iris:" + dimensionTypeKey + '"'); + for (File datapacks : folders) { + File output = new File(datapacks, "iris/data/iris/dimension_type/" + dimensionTypeKey + ".json"); + output.getParentFile().mkdirs(); + try { + IO.writeAll(output, json); + } catch (IOException e) { Iris.reportError(e); e.printStackTrace(); } 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 index e97df0eba..079caa5a3 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisExternalDatapack.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisExternalDatapack.java @@ -1,8 +1,6 @@ package art.arcane.iris.engine.object; -import art.arcane.iris.engine.object.annotations.ArrayType; import art.arcane.iris.engine.object.annotations.Desc; -import art.arcane.volmlib.util.collection.KList; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -12,7 +10,7 @@ import lombok.experimental.Accessors; @NoArgsConstructor @AllArgsConstructor @Accessors(chain = true) -@Desc("Defines a pack-scoped external datapack source for structure import and optional vanilla replacement") +@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 = ""; @@ -26,28 +24,6 @@ public class IrisExternalDatapack { @Desc("If true, Iris hard-fails startup when this external datapack cannot be synced/imported/installed") private boolean required = false; - @Desc("If true, minecraft namespace worldgen assets may replace vanilla targets listed in replaceTargets") - private boolean replaceVanilla = false; - - @Desc("If true, structures projected from this datapack id receive smartbore foundation extension during generation") - private boolean supportSmartBore = true; - - @Desc("Explicit replacement targets for minecraft namespace assets") - private IrisExternalDatapackReplaceTargets replaceTargets = new IrisExternalDatapackReplaceTargets(); - - @ArrayType(type = IrisExternalDatapackStructureAlias.class, min = 1) - @Desc("Optional structure alias mappings used to synthesize vanilla structure replacements from non-minecraft source keys") - private KList structureAliases = new KList<>(); - - @ArrayType(type = IrisExternalDatapackStructureSetAlias.class, min = 1) - @Desc("Optional structure-set alias mappings used to synthesize vanilla structure_set replacements from non-minecraft source keys") - private KList structureSetAliases = new KList<>(); - - @ArrayType(type = IrisExternalDatapackTemplateAlias.class, min = 1) - @Desc("Optional template location alias mappings applied while projecting template pools") - private KList templateAliases = new KList<>(); - - @ArrayType(type = IrisExternalDatapackStructurePatch.class, min = 1) - @Desc("Structure placement patches applied when this external datapack is projected") - private KList structurePatches = new KList<>(); + @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 index 6cbb56968..9dd7b76dd 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisExternalDatapackBinding.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisExternalDatapackBinding.java @@ -18,8 +18,8 @@ public class IrisExternalDatapackBinding { @Desc("Enable or disable this scoped binding") private boolean enabled = true; - @Desc("Override replaceVanilla behavior for this scoped binding (null keeps dimension default)") - private Boolean replaceVanillaOverride = null; + @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; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisExternalDatapackReplaceTargets.java b/core/src/main/java/art/arcane/iris/engine/object/IrisExternalDatapackReplaceTargets.java deleted file mode 100644 index 08af7f0c4..000000000 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisExternalDatapackReplaceTargets.java +++ /dev/null @@ -1,54 +0,0 @@ -package art.arcane.iris.engine.object; - -import art.arcane.iris.engine.object.annotations.ArrayType; -import art.arcane.iris.engine.object.annotations.Desc; -import art.arcane.volmlib.util.collection.KList; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@Data -@NoArgsConstructor -@AllArgsConstructor -@Accessors(chain = true) -@Desc("Explicit minecraft namespace targets that may be replaced by an external datapack") -public class IrisExternalDatapackReplaceTargets { - @ArrayType(type = String.class, min = 1) - @Desc("Structure ids that may be replaced when replaceVanilla is enabled") - private KList structures = new KList<>(); - - @ArrayType(type = String.class, min = 1) - @Desc("Structure set ids that may be replaced when replaceVanilla is enabled") - private KList structureSets = new KList<>(); - - @ArrayType(type = String.class, min = 1) - @Desc("Template pool ids that may be replaced when replaceVanilla is enabled") - private KList templatePools = new KList<>(); - - @ArrayType(type = String.class, min = 1) - @Desc("Processor list ids that may be replaced when replaceVanilla is enabled") - private KList processorLists = new KList<>(); - - @ArrayType(type = String.class, min = 1) - @Desc("Biome has_structure tag ids that may be replaced when replaceVanilla is enabled") - private KList biomeHasStructureTags = new KList<>(); - - @ArrayType(type = String.class, min = 1) - @Desc("Configured feature ids that may be replaced when replaceVanilla is enabled") - private KList configuredFeatures = new KList<>(); - - @ArrayType(type = String.class, min = 1) - @Desc("Placed feature ids that may be replaced when replaceVanilla is enabled") - private KList placedFeatures = new KList<>(); - - public boolean hasAnyTargets() { - return !structures.isEmpty() - || !structureSets.isEmpty() - || !templatePools.isEmpty() - || !processorLists.isEmpty() - || !biomeHasStructureTags.isEmpty() - || !configuredFeatures.isEmpty() - || !placedFeatures.isEmpty(); - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisExternalDatapackStructureAlias.java b/core/src/main/java/art/arcane/iris/engine/object/IrisExternalDatapackStructureAlias.java deleted file mode 100644 index fe7882fc4..000000000 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisExternalDatapackStructureAlias.java +++ /dev/null @@ -1,20 +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("Maps a vanilla structure replacement target to a source structure key from an external datapack") -public class IrisExternalDatapackStructureAlias { - @Desc("Vanilla replacement target structure id") - private String target = ""; - - @Desc("Source structure id to clone when the target id is not provided directly") - private String source = ""; -} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisExternalDatapackStructurePatch.java b/core/src/main/java/art/arcane/iris/engine/object/IrisExternalDatapackStructurePatch.java deleted file mode 100644 index c1dedbe66..000000000 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisExternalDatapackStructurePatch.java +++ /dev/null @@ -1,23 +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 a structure-level patch override for external datapack projection") -public class IrisExternalDatapackStructurePatch { - @Desc("Structure id to patch") - private String structure = ""; - - @Desc("Enable or disable this patch entry") - private boolean enabled = true; - - @Desc("Absolute start height override for this structure") - private int startHeightAbsolute = -27; -} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisExternalDatapackStructureSetAlias.java b/core/src/main/java/art/arcane/iris/engine/object/IrisExternalDatapackStructureSetAlias.java deleted file mode 100644 index 2b6a7cfd9..000000000 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisExternalDatapackStructureSetAlias.java +++ /dev/null @@ -1,20 +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("Maps a vanilla structure_set replacement target to a source structure_set key from an external datapack") -public class IrisExternalDatapackStructureSetAlias { - @Desc("Vanilla replacement target structure_set id") - private String target = ""; - - @Desc("Source structure_set id to clone when the target id is not provided directly") - private String source = ""; -} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisExternalDatapackTemplateAlias.java b/core/src/main/java/art/arcane/iris/engine/object/IrisExternalDatapackTemplateAlias.java deleted file mode 100644 index 959820809..000000000 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisExternalDatapackTemplateAlias.java +++ /dev/null @@ -1,23 +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("Maps missing template-pool element locations from an external datapack to replacement template locations") -public class IrisExternalDatapackTemplateAlias { - @Desc("Source template location to rewrite") - private String from = ""; - - @Desc("Target template location. Use minecraft:empty to convert the element to an empty pool element") - private String to = ""; - - @Desc("Enable or disable this alias entry") - private boolean enabled = true; -} 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 062d6f60c..a3e828752 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 @@ -57,6 +57,8 @@ import org.bukkit.block.data.BlockData; import org.bukkit.block.data.MultipleFacing; import org.bukkit.block.data.Waterlogged; import org.bukkit.block.data.type.Leaves; +import org.bukkit.block.data.type.Slab; +import org.bukkit.block.data.type.Stairs; import org.bukkit.util.BlockVector; import org.bukkit.util.Vector; @@ -131,13 +133,10 @@ public class IrisObject extends IrisRegistrant { new IrisPosition(new BlockVector(size.getX() - 1, size.getY() - 1, size.getZ() - 1).subtract(center).toBlockVector())); } - @SuppressWarnings({"resource", "RedundantSuppression"}) public static BlockVector sampleSize(File file) throws IOException { - FileInputStream in = new FileInputStream(file); - DataInputStream din = new DataInputStream(in); - BlockVector bv = new BlockVector(din.readInt(), din.readInt(), din.readInt()); - Iris.later(din::close); - return bv; + try (DataInputStream din = new DataInputStream(new FileInputStream(file))) { + return new BlockVector(din.readInt(), din.readInt(), din.readInt()); + } } private static List blocksBetweenTwoPoints(Vector loc1, Vector loc2) { @@ -159,6 +158,16 @@ public class IrisObject extends IrisRegistrant { return locations; } + private static boolean shouldStilt(BlockData data) { + if (!data.getMaterial().isOccluding()) { + return false; + } + if (data instanceof Stairs || data instanceof Slab) { + return false; + } + return data.getMaterial() != Material.DIRT_PATH; + } + public AxisAlignedBB getAABB() { return aabb.aquire(() -> getAABBFor(new BlockVector(w, h, d))); } @@ -494,9 +503,9 @@ public class IrisObject extends IrisRegistrant { return; } - FileOutputStream out = new FileOutputStream(file); - write(out); - out.close(); + try (FileOutputStream out = new FileOutputStream(file)) { + write(out); + } } public void write(File file, VolmitSender sender) throws IOException { @@ -504,9 +513,9 @@ public class IrisObject extends IrisRegistrant { return; } - FileOutputStream out = new FileOutputStream(file); - write(out, sender); - out.close(); + try (FileOutputStream out = new FileOutputStream(file)) { + write(out, sender); + } } public void shrinkwrap() { @@ -672,7 +681,8 @@ public class IrisObject extends IrisRegistrant { boolean warped = !config.getWarp().isFlat(); 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.CENTER_STILT || config.getMode() == ObjectPlaceMode.ERODE_STILT); + boolean eroding = config.getMode() == ObjectPlaceMode.ERODE_STILT; KMap heightmap = config.getSnow() > 0 ? new KMap<>() : null; int spinx = rng.imax() / 1000; int spiny = rng.imax() / 1000; @@ -954,7 +964,7 @@ public class IrisObject extends IrisRegistrant { i = config.getRotation().rotate(i.clone(), spinx, spiny, spinz).clone(); i = config.getTranslate().translate(i.clone(), config.getRotation(), spinx, spiny, spinz).clone(); - if (stilting && i.getBlockY() < lowest && B.isSolid(data)) { + if (stilting && i.getBlockY() < lowest && shouldStilt(data)) { lowest = i.getBlockY(); } @@ -1054,6 +1064,45 @@ public class IrisObject extends IrisRegistrant { if (stilting) { readLock.lock(); IrisStiltSettings settings = config.getStiltSettings(); + + double erodeCentroidX = 0; + double erodeCentroidZ = 0; + double erodeMaxDist = 1; + if (eroding) { + int centroidCount = 0; + for (BlockVector g : blocks.keys()) { + BlockVector rot = config.getRotation().rotate(g.clone(), spinx, spiny, spinz).clone(); + rot = config.getTranslate().translate(rot.clone(), config.getRotation(), spinx, spiny, spinz).clone(); + if (rot.getBlockY() == lowest) { + BlockData bd = blocks.get(g); + if (bd != null && shouldStilt(bd)) { + erodeCentroidX += rot.getX(); + erodeCentroidZ += rot.getZ(); + centroidCount++; + } + } + } + if (centroidCount > 0) { + erodeCentroidX /= centroidCount; + erodeCentroidZ /= centroidCount; + } + for (BlockVector g : blocks.keys()) { + BlockVector rot = config.getRotation().rotate(g.clone(), spinx, spiny, spinz).clone(); + rot = config.getTranslate().translate(rot.clone(), config.getRotation(), spinx, spiny, spinz).clone(); + if (rot.getBlockY() == lowest) { + BlockData bd = blocks.get(g); + if (bd != null && shouldStilt(bd)) { + double dx = rot.getX() - erodeCentroidX; + double dz = rot.getZ() - erodeCentroidZ; + double dist = Math.sqrt(dx * dx + dz * dz); + if (dist > erodeMaxDist) { + erodeMaxDist = dist; + } + } + } + } + } + for (BlockVector g : blocks.keys()) { BlockData sourceData; try { @@ -1069,13 +1118,18 @@ public class IrisObject extends IrisRegistrant { sourceData = AIR; } - if (!B.isSolid(sourceData)) { + if (!shouldStilt(sourceData)) { continue; } BlockData d = sourceData; if (settings != null && settings.getPalette() != null) { d = config.getStiltSettings().getPalette().get(rng, x, y, z, rdata); + } else { + Material mat = d.getMaterial(); + if (mat == Material.GRASS_BLOCK || mat == Material.MYCELIUM || mat == Material.PODZOL || mat == Material.DIRT_PATH) { + d = Material.DIRT.createBlockData(); + } } BlockVector i = g.clone(); @@ -1102,7 +1156,7 @@ public class IrisObject extends IrisRegistrant { } } - if (d == null || !B.isSolid(d)) + if (d == null || !d.getMaterial().isOccluding()) continue; xx = x + (int) Math.round(i.getX()); @@ -1127,7 +1181,31 @@ public class IrisObject extends IrisRegistrant { if (settings.getYMax() != 0) lowerBound -= Math.min(config.getStiltSettings().getYMax() - (lowest + y - highest), 0); } + + if (eroding) { + double dx = i.getX() - erodeCentroidX; + double dz = i.getZ() - erodeCentroidZ; + double normalizedDist = Math.sqrt(dx * dx + dz * dz) / erodeMaxDist; + normalizedDist = Math.min(normalizedDist, 1.0); + int totalDepth = (lowest + y) - lowerBound; + int erodeDepth = (int) (totalDepth * Math.pow(1.0 - normalizedDist, 1.5)); + lowerBound = (lowest + y) - erodeDepth; + } + for (int j = lowest + y; j > lowerBound; j--) { + if (eroding) { + int depth = (lowest + y) - j; + int totalDepth = (lowest + y) - lowerBound; + double depthRatio = totalDepth > 0 ? (double) depth / totalDepth : 0; + if (depthRatio > 0.4) { + long hash = ((long) (xx * 341873128712L) ^ ((long) j * 132897987541L) ^ ((long) zz * 735791245321L)); + double skipChance = (depthRatio - 0.4) / 0.6; + if ((Math.abs(hash) % 1000) / 1000.0 < skipChance * 0.7) { + continue; + } + } + } + if (B.isVineBlock(d)) { MultipleFacing f = (MultipleFacing) d; for (BlockFace face : f.getAllowedFaces()) { 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 a459ea650..fe3a661ed 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 @@ -62,6 +62,10 @@ public enum ObjectPlaceMode { CENTER_STILT, + @Desc("Erode stilting tapers columns downward like an ice cream cone. Blocks near the center extend deepest while edge blocks drop off first. Blocks are randomly skipped in the lower portion for a rough organic eroded texture.") + + ERODE_STILT, + @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 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 03458157c..6d12c61c8 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 @@ -380,6 +380,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun INMS.get().placeStructures(c); setChunkReplacementPhase(phaseRef, effectiveListener, "chunk-load-callback", x, z); engine.getWorldManager().onChunkLoad(c, true); + world.refreshChunk(c.getX(), c.getZ()); } finally { Iris.tickets.removeTicket(c); } @@ -464,6 +465,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun CompletableFuture.runAsync(() -> { setChunkReplacementPhase(phaseRef, effectiveListener, "chunk-load-callback", x, z); engine.getWorldManager().onChunkLoad(c, true); + world.refreshChunk(c.getX(), c.getZ()); }, syncExecutor).get(); } finally { Iris.tickets.removeTicket(c); 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 e66d3408d..4ce01f48a 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 @@ -86,16 +86,29 @@ import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import java.awt.Color; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.net.URI; +import java.net.URL; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.*; import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.regex.Pattern; import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; public class NMSBinding implements INMSBinding { private final KMap baseBiomeCache = new KMap<>(); @@ -903,6 +916,96 @@ public class NMSBinding implements INMSBinding { .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" + + "|structures?/.+\\.nbt" + + "|tags/worldgen/biome/has_structure/.+\\.json)$" + ); + + @Override + public Map extractVanillaDatapack() { + Map entries = new LinkedHashMap<>(); + File serverJar = resolveServerJar(); + if (serverJar == null) { + Iris.error("Unable to locate server JAR for vanilla datapack extraction."); + return entries; + } + + Iris.info("Extracting vanilla datapack from " + serverJar.getName() + "..."); + try (ZipFile zip = new ZipFile(serverJar)) { + Enumeration zipEntries = zip.entries(); + while (zipEntries.hasMoreElements()) { + ZipEntry entry = zipEntries.nextElement(); + if (entry.isDirectory()) { + continue; + } + String name = entry.getName(); + if (!VANILLA_DATAPACK_ENTRY.matcher(name).matches()) { + continue; + } + String datapackPath = normalizeStructurePath(name); + try (InputStream is = zip.getInputStream(entry)) { + entries.put(datapackPath, is.readAllBytes()); + } + } + } catch (IOException e) { + Iris.error("Failed to read vanilla datapack entries from " + serverJar.getName()); + e.printStackTrace(); + } + + Iris.info("Extracted " + entries.size() + " vanilla datapack entries."); + return entries; + } + + private static String normalizeStructurePath(String path) { + return path.replace("data/minecraft/structures/", "data/minecraft/structure/"); + } + + private static File resolveServerJar() { + try { + URL url = MinecraftServer.class.getProtectionDomain().getCodeSource().getLocation(); + if (url != null) { + File file = Path.of(url.toURI()).toFile(); + if (file.isFile() && file.getName().endsWith(".jar")) { + return file; + } + if (file.isDirectory()) { + return resolveServerJarFromDirectory(file); + } + } + } catch (Exception ignored) { + } + + String classpath = System.getProperty("java.class.path", ""); + for (String entry : classpath.split(File.pathSeparator)) { + File file = new File(entry); + if (file.isFile() && file.getName().endsWith(".jar") && containsVanillaData(file)) { + return file; + } + } + return null; + } + + private static File resolveServerJarFromDirectory(File dir) { + File[] jars = dir.listFiles((d, name) -> name.endsWith(".jar")); + if (jars == null) return null; + for (File jar : jars) { + if (containsVanillaData(jar)) { + return jar; + } + } + return null; + } + + private static boolean containsVanillaData(File jar) { + try (ZipFile zip = new ZipFile(jar)) { + return zip.getEntry("data/minecraft/worldgen/structure_set/villages.json") != null + || zip.getEntry("data/minecraft/worldgen/structure_set/village_plains.json") != null; + } catch (Exception e) { + return false; + } + } + @Override public Object createRuntimeLevelStem(Object registryAccess, ChunkGenerator raw) { if (!(registryAccess instanceof RegistryAccess access)) {