diff --git a/.gitignore b/.gitignore index 77cb82cbf..215c00356 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ libs/ collection/ /core/src/main/java/art/arcane/iris/util/uniques/ + +DataPackExamples/ diff --git a/DataPackExamples/Dungeons-And-Taverns-Data-Pack-1.21.11.zip b/DataPackExamples/Dungeons-And-Taverns-Data-Pack-1.21.11.zip deleted file mode 100644 index 1f07313d6..000000000 Binary files a/DataPackExamples/Dungeons-And-Taverns-Data-Pack-1.21.11.zip and /dev/null differ diff --git a/DataPackExamples/Qraftyfied-Structures-Data-Pack-1.21.11.zip b/DataPackExamples/Qraftyfied-Structures-Data-Pack-1.21.11.zip deleted file mode 100644 index 5dbd65e00..000000000 Binary files a/DataPackExamples/Qraftyfied-Structures-Data-Pack-1.21.11.zip and /dev/null differ diff --git a/DataPackExamples/caves.zip b/DataPackExamples/caves.zip deleted file mode 100644 index c44e1c0cb..000000000 Binary files a/DataPackExamples/caves.zip and /dev/null differ 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 a16e7101e..079a2f9ae 100644 --- a/core/src/main/java/art/arcane/iris/core/ExternalDataPackPipeline.java +++ b/core/src/main/java/art/arcane/iris/core/ExternalDataPackPipeline.java @@ -2,8 +2,10 @@ package art.arcane.iris.core; import art.arcane.iris.Iris; import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.nms.INMS; import art.arcane.iris.engine.object.IrisObject; import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisExternalDatapackReplaceTargets; import art.arcane.iris.engine.object.TileData; import art.arcane.iris.util.common.data.B; import art.arcane.iris.util.common.math.Vector3i; @@ -67,17 +69,22 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipFile; public final class ExternalDataPackPipeline { + private static final Pattern STRUCTURE_JSON_ENTRY = Pattern.compile("(?i)^data/([^/]+)/worldgen/structure/(.+)\\.json$"); + private static final Pattern STRUCTURE_SET_JSON_ENTRY = Pattern.compile("(?i)^data/([^/]+)/worldgen/structure_set/(.+)\\.json$"); + private static final Pattern TEMPLATE_POOL_JSON_ENTRY = Pattern.compile("(?i)^data/([^/]+)/worldgen/template_pool/(.+)\\.json$"); + private static final Pattern PROCESSOR_LIST_JSON_ENTRY = Pattern.compile("(?i)^data/([^/]+)/worldgen/processor_list/(.+)\\.json$"); + private static final Pattern BIOME_HAS_STRUCTURE_TAG_ENTRY = Pattern.compile("(?i)^data/([^/]+)/tags/worldgen/biome/has_structure/(.+)\\.json$"); private static final Pattern MODRINTH_VERSION_URL = Pattern.compile("^https?://modrinth\\.com/(?:datapack|mod|plugin|resourcepack)/([^/?#]+)/version/([^/?#]+).*$", Pattern.CASE_INSENSITIVE); private static final Pattern STRUCTURE_ENTRY = Pattern.compile("(?i)(?:^|.*/)data/([^/]+)/(?:structure|structures)/(.+\\.nbt)$"); - private static final String PACK_NAME = "datapack-imports"; + private static final String EXTERNAL_PACK_INDEX = "datapack-imports"; + private static final String PACK_NAME = EXTERNAL_PACK_INDEX; + 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 String IMPORT_PREFIX = "imports"; private static final int CONNECT_TIMEOUT_MS = 4000; private static final int READ_TIMEOUT_MS = 8000; private static final int IMPORT_PARALLELISM = Math.max(1, Math.min(8, Runtime.getRuntime().availableProcessors())); private static final int MAX_IN_FLIGHT = Math.max(2, IMPORT_PARALLELISM * 3); - private static final List PINNED_MODRINTH_URLS = List.of( - "https://modrinth.com/datapack/dungeons-and-taverns/version/v5.1.0" - ); private static final Map BLOCK_DATA_CACHE = new ConcurrentHashMap<>(); private static final Map PACK_ENVIRONMENT_CACHE = new ConcurrentHashMap<>(); private static final BlockData AIR = B.getAir(); @@ -85,164 +92,714 @@ public final class ExternalDataPackPipeline { private ExternalDataPackPipeline() { } - public static void syncPinnedDatapacks(File sourceFolder) { - if (sourceFolder == null) { - return; - } + public static String sanitizePackNameValue(String value) { + return sanitizePackName(value); + } - File cacheFolder = Iris.instance.getDataFolder("cache", "datapacks"); - cacheFolder.mkdirs(); - boolean offline = false; - int downloaded = 0; - int upToDate = 0; - int restoredFromCache = 0; - int failed = 0; + public static String normalizeEnvironmentValue(String value) { + return normalizeEnvironment(value); + } - for (String pageUrl : PINNED_MODRINTH_URLS) { - if (offline) { - break; - } + public static PipelineSummary processDatapacks(List requests, Map> worldDatapackFoldersByPack) { + PipelineSummary summary = new PipelineSummary(); + PACK_ENVIRONMENT_CACHE.clear(); - try { - ModrinthFile file = resolveModrinthFile(pageUrl); - if (file == null) { - failed++; + Set knownWorldDatapackFolders = new LinkedHashSet<>(); + if (worldDatapackFoldersByPack != null) { + for (Map.Entry> entry : worldDatapackFoldersByPack.entrySet()) { + KList folders = entry.getValue(); + if (folders == null) { continue; } - - File output = new File(sourceFolder, file.outputFileName()); - File cached = new File(cacheFolder, file.outputFileName()); - if (isUpToDate(output, file.sha1)) { - upToDate++; - continue; - } - - if (isUpToDate(cached, file.sha1)) { - copyFile(cached, output); - if (isUpToDate(output, file.sha1)) { - restoredFromCache++; - continue; + for (File folder : folders) { + if (folder != null) { + knownWorldDatapackFolders.add(folder); } - output.delete(); } + } + } + collectWorldDatapackFolders(knownWorldDatapackFolders); + summary.legacyDownloadRemovals = removeLegacyGlobalDownloads(); + summary.legacyWorldCopyRemovals = removeLegacyWorldDatapackCopies(knownWorldDatapackFolders); - downloadToFile(file.url, cached); - if (!isUpToDate(cached, file.sha1)) { - failed++; - cached.delete(); - Iris.warn("Pinned datapack hash mismatch for " + pageUrl); + List normalizedRequests = normalizeRequests(requests); + summary.requests = normalizedRequests.size(); + if (normalizedRequests.isEmpty()) { + summary.legacyWorldCopyRemovals += pruneManagedWorldDatapacks(knownWorldDatapackFolders, Set.of()); + return summary; + } + + List sourceInputs = new ArrayList<>(); + for (DatapackRequest request : normalizedRequests) { + if (request == null) { + continue; + } + + if (request.replaceVanilla() && !request.hasReplacementTargets()) { + if (request.required()) { + summary.requiredFailures++; + } else { + summary.optionalFailures++; + } + Iris.warn("Skipped external datapack request " + request.id() + " because replaceVanilla requires explicit replacement targets."); + continue; + } + + RequestSyncResult syncResult = syncRequest(request); + if (!syncResult.success()) { + if (request.required()) { + summary.requiredFailures++; + } else { + summary.optionalFailures++; + } + Iris.warn("Failed external datapack request " + request.id() + ": " + syncResult.error()); + continue; + } + + if (syncResult.downloaded()) { + summary.syncedRequests++; + } else if (syncResult.restored()) { + summary.restoredRequests++; + } + sourceInputs.add(new RequestedSourceInput(syncResult.source(), request)); + } + + if (sourceInputs.isEmpty()) { + if (summary.requiredFailures == 0) { + summary.legacyWorldCopyRemovals += pruneManagedWorldDatapacks(knownWorldDatapackFolders, Set.of()); + } + 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 (RequestedSourceInput sourceInput : sourceInputs) { + File entry = sourceInput.source(); + DatapackRequest request = sourceInput.request(); + if (entry == null || !entry.exists() || request == null) { + continue; + } + + SourceDescriptor sourceDescriptor = createSourceDescriptor(entry, request.targetPack(), request.requiredEnvironment()); + if (sourceDescriptor.requiredEnvironment() != null) { + String packEnvironment = resolvePackEnvironment(sourceDescriptor.targetPack()); + if (packEnvironment == null || !packEnvironment.equals(sourceDescriptor.requiredEnvironment())) { + if (request.required()) { + summary.requiredFailures++; + } else { + summary.optionalFailures++; + } + Iris.warn("Skipped external datapack source " + sourceDescriptor.sourceName() + + " targetPack=" + sourceDescriptor.targetPack() + + " requiredEnvironment=" + sourceDescriptor.requiredEnvironment() + + " packEnvironment=" + (packEnvironment == null ? "unknown" : packEnvironment)); continue; } + } - copyFile(cached, output); - if (!isUpToDate(output, file.sha1)) { - failed++; - output.delete(); - Iris.warn("Pinned datapack output write mismatch for " + pageUrl); - continue; + seenSourceKeys.add(sourceDescriptor.sourceKey()); + File sourceRoot = resolveSourceRoot(sourceDescriptor.targetPack(), sourceDescriptor.sourceKey()); + JSONObject cachedSource = oldSources.get(sourceDescriptor.sourceKey()); + String cachedTargetPack = cachedSource == null + ? null + : sanitizePackName(cachedSource.optString("targetPack", defaultTargetPack())); + boolean sameTargetPack = cachedTargetPack != null && cachedTargetPack.equals(sourceDescriptor.targetPack()); + + if (cachedSource != null + && sourceDescriptor.fingerprint().equals(cachedSource.optString("fingerprint", "")) + && sameTargetPack + && sourceRoot.exists()) { + newSources.put(cachedSource); + addSourceToSummary(importSummary, cachedSource, true); + } else { + if (cachedTargetPack != null && !cachedTargetPack.equals(sourceDescriptor.targetPack())) { + File previousSourceRoot = resolveSourceRoot(cachedTargetPack, sourceDescriptor.sourceKey()); + deleteFolder(previousSourceRoot); } - downloaded++; - Iris.info("Downloaded pinned datapack: " + output.getName()); - } catch (IOException e) { - offline = true; - failed++; - Iris.warn("Pinned datapack sync skipped because Iris appears offline: " + e.getMessage()); - } catch (Throwable e) { - failed++; - Iris.warn("Failed pinned datapack sync for " + pageUrl + ": " + e.getMessage()); - Iris.reportError(e); + deleteFolder(sourceRoot); + sourceRoot.mkdirs(); + JSONObject sourceResult = convertSource(entry, sourceDescriptor, sourceRoot); + newSources.put(sourceResult); + addSourceToSummary(importSummary, sourceResult, false); + if (sourceResult.optInt("failed", 0) > 0) { + if (request.required()) { + summary.requiredFailures++; + } else { + summary.optionalFailures++; + } + } + } + + KList targetWorldFolders = resolveTargetWorldFolders(request.targetPack(), worldDatapackFoldersByPack); + ProjectionResult projectionResult = projectSourceToWorldDatapacks(entry, sourceDescriptor, request, targetWorldFolders); + summary.worldDatapacksInstalled += projectionResult.installedDatapacks(); + summary.worldAssetsInstalled += projectionResult.installedAssets(); + if (projectionResult.managedName() != null && !projectionResult.managedName().isBlank() && projectionResult.installedDatapacks() > 0) { + activeManagedWorldDatapackNames.add(projectionResult.managedName()); + } + if (!projectionResult.success()) { + if (request.required()) { + summary.requiredFailures++; + } else { + summary.optionalFailures++; + } } } - if (downloaded > 0 || upToDate > 0 || restoredFromCache > 0 || failed > 0) { - Iris.info("Pinned datapack sync: downloaded=" + downloaded - + ", upToDate=" + upToDate - + ", restoredFromCache=" + restoredFromCache - + ", failed=" + failed); + pruneRemovedSourceFolders(oldSources, seenSourceKeys); + writeIndex(indexFile, newSources, importSummary); + summary.setImportSummary(importSummary); + if (summary.requiredFailures == 0) { + summary.legacyWorldCopyRemovals += pruneManagedWorldDatapacks(knownWorldDatapackFolders, activeManagedWorldDatapackNames); + } + + return summary; + } + + private static List normalizeRequests(List requests) { + Map deduplicated = new HashMap<>(); + if (requests == null) { + return new ArrayList<>(); + } + + for (DatapackRequest request : requests) { + if (request == null) { + continue; + } + String dedupeKey = request.getDedupeKey(); + if (dedupeKey.isBlank()) { + continue; + } + + DatapackRequest existing = deduplicated.get(dedupeKey); + if (existing == null) { + deduplicated.put(dedupeKey, request); + } else { + deduplicated.put(dedupeKey, existing.merge(request)); + } + } + + return new ArrayList<>(deduplicated.values()); + } + + private static RequestSyncResult syncRequest(DatapackRequest request) { + if (request == null) { + return RequestSyncResult.failure("request is null"); + } + + String url = request.url(); + if (url == null || url.isBlank()) { + return RequestSyncResult.failure("url is blank"); + } + + File packSourceFolder = Iris.instance.getDataFolder("packs", request.targetPack(), "externaldatapacks"); + File cacheFolder = Iris.instance.getDataFolder("cache", "datapacks"); + packSourceFolder.mkdirs(); + cacheFolder.mkdirs(); + + try { + ResolvedRemoteFile remoteFile = resolveRemoteFile(url); + File output = new File(packSourceFolder, remoteFile.outputFileName()); + File cached = new File(cacheFolder, remoteFile.outputFileName()); + if (isUpToDate(output, remoteFile.sha1())) { + writeRequestMetadata(packSourceFolder, request, output.getName(), remoteFile.sha1()); + return RequestSyncResult.restored(output); + } + + if (isUpToDate(cached, remoteFile.sha1())) { + copyFile(cached, output); + if (isUpToDate(output, remoteFile.sha1())) { + writeRequestMetadata(packSourceFolder, request, output.getName(), remoteFile.sha1()); + return RequestSyncResult.restored(output); + } + output.delete(); + } + + downloadToFile(remoteFile.url(), cached); + if (!isUpToDate(cached, remoteFile.sha1())) { + cached.delete(); + return RequestSyncResult.failure("hash mismatch for downloaded datapack"); + } + + copyFile(cached, output); + if (!isUpToDate(output, remoteFile.sha1())) { + output.delete(); + return RequestSyncResult.failure("output write mismatch after download"); + } + + writeRequestMetadata(packSourceFolder, request, output.getName(), remoteFile.sha1()); + return RequestSyncResult.downloaded(output); + } catch (Throwable e) { + RequestSyncResult restored = restoreRequestFromMetadata(packSourceFolder, request); + if (restored.success()) { + return restored; + } + String message = e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage(); + return RequestSyncResult.failure(message); } } - public static int removeLegacyWorldDatapackCopies(File sourceFolder, KList worldDatapackFolders) { - if (sourceFolder == null) { + private static ResolvedRemoteFile resolveRemoteFile(String url) throws IOException { + Matcher matcher = MODRINTH_VERSION_URL.matcher(url); + if (matcher.matches()) { + ModrinthFile modrinthFile = resolveModrinthFile(url); + return new ResolvedRemoteFile(modrinthFile.url(), modrinthFile.outputFileName(), modrinthFile.sha1()); + } + + String path = URI.create(url).getPath(); + String fileName = path == null || path.isBlank() ? "datapack.zip" : path; + String extension = extension(fileName); + String outputName = "external-" + shortHash(url) + extension; + return new ResolvedRemoteFile(url, outputName, null); + } + + private static void writeRequestMetadata(File packSourceFolder, DatapackRequest request, String fileName, String sha1) { + try { + File metadataFile = getRequestMetadataFile(packSourceFolder, request); + JSONObject metadata = new JSONObject(); + metadata.put("url", request.url()); + metadata.put("file", fileName); + if (sha1 != null && !sha1.isBlank()) { + metadata.put("sha1", sha1); + } + metadata.put("updatedAt", Instant.now().toString()); + Files.writeString(metadataFile.toPath(), metadata.toString(4), StandardCharsets.UTF_8); + } catch (Throwable e) { + Iris.reportError(e); + } + } + + private static RequestSyncResult restoreRequestFromMetadata(File packSourceFolder, DatapackRequest request) { + try { + File metadataFile = getRequestMetadataFile(packSourceFolder, request); + if (!metadataFile.exists() || !metadataFile.isFile()) { + return RequestSyncResult.failure("no cached metadata"); + } + + JSONObject metadata = new JSONObject(Files.readString(metadataFile.toPath(), StandardCharsets.UTF_8)); + String fileName = metadata.optString("file", ""); + if (fileName.isBlank()) { + return RequestSyncResult.failure("cached metadata missing file"); + } + + String sha1 = metadata.optString("sha1", ""); + File candidate = new File(packSourceFolder, fileName); + if (!isUpToDate(candidate, sha1.isBlank() ? null : sha1)) { + return RequestSyncResult.failure("cached datapack failed integrity check"); + } + return RequestSyncResult.restored(candidate); + } catch (Throwable e) { + return RequestSyncResult.failure("failed to restore cached metadata"); + } + } + + private static File getRequestMetadataFile(File packSourceFolder, DatapackRequest request) { + return new File(packSourceFolder, ".iris-request-" + shortHash(request.getDedupeKey()) + ".json"); + } + + private static int removeLegacyGlobalDownloads() { + File legacyFolder = Iris.instance.getDataFolder("datapacks"); + if (!legacyFolder.exists()) { return 0; } - Set datapackFolders = new LinkedHashSet<>(); - if (worldDatapackFolders != null) { - for (File folder : worldDatapackFolders) { - if (folder != null) { - datapackFolders.add(folder); - } - } - } - collectWorldDatapackFolders(datapackFolders); - if (datapackFolders.isEmpty()) { - return 0; - } - - Set managedNames = new HashSet<>(); - File[] sourceEntries = sourceFolder.listFiles(); - if (sourceEntries != null) { - for (File sourceEntry : sourceEntries) { - if (sourceEntry == null || sourceEntry.getName().startsWith(".")) { - continue; - } - if (isArchive(sourceEntry.getName()) - || (sourceEntry.isDirectory() && looksLikeDatapackDirectory(sourceEntry))) { - managedNames.add(sourceEntry.getName()); - } - } - } - - JSONObject index = readExistingIndex(new File(Iris.instance.getDataFolder("packs", PACK_NAME), "datapack-index.json")); - JSONArray indexedSources = index.optJSONArray("sources"); - if (indexedSources != null) { - for (int i = 0; i < indexedSources.length(); i++) { - JSONObject indexedSource = indexedSources.optJSONObject(i); - if (indexedSource == null) { - continue; - } - - String sourceName = indexedSource.optString("sourceName", ""); - if (!sourceName.isBlank()) { - managedNames.add(sourceName); - } - } - } + File[] entries = legacyFolder.listFiles(); + int removed = entries == null ? 0 : entries.length; + deleteFolder(legacyFolder); + return removed; + } + private static int removeLegacyWorldDatapackCopies(Set worldDatapackFolders) { int removed = 0; - for (File datapacksFolder : datapackFolders) { - if (datapacksFolder == null || !datapacksFolder.exists() || !datapacksFolder.isDirectory()) { + for (File folder : worldDatapackFolders) { + if (folder == null || !folder.exists() || !folder.isDirectory()) { continue; } - File[] datapackEntries = datapacksFolder.listFiles(); - if (datapackEntries == null || datapackEntries.length == 0) { + File[] entries = folder.listFiles(); + if (entries == null) { continue; } - for (File datapackEntry : datapackEntries) { - if (datapackEntry == null || datapackEntry.getName().startsWith(".")) { + for (File entry : entries) { + if (entry == null) { continue; } - - String lowerName = datapackEntry.getName().toLowerCase(Locale.ROOT); - boolean managed = managedNames.contains(datapackEntry.getName()) || lowerName.startsWith("modrinth-"); - if (!managed) { + String name = entry.getName().toLowerCase(Locale.ROOT); + if (!name.startsWith("modrinth-")) { continue; } - - deleteFolder(datapackEntry); - if (!datapackEntry.exists()) { + deleteFolder(entry); + if (!entry.exists()) { removed++; } } } - return removed; } + private static int pruneManagedWorldDatapacks(Set worldDatapackFolders, Set activeManagedNames) { + int removed = 0; + for (File folder : worldDatapackFolders) { + if (folder == null || !folder.exists() || !folder.isDirectory()) { + continue; + } + + File[] entries = folder.listFiles(); + if (entries == null) { + continue; + } + + for (File entry : entries) { + if (entry == null) { + continue; + } + String name = entry.getName(); + if (!name.startsWith(MANAGED_WORLD_PACK_PREFIX)) { + continue; + } + if (activeManagedNames.contains(name)) { + continue; + } + deleteFolder(entry); + if (!entry.exists()) { + removed++; + } + } + } + return removed; + } + + private static KList resolveTargetWorldFolders(String targetPack, Map> worldDatapackFoldersByPack) { + KList resolved = new KList<>(); + if (worldDatapackFoldersByPack == null || worldDatapackFoldersByPack.isEmpty()) { + return resolved; + } + + String normalizedPack = sanitizePackName(targetPack); + KList direct = worldDatapackFoldersByPack.get(normalizedPack); + if (direct != null) { + for (File file : direct) { + if (file != null && !resolved.contains(file)) { + resolved.add(file); + } + } + return resolved; + } + + KList fallback = worldDatapackFoldersByPack.get(targetPack); + if (fallback != null) { + for (File file : fallback) { + if (file != null && !resolved.contains(file)) { + resolved.add(file); + } + } + } + + return resolved; + } + + private static ProjectionResult projectSourceToWorldDatapacks(File source, SourceDescriptor sourceDescriptor, DatapackRequest request, KList worldDatapackFolders) { + if (source == null || sourceDescriptor == null || request == null) { + return ProjectionResult.failure(""); + } + + String managedName = buildManagedWorldDatapackName(sourceDescriptor.targetPack(), sourceDescriptor.sourceKey()); + if (worldDatapackFolders == null || worldDatapackFolders.isEmpty()) { + return ProjectionResult.success(managedName, 0, 0); + } + + int installedDatapacks = 0; + int installedAssets = 0; + for (File worldDatapackFolder : worldDatapackFolders) { + if (worldDatapackFolder == null) { + continue; + } + + try { + worldDatapackFolder.mkdirs(); + File managedFolder = new File(worldDatapackFolder, managedName); + deleteFolder(managedFolder); + int copiedAssets = copyProjectedEntries(source, managedFolder, request); + if (copiedAssets <= 0) { + deleteFolder(managedFolder); + continue; + } + writeManagedPackMeta(managedFolder); + installedDatapacks++; + installedAssets += copiedAssets; + } catch (Throwable e) { + Iris.warn("Failed to project external datapack source " + sourceDescriptor.sourceName() + " into " + worldDatapackFolder.getPath()); + Iris.reportError(e); + return ProjectionResult.failure(managedName); + } + } + + return ProjectionResult.success(managedName, installedDatapacks, installedAssets); + } + + private static int copyProjectedEntries(File source, File managedFolder, DatapackRequest request) throws IOException { + if (source.isDirectory()) { + return copyProjectedDirectoryEntries(source, managedFolder, request); + } + if (isArchive(source.getName())) { + return copyProjectedArchiveEntries(source, managedFolder, request); + } + return 0; + } + + private static int copyProjectedDirectoryEntries(File source, File managedFolder, DatapackRequest request) throws IOException { + int copied = 0; + ArrayDeque queue = new ArrayDeque<>(); + queue.add(source); + while (!queue.isEmpty()) { + File next = queue.removeFirst(); + File[] children = next.listFiles(); + if (children == null) { + continue; + } + + Arrays.sort(children, Comparator.comparing(File::getName, String.CASE_INSENSITIVE_ORDER)); + for (File child : children) { + if (child == null || child.getName().startsWith(".")) { + continue; + } + if (child.isDirectory()) { + queue.add(child); + continue; + } + + String relative = source.toPath().relativize(child.toPath()).toString().replace('\\', '/'); + String normalizedRelative = normalizeRelativePath(relative); + if (normalizedRelative == null || !shouldProjectEntry(normalizedRelative, request)) { + continue; + } + + File output = new File(managedFolder, normalizedRelative); + File parent = output.getParentFile(); + if (parent != null) { + parent.mkdirs(); + } + copyFile(child, output); + copied++; + } + } + return copied; + } + + private static int copyProjectedArchiveEntries(File source, File managedFolder, DatapackRequest request) throws IOException { + int copied = 0; + try (ZipFile zipFile = new ZipFile(source)) { + List entries = zipFile.stream() + .filter(entry -> !entry.isDirectory()) + .sorted(Comparator.comparing(ZipEntry::getName, String.CASE_INSENSITIVE_ORDER)) + .toList(); + + for (ZipEntry zipEntry : entries) { + String normalizedRelative = normalizeRelativePath(zipEntry.getName()); + if (normalizedRelative == null || !shouldProjectEntry(normalizedRelative, request)) { + continue; + } + + File output = new File(managedFolder, normalizedRelative); + File parent = output.getParentFile(); + if (parent != null) { + parent.mkdirs(); + } + try (InputStream inputStream = zipFile.getInputStream(zipEntry)) { + writeInputStreamToFile(inputStream, output); + } + copied++; + } + } + return copied; + } + + private static void writeInputStreamToFile(InputStream inputStream, File output) throws IOException { + File parent = output.getParentFile(); + if (parent != null) { + parent.mkdirs(); + } + + File temp = parent == null + ? new File(output.getPath() + ".tmp-" + System.nanoTime()) + : new File(parent, output.getName() + ".tmp-" + System.nanoTime()); + Files.copy(inputStream, temp.toPath(), StandardCopyOption.REPLACE_EXISTING); + try { + Files.move(temp.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + } catch (IOException e) { + Files.move(temp.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + } + + private static void writeManagedPackMeta(File managedFolder) throws IOException { + managedFolder.mkdirs(); + int packFormat = INMS.get().getDataVersion().getPackFormat(); + JSONObject root = new JSONObject(); + JSONObject pack = new JSONObject(); + pack.put("description", MANAGED_PACK_META_DESCRIPTION); + pack.put("pack_format", packFormat); + root.put("pack", pack); + Files.writeString(new File(managedFolder, "pack.mcmeta").toPath(), root.toString(4), StandardCharsets.UTF_8); + } + + private static boolean shouldProjectEntry(String relativePath, DatapackRequest request) { + ProjectedEntry entry = parseProjectedEntry(relativePath); + if (entry == null) { + return false; + } + + if (!"minecraft".equals(entry.namespace())) { + 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 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(); + }; + } + + private static ProjectedEntry parseProjectedEntry(String relativePath) { + String normalized = relativePath.replace('\\', '/'); + Matcher matcher = STRUCTURE_JSON_ENTRY.matcher(normalized); + if (matcher.matches()) { + String key = normalizeResourceKey(matcher.group(1), matcher.group(2), "worldgen/structure/"); + return key == null ? null : new ProjectedEntry(ProjectedEntryType.STRUCTURE, normalizeNamespace(matcher.group(1)), key); + } + + matcher = STRUCTURE_SET_JSON_ENTRY.matcher(normalized); + if (matcher.matches()) { + String key = normalizeResourceKey(matcher.group(1), matcher.group(2), "worldgen/structure_set/"); + return key == null ? null : new ProjectedEntry(ProjectedEntryType.STRUCTURE_SET, normalizeNamespace(matcher.group(1)), key); + } + + matcher = TEMPLATE_POOL_JSON_ENTRY.matcher(normalized); + if (matcher.matches()) { + String key = normalizeResourceKey(matcher.group(1), matcher.group(2), "worldgen/template_pool/"); + return key == null ? null : new ProjectedEntry(ProjectedEntryType.TEMPLATE_POOL, normalizeNamespace(matcher.group(1)), key); + } + + matcher = PROCESSOR_LIST_JSON_ENTRY.matcher(normalized); + if (matcher.matches()) { + String key = normalizeResourceKey(matcher.group(1), matcher.group(2), "worldgen/processor_list/"); + return key == null ? null : new ProjectedEntry(ProjectedEntryType.PROCESSOR_LIST, normalizeNamespace(matcher.group(1)), key); + } + + matcher = BIOME_HAS_STRUCTURE_TAG_ENTRY.matcher(normalized); + if (matcher.matches()) { + String key = normalizeResourceKey(matcher.group(1), matcher.group(2), "tags/worldgen/biome/has_structure/", "worldgen/biome/has_structure/", "has_structure/"); + return key == null ? null : new ProjectedEntry(ProjectedEntryType.BIOME_HAS_STRUCTURE_TAG, normalizeNamespace(matcher.group(1)), key); + } + + EntryPath entryPath = resolveEntryPath(normalized); + if (entryPath == null) { + return null; + } + String key = normalizeResourceKey(entryPath.namespace, stripExtension(entryPath.structurePath)); + return key == null ? null : new ProjectedEntry(ProjectedEntryType.STRUCTURE_NBT, normalizeNamespace(entryPath.namespace), key); + } + + private static String normalizeRelativePath(String value) { + if (value == null || value.isBlank()) { + return null; + } + String normalized = value.replace('\\', '/').replaceAll("/+", "/"); + normalized = normalized.replaceAll("^/+", "").replaceAll("/+$", ""); + if (normalized.isBlank() || normalized.contains("..")) { + return null; + } + return normalized; + } + + private static String buildManagedWorldDatapackName(String targetPack, String sourceKey) { + String pack = sanitizePackName(targetPack); + String source = sanitizePath(sourceKey).replace("/", "_"); + if (pack.isBlank()) { + pack = "pack"; + } + if (source.isBlank()) { + source = "source"; + } + return MANAGED_WORLD_PACK_PREFIX + pack + "-" + source; + } + + private static String normalizeNamespace(String namespace) { + String cleaned = sanitizePath(namespace); + return cleaned.isBlank() ? "minecraft" : cleaned; + } + + private static String normalizeResourceKey(String namespace, String value, String... prefixes) { + String normalizedNamespace = normalizeNamespace(namespace); + if (value == null || value.isBlank()) { + return null; + } + + String cleaned = value.trim().replace('\\', '/'); + if (cleaned.startsWith("#")) { + cleaned = cleaned.substring(1); + } + if (cleaned.isBlank()) { + return null; + } + + String keyNamespace = normalizedNamespace; + String path = cleaned; + int colon = cleaned.indexOf(':'); + if (colon > 0 && colon < cleaned.length() - 1) { + keyNamespace = normalizeNamespace(cleaned.substring(0, colon)); + path = cleaned.substring(colon + 1); + } + + path = sanitizePath(path); + if (path.isBlank()) { + return null; + } + + if (prefixes != null) { + for (String prefix : prefixes) { + String cleanedPrefix = sanitizePath(prefix); + if (!cleanedPrefix.isBlank() && path.startsWith(cleanedPrefix)) { + path = path.substring(cleanedPrefix.length()); + } + } + } + + path = path.replaceAll("^/+", "").replaceAll("/+$", ""); + if (path.endsWith(".json")) { + path = stripExtension(path); + } + if (path.endsWith(".nbt")) { + path = stripExtension(path); + } + if (path.isBlank()) { + return null; + } + + return keyNamespace + ":" + path; + } + private static void collectWorldDatapackFolders(Set folders) { try { File container = Bukkit.getWorldContainer(); @@ -271,224 +828,6 @@ public final class ExternalDataPackPipeline { } } - public static ImportSummary importDatapackStructures(File sourceFolder) { - ImportSummary summary = new ImportSummary(); - if (sourceFolder == null || !sourceFolder.exists()) { - return summary; - } - PACK_ENVIRONMENT_CACHE.clear(); - - File importPackFolder = Iris.instance.getDataFolder("packs", PACK_NAME); - 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<>(); - - List sources = discoverSources(sourceFolder); - if (sources.isEmpty()) { - pruneRemovedSourceFolders(oldSources, seenSourceKeys); - writeIndex(indexFile, newSources, summary); - return summary; - } - - for (SourceInput sourceInput : sources) { - File entry = sourceInput.source(); - if (entry == null || !entry.exists()) { - continue; - } - - SourceDescriptor sourceDescriptor = createSourceDescriptor(entry, sourceInput.targetPack(), sourceInput.requiredEnvironment()); - if (sourceDescriptor.requiredEnvironment() != null) { - String packEnvironment = resolvePackEnvironment(sourceDescriptor.targetPack()); - if (packEnvironment == null || !packEnvironment.equals(sourceDescriptor.requiredEnvironment())) { - summary.skipped++; - Iris.warn("Skipped external datapack source " + sourceDescriptor.sourceName() - + " targetPack=" + sourceDescriptor.targetPack() - + " requiredEnvironment=" + sourceDescriptor.requiredEnvironment() - + " packEnvironment=" + (packEnvironment == null ? "unknown" : packEnvironment)); - continue; - } - } - - seenSourceKeys.add(sourceDescriptor.sourceKey()); - File sourceRoot = resolveSourceRoot(sourceDescriptor.targetPack(), sourceDescriptor.sourceKey()); - JSONObject cachedSource = oldSources.get(sourceDescriptor.sourceKey()); - String cachedTargetPack = cachedSource == null - ? null - : sanitizePackName(cachedSource.optString("targetPack", defaultTargetPack())); - boolean sameTargetPack = cachedTargetPack != null && cachedTargetPack.equals(sourceDescriptor.targetPack()); - - if (cachedSource != null - && sourceDescriptor.fingerprint().equals(cachedSource.optString("fingerprint", "")) - && sameTargetPack - && sourceRoot.exists()) { - newSources.put(cachedSource); - addSourceToSummary(summary, cachedSource, true); - continue; - } - - if (cachedTargetPack != null && !cachedTargetPack.equals(sourceDescriptor.targetPack())) { - File previousSourceRoot = resolveSourceRoot(cachedTargetPack, sourceDescriptor.sourceKey()); - deleteFolder(previousSourceRoot); - } - - deleteFolder(sourceRoot); - sourceRoot.mkdirs(); - JSONObject sourceResult = convertSource(entry, sourceDescriptor, sourceRoot); - newSources.put(sourceResult); - addSourceToSummary(summary, sourceResult, false); - } - - pruneRemovedSourceFolders(oldSources, seenSourceKeys); - writeIndex(indexFile, newSources, summary); - return summary; - } - - private static List discoverSources(File sourceFolder) { - List sources = new ArrayList<>(); - File[] entries = sourceFolder.listFiles(); - if (entries == null || entries.length == 0) { - return sources; - } - - Arrays.sort(entries, Comparator.comparing(File::getName, String.CASE_INSENSITIVE_ORDER)); - String defaultPack = defaultTargetPack(); - for (File entry : entries) { - if (entry == null || !entry.exists() || entry.getName().startsWith(".")) { - continue; - } - - if (entry.isDirectory() && !looksLikeDatapackDirectory(entry)) { - collectContainerSources(entry, sources, defaultPack); - continue; - } - - if (!(entry.isDirectory() || isArchive(entry.getName()))) { - continue; - } - - SourceControl sourceControl = resolveSourceControl(entry, defaultPack, null); - sources.add(new SourceInput(entry, sourceControl.targetPack(), sourceControl.requiredEnvironment())); - } - - return sources; - } - - private static void collectContainerSources(File container, List sources, String defaultPack) { - File[] children = container.listFiles(); - if (children == null || children.length == 0) { - return; - } - - Arrays.sort(children, Comparator.comparing(File::getName, String.CASE_INSENSITIVE_ORDER)); - SourceControl containerControl = resolveContainerControl(container, defaultPack); - for (File child : children) { - if (child == null || !child.exists() || child.getName().startsWith(".")) { - continue; - } - if (!(child.isDirectory() || isArchive(child.getName()))) { - continue; - } - - SourceControl sourceControl = resolveSourceControl(child, containerControl.targetPack(), containerControl.requiredEnvironment()); - sources.add(new SourceInput(child, sourceControl.targetPack(), sourceControl.requiredEnvironment())); - } - } - - private static SourceControl resolveContainerControl(File container, String defaultPack) { - String targetPack = sanitizePackName(container.getName()); - if (targetPack.isEmpty()) { - targetPack = sanitizePackName(defaultPack); - } - if (targetPack.isEmpty()) { - targetPack = defaultTargetPack(); - } - - File control = new File(container, ".iris-pack.json"); - if (!control.exists() || !control.isFile()) { - return new SourceControl(targetPack, null); - } - - return parseSourceControl(control, targetPack, null); - } - - private static SourceControl resolveSourceControl(File source, String targetPack, String requiredEnvironment) { - String sanitizedPack = sanitizePackName(targetPack); - if (sanitizedPack.isEmpty()) { - sanitizedPack = defaultTargetPack(); - } - String normalizedEnvironment = normalizeEnvironment(requiredEnvironment); - File controlFile = findSourceControlFile(source); - if (controlFile == null) { - return new SourceControl(sanitizedPack, normalizedEnvironment); - } - return parseSourceControl(controlFile, sanitizedPack, normalizedEnvironment); - } - - private static SourceControl parseSourceControl(File controlFile, String defaultTargetPack, String defaultRequiredEnvironment) { - String targetPack = defaultTargetPack; - String requiredEnvironment = defaultRequiredEnvironment; - try { - String raw = Files.readString(controlFile.toPath(), StandardCharsets.UTF_8); - JSONObject json = new JSONObject(raw); - String configuredPack = sanitizePackName(json.optString("targetPack", "")); - if (!configuredPack.isEmpty()) { - targetPack = configuredPack; - } - - String configuredEnvironment = normalizeEnvironment(json.optString("environment", json.optString("dimension", ""))); - if (configuredEnvironment != null) { - requiredEnvironment = configuredEnvironment; - } - } catch (Throwable e) { - Iris.warn("Failed to parse external datapack control file " + controlFile.getPath()); - Iris.reportError(e); - } - - return new SourceControl(targetPack, requiredEnvironment); - } - - private static File findSourceControlFile(File source) { - if (source == null) { - return null; - } - - if (source.isDirectory()) { - File hidden = new File(source, ".iris-import.json"); - if (hidden.exists() && hidden.isFile()) { - return hidden; - } - - File plain = new File(source, "iris-import.json"); - if (plain.exists() && plain.isFile()) { - return plain; - } - } - - File parent = source.getParentFile(); - if (parent == null) { - return null; - } - - File exact = new File(parent, source.getName() + ".iris.json"); - if (exact.exists() && exact.isFile()) { - return exact; - } - - String stripped = stripExtension(source.getName()); - if (!stripped.equals(source.getName())) { - File strippedFile = new File(parent, stripped + ".iris.json"); - if (strippedFile.exists() && strippedFile.isFile()) { - return strippedFile; - } - } - - return null; - } - private static String defaultTargetPack() { String configured = sanitizePackName(IrisSettings.get().getGenerator().getDefaultWorldType()); if (!configured.isEmpty()) { @@ -1483,15 +1822,290 @@ public final class ExternalDataPackPipeline { } } + public record DatapackRequest( + String id, + String url, + String targetPack, + String requiredEnvironment, + boolean required, + boolean replaceVanilla, + Set structures, + Set structureSets, + Set templatePools, + Set processorLists, + Set biomeHasStructureTags + ) { + public DatapackRequest( + String id, + String url, + String targetPack, + String requiredEnvironment, + boolean required, + boolean replaceVanilla, + IrisExternalDatapackReplaceTargets replaceTargets + ) { + this( + normalizeRequestId(id, url), + url == null ? "" : url.trim(), + normalizeRequestPack(targetPack), + normalizeEnvironment(requiredEnvironment), + required, + replaceVanilla, + normalizeTargets(replaceTargets == null ? null : replaceTargets.getStructures(), "worldgen/structure/"), + normalizeTargets(replaceTargets == null ? null : replaceTargets.getStructureSets(), "worldgen/structure_set/"), + 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/") + ); + } + + public DatapackRequest { + id = normalizeRequestId(id, url); + url = url == null ? "" : url.trim(); + targetPack = normalizeRequestPack(targetPack); + requiredEnvironment = normalizeEnvironment(requiredEnvironment); + structures = immutableSet(structures); + structureSets = immutableSet(structureSets); + templatePools = immutableSet(templatePools); + processorLists = immutableSet(processorLists); + biomeHasStructureTags = immutableSet(biomeHasStructureTags); + } + + public String getDedupeKey() { + return targetPack + "|" + url; + } + + public boolean hasReplacementTargets() { + return !structures.isEmpty() + || !structureSets.isEmpty() + || !templatePools.isEmpty() + || !processorLists.isEmpty() + || !biomeHasStructureTags.isEmpty(); + } + + public DatapackRequest merge(DatapackRequest other) { + if (other == null) { + return this; + } + String environment = requiredEnvironment; + if ((environment == null || environment.isBlank()) && other.requiredEnvironment != null && !other.requiredEnvironment.isBlank()) { + environment = other.requiredEnvironment; + } + return new DatapackRequest( + id, + url, + targetPack, + environment, + required || other.required, + replaceVanilla || other.replaceVanilla, + union(structures, other.structures), + union(structureSets, other.structureSets), + union(templatePools, other.templatePools), + union(processorLists, other.processorLists), + union(biomeHasStructureTags, other.biomeHasStructureTags) + ); + } + + private static String normalizeRequestId(String id, String url) { + String cleaned = id == null ? "" : id.trim(); + if (!cleaned.isBlank()) { + return cleaned; + } + return url == null ? "" : url.trim(); + } + + private static String normalizeRequestPack(String targetPack) { + String sanitized = sanitizePackName(targetPack); + if (!sanitized.isBlank()) { + return sanitized; + } + return defaultTargetPack(); + } + + private static Set normalizeTargets(KList values, String... prefixes) { + LinkedHashSet normalized = new LinkedHashSet<>(); + if (values == null) { + return normalized; + } + for (String value : values) { + String normalizedKey = normalizeResourceKey("minecraft", value, prefixes); + if (normalizedKey != null && !normalizedKey.isBlank()) { + normalized.add(normalizedKey); + } + } + return normalized; + } + + private static Set immutableSet(Set values) { + LinkedHashSet copy = new LinkedHashSet<>(); + if (values != null) { + copy.addAll(values); + } + return Set.copyOf(copy); + } + + private static Set union(Set first, Set second) { + LinkedHashSet merged = new LinkedHashSet<>(); + if (first != null) { + merged.addAll(first); + } + if (second != null) { + merged.addAll(second); + } + return merged; + } + } + + public static final class PipelineSummary { + private int requests; + private int syncedRequests; + private int restoredRequests; + private int optionalFailures; + private int requiredFailures; + private int importedSources; + private int cachedSources; + private int scannedStructures; + private int convertedStructures; + private int failedConversions; + private int skippedConversions; + private int entitiesIgnored; + private int blockEntities; + private int worldDatapacksInstalled; + private int worldAssetsInstalled; + private int legacyDownloadRemovals; + private int legacyWorldCopyRemovals; + + private void setImportSummary(ImportSummary importSummary) { + if (importSummary == null) { + return; + } + this.importedSources = importSummary.getSources(); + this.cachedSources = importSummary.getCachedSources(); + this.scannedStructures = importSummary.getNbtScanned(); + this.convertedStructures = importSummary.getConverted(); + this.failedConversions = importSummary.getFailed(); + this.skippedConversions = importSummary.getSkipped(); + this.entitiesIgnored = importSummary.getEntitiesIgnored(); + this.blockEntities = importSummary.getBlockEntities(); + } + + public int getRequests() { + return requests; + } + + public int getSyncedRequests() { + return syncedRequests; + } + + public int getRestoredRequests() { + return restoredRequests; + } + + public int getOptionalFailures() { + return optionalFailures; + } + + public int getRequiredFailures() { + return requiredFailures; + } + + public int getImportedSources() { + return importedSources; + } + + public int getCachedSources() { + return cachedSources; + } + + public int getScannedStructures() { + return scannedStructures; + } + + public int getConvertedStructures() { + return convertedStructures; + } + + public int getFailedConversions() { + return failedConversions; + } + + public int getSkippedConversions() { + return skippedConversions; + } + + public int getEntitiesIgnored() { + return entitiesIgnored; + } + + public int getBlockEntities() { + return blockEntities; + } + + public int getWorldDatapacksInstalled() { + return worldDatapacksInstalled; + } + + public int getWorldAssetsInstalled() { + return worldAssetsInstalled; + } + + public int getLegacyDownloadRemovals() { + return legacyDownloadRemovals; + } + + public int getLegacyWorldCopyRemovals() { + return legacyWorldCopyRemovals; + } + } + + private record RequestedSourceInput(File source, DatapackRequest request) { + } + + private record ResolvedRemoteFile(String url, String outputFileName, String sha1) { + } + + private record RequestSyncResult(boolean success, boolean downloaded, boolean restored, File source, String error) { + private static RequestSyncResult downloaded(File source) { + return new RequestSyncResult(true, true, false, source, ""); + } + + private static RequestSyncResult restored(File source) { + return new RequestSyncResult(true, false, true, source, ""); + } + + private static RequestSyncResult failure(String error) { + return new RequestSyncResult(false, false, false, null, error == null ? "unknown error" : error); + } + } + + private record ProjectedEntry(ProjectedEntryType type, String namespace, String key) { + } + + private enum ProjectedEntryType { + STRUCTURE, + STRUCTURE_SET, + TEMPLATE_POOL, + PROCESSOR_LIST, + STRUCTURE_NBT, + BIOME_HAS_STRUCTURE_TAG + } + + private record ProjectionResult(boolean success, int installedDatapacks, int installedAssets, String managedName) { + private static ProjectionResult success(String managedName, int installedDatapacks, int installedAssets) { + return new ProjectionResult(true, installedDatapacks, installedAssets, managedName); + } + + private static ProjectionResult failure(String managedName) { + return new ProjectionResult(false, 0, 0, managedName); + } + } + private record EntryPath(String originalPath, String namespace, String structurePath) { } - private record SourceInput(File source, String targetPack, String requiredEnvironment) { - } - - private record SourceControl(String targetPack, String requiredEnvironment) { - } - private record SourceDescriptor(String sourceKey, String sourceName, String fingerprint, String targetPack, String requiredEnvironment) { } diff --git a/core/src/main/java/art/arcane/iris/core/IrisSettings.java b/core/src/main/java/art/arcane/iris/core/IrisSettings.java index fda47968c..bbb9fd6bb 100644 --- a/core/src/main/java/art/arcane/iris/core/IrisSettings.java +++ b/core/src/main/java/art/arcane/iris/core/IrisSettings.java @@ -257,7 +257,6 @@ public class IrisSettings { public boolean preventLeafDecay = true; public boolean useMulticore = false; public boolean useMulticoreMantle = false; - public boolean offsetNoiseTypes = false; public boolean earlyCustomBlocks = false; } 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 6f1a3fa8b..02766b809 100644 --- a/core/src/main/java/art/arcane/iris/core/ServerConfigurator.java +++ b/core/src/main/java/art/arcane/iris/core/ServerConfigurator.java @@ -20,6 +20,7 @@ package art.arcane.iris.core; import art.arcane.iris.Iris; import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.loader.ResourceLoader; import art.arcane.iris.core.nms.INMS; import art.arcane.iris.core.nms.datapack.DataVersion; import art.arcane.iris.core.nms.datapack.IDataFixer; @@ -142,27 +143,156 @@ public class ServerConfigurator { return; } - File source = Iris.instance.getDataFolder("datapacks"); - source.mkdirs(); - ExternalDataPackPipeline.syncPinnedDatapacks(source); - int removedLegacyCopies = ExternalDataPackPipeline.removeLegacyWorldDatapackCopies(source, folders); - ExternalDataPackPipeline.ImportSummary summary = ExternalDataPackPipeline.importDatapackStructures(source); - if (removedLegacyCopies > 0) { - Iris.info("Removed " + removedLegacyCopies + " legacy external datapack world copies."); + KList requests = collectExternalDatapackRequests(); + KMap> worldDatapackFoldersByPack = collectWorldDatapackFoldersByPack(folders); + ExternalDataPackPipeline.PipelineSummary summary = ExternalDataPackPipeline.processDatapacks(requests, worldDatapackFoldersByPack); + if (summary.getLegacyDownloadRemovals() > 0) { + Iris.info("Removed " + summary.getLegacyDownloadRemovals() + " legacy global datapack downloads."); } - if (summary.getSources() > 0) { - Iris.info("External datapack structure import: sources=" + summary.getSources() - + ", cached=" + summary.getCachedSources() - + ", scanned=" + summary.getNbtScanned() - + ", converted=" + summary.getConverted() - + ", failed=" + summary.getFailed() - + ", skipped=" + summary.getSkipped() - + ", entitiesIgnored=" + summary.getEntitiesIgnored() - + ", blockEntities=" + summary.getBlockEntities()); + if (summary.getLegacyWorldCopyRemovals() > 0) { + Iris.info("Removed " + summary.getLegacyWorldCopyRemovals() + " legacy managed world datapack copies."); } - if (summary.getSources() > 0 || summary.getConverted() > 0) { - Iris.info("External datapack world install is disabled; only structure template import is applied."); + if (summary.getRequests() > 0 || summary.getImportedSources() > 0 || summary.getWorldDatapacksInstalled() > 0) { + Iris.info("External datapack sync/import/install: requests=" + summary.getRequests() + + ", synced=" + summary.getSyncedRequests() + + ", restored=" + summary.getRestoredRequests() + + ", importedSources=" + summary.getImportedSources() + + ", cachedSources=" + summary.getCachedSources() + + ", converted=" + summary.getConvertedStructures() + + ", failedConversions=" + summary.getFailedConversions() + + ", worldDatapacks=" + summary.getWorldDatapacksInstalled() + + ", worldAssets=" + summary.getWorldAssetsInstalled() + + ", optionalFailures=" + summary.getOptionalFailures() + + ", requiredFailures=" + summary.getRequiredFailures()); } + if (summary.getRequiredFailures() > 0) { + throw new IllegalStateException("Required external datapack setup failed for " + summary.getRequiredFailures() + " request(s)."); + } + } + + private static KList collectExternalDatapackRequests() { + KMap deduplicated = new KMap<>(); + try (Stream stream = allPacks()) { + stream.forEach(data -> { + ResourceLoader loader = data.getDimensionLoader(); + if (loader == null) { + return; + } + + KList dimensions = loader.loadAll(loader.getPossibleKeys()); + for (IrisDimension dimension : dimensions) { + if (dimension == null || dimension.getExternalDatapacks() == null || dimension.getExternalDatapacks().isEmpty()) { + continue; + } + + String targetPack = sanitizePackName(dimension.getLoadKey()); + if (targetPack.isBlank()) { + targetPack = sanitizePackName(data.getDataFolder().getName()); + } + String environment = ExternalDataPackPipeline.normalizeEnvironmentValue(dimension.getEnvironment() == null ? null : dimension.getEnvironment().name()); + + for (IrisExternalDatapack externalDatapack : dimension.getExternalDatapacks()) { + if (externalDatapack == null || !externalDatapack.isEnabled()) { + continue; + } + + String url = externalDatapack.getUrl() == null ? "" : externalDatapack.getUrl().trim(); + if (url.isBlank()) { + continue; + } + + String requestId = externalDatapack.getId() == null ? "" : externalDatapack.getId().trim(); + if (requestId.isBlank()) { + requestId = url; + } + + IrisExternalDatapackReplaceTargets replaceTargets = externalDatapack.getReplaceTargets(); + ExternalDataPackPipeline.DatapackRequest request = new ExternalDataPackPipeline.DatapackRequest( + requestId, + url, + targetPack, + environment, + externalDatapack.isRequired(), + externalDatapack.isReplaceVanilla(), + replaceTargets + ); + + String dedupeKey = request.getDedupeKey(); + ExternalDataPackPipeline.DatapackRequest existing = deduplicated.get(dedupeKey); + if (existing == null) { + deduplicated.put(dedupeKey, request); + continue; + } + + deduplicated.put(dedupeKey, existing.merge(request)); + } + } + }); + } + + return new KList<>(deduplicated.v()); + } + + private static KMap> collectWorldDatapackFoldersByPack(KList fallbackFolders) { + KMap> foldersByPack = new KMap<>(); + KMap mappedWorlds = IrisWorlds.get().getWorlds(); + + for (String worldName : mappedWorlds.k()) { + String packName = sanitizePackName(mappedWorlds.get(worldName)); + if (packName.isBlank()) { + continue; + } + File datapacksFolder = new File(Bukkit.getWorldContainer(), worldName + File.separator + "datapacks"); + addWorldDatapackFolder(foldersByPack, packName, datapacksFolder); + } + + for (org.bukkit.World world : Bukkit.getWorlds()) { + String worldName = world.getName(); + String mappedPack = mappedWorlds.get(worldName); + String packName = sanitizePackName(mappedPack); + if (packName.isBlank()) { + packName = sanitizePackName(IrisSettings.get().getGenerator().getDefaultWorldType()); + } + if (packName.isBlank()) { + continue; + } + File datapacksFolder = new File(world.getWorldFolder(), "datapacks"); + addWorldDatapackFolder(foldersByPack, packName, datapacksFolder); + } + + String defaultPack = sanitizePackName(IrisSettings.get().getGenerator().getDefaultWorldType()); + if (!defaultPack.isBlank()) { + for (File folder : fallbackFolders) { + addWorldDatapackFolder(foldersByPack, defaultPack, folder); + } + } + + return foldersByPack; + } + + private static void addWorldDatapackFolder(KMap> foldersByPack, String packName, File folder) { + if (folder == null || packName == null || packName.isBlank()) { + return; + } + KList folders = foldersByPack.computeIfAbsent(packName, k -> new KList<>()); + if (!folders.contains(folder)) { + folders.add(folder); + } + } + + private static String sanitizePackName(String value) { + if (value == null) { + return ""; + } + String sanitized = value.trim().toLowerCase().replace("\\", "/"); + sanitized = sanitized.replaceAll("[^a-z0-9_\\-./]", "_"); + sanitized = sanitized.replaceAll("/+", "/"); + sanitized = sanitized.replaceAll("^/+", ""); + sanitized = sanitized.replaceAll("/+$", ""); + if (sanitized.contains("..")) { + sanitized = sanitized.replace("..", "_"); + } + return sanitized.replace("/", "_"); } private static boolean verifyDataPacksPost(boolean allowRestarting) { diff --git a/core/src/main/java/art/arcane/iris/core/commands/CommandEdit.java b/core/src/main/java/art/arcane/iris/core/commands/CommandEdit.java index 16faa3194..5493c1604 100644 --- a/core/src/main/java/art/arcane/iris/core/commands/CommandEdit.java +++ b/core/src/main/java/art/arcane/iris/core/commands/CommandEdit.java @@ -114,22 +114,4 @@ public class CommandEdit implements DirectorExecutor { } } - @Director(description = "Edit the cave file you specified", aliases = {"c"}, origin = DirectorOrigin.PLAYER) - public void cave(@Param(contextual = false, description = "The cave to edit") IrisCave cave) { - if (noStudio()) { - return; - } - try { - if (cave == null || cave.getLoadFile() == null) { - sender().sendMessage(C.GOLD + "Cannot find the file; Perhaps it was not loaded directly from a file?"); - return; - } - Desktop.getDesktop().open(cave.getLoadFile()); - sender().sendMessage(C.GREEN + "Opening " + cave.getTypeName() + " " + cave.getLoadFile().getName().split("\\Q.\\E")[0] + " in VSCode! "); - } catch (Throwable e) { - Iris.reportError(e); - sender().sendMessage(C.RED + "Cant find the file. Or registrant does not exist"); - } - } - } diff --git a/core/src/main/java/art/arcane/iris/core/loader/IrisData.java b/core/src/main/java/art/arcane/iris/core/loader/IrisData.java index f43115f6f..12afee310 100644 --- a/core/src/main/java/art/arcane/iris/core/loader/IrisData.java +++ b/core/src/main/java/art/arcane/iris/core/loader/IrisData.java @@ -74,8 +74,6 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory { private ResourceLoader matterLoader; private ResourceLoader imageLoader; private ResourceLoader scriptLoader; - private ResourceLoader caveLoader; - private ResourceLoader ravineLoader; private ResourceLoader matterObjectLoader; private KMap> possibleSnippets; private Gson gson; @@ -158,10 +156,6 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory { return loadAny(IrisScript.class, key, nearest); } - public static IrisRavine loadAnyRavine(String key, @Nullable IrisData nearest) { - return loadAny(IrisRavine.class, key, nearest); - } - public static IrisRegion loadAnyRegion(String key, @Nullable IrisData nearest) { return loadAny(IrisRegion.class, key, nearest); } @@ -170,10 +164,6 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory { return loadAny(IrisMarker.class, key, nearest); } - public static IrisCave loadAnyCave(String key, @Nullable IrisData nearest) { - return loadAny(IrisCave.class, key, nearest); - } - public static IrisImage loadAnyImage(String key, @Nullable IrisData nearest) { return loadAny(IrisImage.class, key, nearest); } @@ -352,9 +342,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory { this.modLoader = registerLoader(IrisMod.class); this.dimensionLoader = registerLoader(IrisDimension.class); this.generatorLoader = registerLoader(IrisGenerator.class); - this.caveLoader = registerLoader(IrisCave.class); this.markerLoader = registerLoader(IrisMarker.class); - this.ravineLoader = registerLoader(IrisRavine.class); this.blockLoader = registerLoader(IrisBlockData.class); this.expressionLoader = registerLoader(IrisExpression.class); this.objectLoader = registerLoader(IrisObject.class); diff --git a/core/src/main/java/art/arcane/iris/engine/IrisComplex.java b/core/src/main/java/art/arcane/iris/engine/IrisComplex.java index 81bfe076b..3de022964 100644 --- a/core/src/main/java/art/arcane/iris/engine/IrisComplex.java +++ b/core/src/main/java/art/arcane/iris/engine/IrisComplex.java @@ -119,7 +119,6 @@ public class IrisComplex implements DataProvider { .forEach(this::registerGenerators)); } generatorBounds = buildGeneratorBounds(engine); - boolean legacy = engine.getDimension().isLegacyRarity(); KList overlayNoise = engine.getDimension().getOverlayNoise(); overlayStream = overlayNoise.isEmpty() ? ProceduralStream.ofDouble((x, z) -> 0.0D).waste("Overlay Stream") @@ -143,7 +142,7 @@ public class IrisComplex implements DataProvider { ProceduralStream.of((x, z) -> focusRegion, Interpolated.of(a -> 0D, a -> focusRegion)) : regionStyleStream - .selectRarity(data.getRegionLoader().loadAll(engine.getDimension().getRegions()), legacy) + .selectRarity(data.getRegionLoader().loadAll(engine.getDimension().getRegions())) .cache2D("regionStream", engine, cacheSize).waste("Region Stream"); regionIDStream = regionIdentityStream.convertCached((i) -> new UUID(Double.doubleToLongBits(i), String.valueOf(i * 38445).hashCode() * 3245556666L)).waste("Region ID Stream"); @@ -152,7 +151,7 @@ public class IrisComplex implements DataProvider { -> engine.getDimension().getCaveBiomeStyle().create(rng.nextParallelRNG(InferredType.CAVE.ordinal()), getData()).stream() .zoom(engine.getDimension().getBiomeZoom()) .zoom(r.getCaveBiomeZoom()) - .selectRarity(data.getBiomeLoader().loadAll(r.getCaveBiomes()), legacy) + .selectRarity(data.getBiomeLoader().loadAll(r.getCaveBiomes())) .onNull(emptyBiome) ).convertAware2D(ProceduralStream::get).cache2D("caveBiomeStream", engine, cacheSize).waste("Cave Biome Stream"); inferredStreams.put(InferredType.CAVE, caveBiomeStream); @@ -162,7 +161,7 @@ public class IrisComplex implements DataProvider { .zoom(engine.getDimension().getBiomeZoom()) .zoom(engine.getDimension().getLandZoom()) .zoom(r.getLandBiomeZoom()) - .selectRarity(data.getBiomeLoader().loadAll(r.getLandBiomes(), (t) -> t.setInferredType(InferredType.LAND)), legacy) + .selectRarity(data.getBiomeLoader().loadAll(r.getLandBiomes(), (t) -> t.setInferredType(InferredType.LAND))) ).convertAware2D(ProceduralStream::get) .cache2D("landBiomeStream", engine, cacheSize).waste("Land Biome Stream"); inferredStreams.put(InferredType.LAND, landBiomeStream); @@ -172,7 +171,7 @@ public class IrisComplex implements DataProvider { .zoom(engine.getDimension().getBiomeZoom()) .zoom(engine.getDimension().getSeaZoom()) .zoom(r.getSeaBiomeZoom()) - .selectRarity(data.getBiomeLoader().loadAll(r.getSeaBiomes(), (t) -> t.setInferredType(InferredType.SEA)), legacy) + .selectRarity(data.getBiomeLoader().loadAll(r.getSeaBiomes(), (t) -> t.setInferredType(InferredType.SEA))) ).convertAware2D(ProceduralStream::get) .cache2D("seaBiomeStream", engine, cacheSize).waste("Sea Biome Stream"); inferredStreams.put(InferredType.SEA, seaBiomeStream); @@ -181,7 +180,7 @@ public class IrisComplex implements DataProvider { -> engine.getDimension().getShoreBiomeStyle().create(rng.nextParallelRNG(InferredType.SHORE.ordinal()), getData()).stream() .zoom(engine.getDimension().getBiomeZoom()) .zoom(r.getShoreBiomeZoom()) - .selectRarity(data.getBiomeLoader().loadAll(r.getShoreBiomes(), (t) -> t.setInferredType(InferredType.SHORE)), legacy) + .selectRarity(data.getBiomeLoader().loadAll(r.getShoreBiomes(), (t) -> t.setInferredType(InferredType.SHORE))) ).convertAware2D(ProceduralStream::get).cache2D("shoreBiomeStream", engine, cacheSize).waste("Shore Biome Stream"); inferredStreams.put(InferredType.SHORE, shoreBiomeStream); bridgeStream = focusBiome != null ? ProceduralStream.of((x, z) -> focusBiome.getInferredType(), diff --git a/core/src/main/java/art/arcane/iris/engine/IrisEngineMantle.java b/core/src/main/java/art/arcane/iris/engine/IrisEngineMantle.java index 38c27df2b..27addb091 100644 --- a/core/src/main/java/art/arcane/iris/engine/IrisEngineMantle.java +++ b/core/src/main/java/art/arcane/iris/engine/IrisEngineMantle.java @@ -41,6 +41,7 @@ import art.arcane.volmlib.util.mantle.runtime.MantleDataAdapter; import art.arcane.volmlib.util.mantle.runtime.MantleHooks; import art.arcane.volmlib.util.mantle.runtime.TectonicPlate; import art.arcane.volmlib.util.mantle.flag.MantleFlag; +import art.arcane.volmlib.util.mantle.flag.ReservedFlag; import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; import art.arcane.iris.util.common.format.C; import art.arcane.volmlib.util.matter.IrisMatter; @@ -153,7 +154,14 @@ public class IrisEngineMantle implements EngineMantle { } private Set getDisabledFlags() { - return disabledFlags.aquire(() -> Set.copyOf(getDimension().getDisabledComponents())); + return disabledFlags.aquire(() -> { + KList disabled = new KList<>(); + disabled.addAll(getDimension().getDisabledComponents()); + if (!getDimension().isCarvingEnabled()) { + disabled.addIfMissing(ReservedFlag.CARVED); + } + return Set.copyOf(disabled); + }); } @Override 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 d454bb72b..396b333aa 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 @@ -29,6 +29,8 @@ import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.matter.MatterCavern; +import java.util.Arrays; + public class IrisCaveCarver3D { private static final byte LIQUID_AIR = 0; private static final byte LIQUID_WATER = 1; @@ -81,6 +83,27 @@ public class IrisCaveCarver3D { } public int carve(MantleWriter writer, int chunkX, int chunkZ) { + double[] fullWeights = new double[256]; + Arrays.fill(fullWeights, 1D); + return carve(writer, chunkX, chunkZ, fullWeights, 0D, 0D); + } + + public int carve( + MantleWriter writer, + int chunkX, + int chunkZ, + double[] columnWeights, + double minWeight, + double thresholdPenalty + ) { + if (columnWeights == null || columnWeights.length < 256) { + double[] fullWeights = new double[256]; + Arrays.fill(fullWeights, 1D); + columnWeights = fullWeights; + } + + double resolvedMinWeight = Math.max(0D, Math.min(1D, minWeight)); + double resolvedThresholdPenalty = Math.max(0D, thresholdPenalty); int worldHeight = writer.getMantle().getWorldHeight(); int minY = Math.max(0, (int) Math.floor(profile.getVerticalRange().getMin())); int maxY = Math.min(worldHeight - 1, (int) Math.ceil(profile.getVerticalRange().getMax())); @@ -140,6 +163,9 @@ public class IrisCaveCarver3D { surfaceBreakFloorY, surfaceBreakColumn, columnThreshold, + columnWeights, + resolvedMinWeight, + resolvedThresholdPenalty, 0D, false ); @@ -162,6 +188,9 @@ public class IrisCaveCarver3D { surfaceBreakFloorY, surfaceBreakColumn, columnThreshold, + columnWeights, + resolvedMinWeight, + resolvedThresholdPenalty, recoveryThresholdBoost, true ); @@ -185,6 +214,9 @@ public class IrisCaveCarver3D { int[] surfaceBreakFloorY, boolean[] surfaceBreakColumn, double[] columnThreshold, + double[] columnWeights, + double minWeight, + double thresholdPenalty, double thresholdBoost, boolean skipExistingCarved ) { @@ -195,6 +227,11 @@ public class IrisCaveCarver3D { for (int lz = 0; lz < 16; lz++) { int z = z0 + lz; int index = (lx << 4) | lz; + double columnWeight = clampColumnWeight(columnWeights[index]); + if (columnWeight <= minWeight) { + continue; + } + int columnTopY = columnMaxY[index]; if (columnTopY < minY) { continue; @@ -203,7 +240,7 @@ public class IrisCaveCarver3D { boolean breakColumn = surfaceBreakColumn[index]; int breakFloorY = surfaceBreakFloorY[index]; int surfaceY = columnSurface[index]; - double threshold = columnThreshold[index] + thresholdBoost; + double threshold = columnThreshold[index] + thresholdBoost - ((1D - columnWeight) * thresholdPenalty); for (int y = minY; y <= columnTopY; y += sampleStep) { double localThreshold = threshold; @@ -353,6 +390,22 @@ public class IrisCaveCarver3D { return sampleDensity(x, y, z) > threshold; } + private double clampColumnWeight(double weight) { + if (Double.isNaN(weight) || Double.isInfinite(weight)) { + return 0D; + } + + if (weight <= 0D) { + return 0D; + } + + if (weight >= 1D) { + return 1D; + } + + return weight; + } + private double signed(double value) { return (value * 2D) - 1D; } 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 0eace6a87..c59b8f8e3 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 @@ -18,26 +18,33 @@ package art.arcane.iris.engine.mantle.components; -import art.arcane.iris.engine.data.cache.Cache; import art.arcane.iris.engine.mantle.ComponentFlag; import art.arcane.iris.engine.mantle.EngineMantle; import art.arcane.iris.engine.mantle.IrisMantleComponent; import art.arcane.iris.engine.mantle.MantleWriter; import art.arcane.iris.engine.object.IrisBiome; -import art.arcane.iris.engine.object.IrisCarving; import art.arcane.iris.engine.object.IrisCaveProfile; -import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisRegion; import art.arcane.iris.util.project.context.ChunkContext; import art.arcane.volmlib.util.documentation.ChunkCoordinates; import art.arcane.volmlib.util.mantle.flag.ReservedFlag; -import art.arcane.volmlib.util.math.RNG; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; import java.util.IdentityHashMap; +import java.util.List; import java.util.Map; @ComponentFlag(ReservedFlag.CARVED) public class MantleCarvingComponent extends IrisMantleComponent { + private static final int CHUNK_SIZE = 16; + private static final int CHUNK_AREA = CHUNK_SIZE * CHUNK_SIZE; + private static final int BLEND_RADIUS = 3; + private static final int FIELD_SIZE = CHUNK_SIZE + (BLEND_RADIUS * 2); + private static final double MIN_WEIGHT = 0.08D; + private static final double THRESHOLD_PENALTY = 0.24D; + private final Map profileCarvers = new IdentityHashMap<>(); public MantleCarvingComponent(EngineMantle engineMantle) { @@ -46,71 +53,147 @@ public class MantleCarvingComponent extends IrisMantleComponent { @Override public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) { - RNG rng = new RNG(Cache.key(x, z) + seed()); - int xxx = 8 + (x << 4); - int zzz = 8 + (z << 4); - IrisRegion region = getComplex().getRegionStream().get(xxx, zzz); - IrisBiome surfaceBiome = getComplex().getTrueBiomeStream().get(xxx, zzz); - IrisCaveProfile caveBiomeProfile = resolveDominantCaveBiomeProfile(x, z); - carve(writer, rng, x, z, region, surfaceBiome, caveBiomeProfile); - } - - @ChunkCoordinates - private void carve(MantleWriter writer, RNG rng, int cx, int cz, IrisRegion region, IrisBiome surfaceBiome, IrisCaveProfile caveBiomeProfile) { - IrisCaveProfile dimensionProfile = getDimension().getCaveProfile(); - IrisCaveProfile surfaceBiomeProfile = surfaceBiome.getCaveProfile(); - IrisCaveProfile regionProfile = region.getCaveProfile(); - IrisCaveProfile activeProfile = resolveActiveProfile(dimensionProfile, regionProfile, surfaceBiomeProfile, caveBiomeProfile); - if (isProfileEnabled(activeProfile)) { - int carved = carveProfile(activeProfile, writer, cx, cz); - if (carved > 0) { - return; - } - - if (activeProfile != regionProfile && isProfileEnabled(regionProfile)) { - carved = carveProfile(regionProfile, writer, cx, cz); - if (carved > 0) { - return; - } - } - - if (activeProfile != surfaceBiomeProfile && isProfileEnabled(surfaceBiomeProfile)) { - carved = carveProfile(surfaceBiomeProfile, writer, cx, cz); - if (carved > 0) { - return; - } - } - - if (activeProfile != dimensionProfile && isProfileEnabled(dimensionProfile)) { - carved = carveProfile(dimensionProfile, writer, cx, cz); - if (carved > 0) { - return; - } - } + List weightedProfiles = resolveWeightedProfiles(x, z); + for (WeightedProfile weightedProfile : weightedProfiles) { + carveProfile(weightedProfile.profile, weightedProfile.columnWeights, writer, x, z); } - - carve(getDimension().getCarving(), writer, nextCarveRng(rng, cx, cz), cx, cz); - carve(surfaceBiome.getCarving(), writer, nextCarveRng(rng, cx, cz), cx, cz); - carve(region.getCarving(), writer, nextCarveRng(rng, cx, cz), cx, cz); } @ChunkCoordinates - private void carve(IrisCarving carving, MantleWriter writer, RNG rng, int cx, int cz) { - carving.doCarving(writer, rng, getEngineMantle().getEngine(), cx << 4, -1, cz << 4, 0); - } - - private RNG nextCarveRng(RNG rng, int cx, int cz) { - return new RNG((rng.nextLong() * cx) + 490495L + cz); - } - - @ChunkCoordinates - private int carveProfile(IrisCaveProfile profile, MantleWriter writer, int cx, int cz) { - if (!isProfileEnabled(profile)) { - return 0; - } - + private void carveProfile(IrisCaveProfile profile, double[] columnWeights, MantleWriter writer, int cx, int cz) { IrisCaveCarver3D carver = getCarver(profile); - return carver.carve(writer, cx, cz); + carver.carve(writer, cx, cz, columnWeights, MIN_WEIGHT, THRESHOLD_PENALTY); + } + + private List resolveWeightedProfiles(int chunkX, int chunkZ) { + IrisCaveProfile[] profileField = buildProfileField(chunkX, chunkZ); + Map profileWeights = new IdentityHashMap<>(); + + for (int localX = 0; localX < CHUNK_SIZE; localX++) { + for (int localZ = 0; localZ < CHUNK_SIZE; localZ++) { + int columnIndex = (localX << 4) | localZ; + Map columnInfluence = sampleColumnInfluence(profileField, localX, localZ); + for (Map.Entry entry : columnInfluence.entrySet()) { + double[] weights = profileWeights.computeIfAbsent(entry.getKey(), key -> new double[CHUNK_AREA]); + weights[columnIndex] = entry.getValue(); + } + } + } + + List weightedProfiles = new ArrayList<>(); + for (Map.Entry entry : profileWeights.entrySet()) { + IrisCaveProfile profile = entry.getKey(); + double[] weights = entry.getValue(); + double totalWeight = 0D; + double maxWeight = 0D; + + for (double weight : weights) { + totalWeight += weight; + if (weight > maxWeight) { + maxWeight = weight; + } + } + + if (maxWeight < MIN_WEIGHT) { + continue; + } + + double averageWeight = totalWeight / CHUNK_AREA; + weightedProfiles.add(new WeightedProfile(profile, weights, averageWeight)); + } + + weightedProfiles.sort(Comparator.comparingDouble(WeightedProfile::averageWeight)); + return weightedProfiles; + } + + private Map sampleColumnInfluence(IrisCaveProfile[] profileField, int localX, int localZ) { + Map profileBlend = new IdentityHashMap<>(); + int centerX = localX + BLEND_RADIUS; + int centerZ = localZ + BLEND_RADIUS; + double totalKernelWeight = 0D; + + for (int offsetX = -BLEND_RADIUS; offsetX <= BLEND_RADIUS; offsetX++) { + for (int offsetZ = -BLEND_RADIUS; offsetZ <= BLEND_RADIUS; offsetZ++) { + int sampleX = centerX + offsetX; + int sampleZ = centerZ + offsetZ; + IrisCaveProfile profile = profileField[(sampleX * FIELD_SIZE) + sampleZ]; + if (!isProfileEnabled(profile)) { + continue; + } + + double kernelWeight = haloWeight(offsetX, offsetZ); + profileBlend.merge(profile, kernelWeight, Double::sum); + totalKernelWeight += kernelWeight; + } + } + + if (totalKernelWeight <= 0D || profileBlend.isEmpty()) { + return Collections.emptyMap(); + } + + Map normalized = new IdentityHashMap<>(); + for (Map.Entry entry : profileBlend.entrySet()) { + normalized.put(entry.getKey(), entry.getValue() / totalKernelWeight); + } + + return normalized; + } + + private IrisCaveProfile[] buildProfileField(int chunkX, int chunkZ) { + IrisCaveProfile[] profileField = new IrisCaveProfile[FIELD_SIZE * FIELD_SIZE]; + int startX = (chunkX << 4) - BLEND_RADIUS; + int startZ = (chunkZ << 4) - BLEND_RADIUS; + + for (int fieldX = 0; fieldX < FIELD_SIZE; fieldX++) { + int worldX = startX + fieldX; + for (int fieldZ = 0; fieldZ < FIELD_SIZE; fieldZ++) { + int worldZ = startZ + fieldZ; + profileField[(fieldX * FIELD_SIZE) + fieldZ] = resolveColumnProfile(worldX, worldZ); + } + } + + return profileField; + } + + private double haloWeight(int offsetX, int offsetZ) { + int edgeDistance = Math.max(Math.abs(offsetX), Math.abs(offsetZ)); + return (BLEND_RADIUS + 1D) - edgeDistance; + } + + private IrisCaveProfile resolveColumnProfile(int worldX, int worldZ) { + IrisCaveProfile resolved = null; + IrisCaveProfile dimensionProfile = getDimension().getCaveProfile(); + if (isProfileEnabled(dimensionProfile)) { + resolved = dimensionProfile; + } + + IrisRegion region = getComplex().getRegionStream().get(worldX, worldZ); + if (region != null) { + IrisCaveProfile regionProfile = region.getCaveProfile(); + if (isProfileEnabled(regionProfile)) { + resolved = regionProfile; + } + } + + IrisBiome surfaceBiome = getComplex().getTrueBiomeStream().get(worldX, worldZ); + if (surfaceBiome != null) { + IrisCaveProfile surfaceProfile = surfaceBiome.getCaveProfile(); + if (isProfileEnabled(surfaceProfile)) { + resolved = surfaceProfile; + } + } + + int surfaceY = getEngineMantle().getEngine().getHeight(worldX, worldZ, true); + int sampleY = Math.max(1, surfaceY - 56); + IrisBiome caveBiome = getEngineMantle().getEngine().getCaveBiome(worldX, sampleY, worldZ); + if (caveBiome != null) { + IrisCaveProfile caveProfile = caveBiome.getCaveProfile(); + if (isProfileEnabled(caveProfile)) { + resolved = caveProfile; + } + } + + return resolved; } private IrisCaveCarver3D getCarver(IrisCaveProfile profile) { @@ -130,82 +213,23 @@ public class MantleCarvingComponent extends IrisMantleComponent { return profile != null && profile.isEnabled(); } - @ChunkCoordinates - private IrisCaveProfile resolveDominantCaveBiomeProfile(int chunkX, int chunkZ) { - int[] offsets = new int[]{1, 4, 8, 12, 15}; - Map profileVotes = new IdentityHashMap<>(); - int validSamples = 0; - IrisCaveProfile dominantProfile = null; - int dominantVotes = 0; - - for (int offsetX : offsets) { - for (int offsetZ : offsets) { - int sampleX = (chunkX << 4) + offsetX; - int sampleZ = (chunkZ << 4) + offsetZ; - int surfaceY = getEngineMantle().getEngine().getHeight(sampleX, sampleZ, true); - int sampleY = Math.max(1, surfaceY - 56); - IrisBiome caveBiome = getEngineMantle().getEngine().getCaveBiome(sampleX, sampleY, sampleZ); - if (caveBiome == null) { - continue; - } - - IrisCaveProfile profile = caveBiome.getCaveProfile(); - if (!isProfileEnabled(profile)) { - continue; - } - - int votes = profileVotes.getOrDefault(profile, 0) + 1; - profileVotes.put(profile, votes); - validSamples++; - if (votes > dominantVotes) { - dominantVotes = votes; - dominantProfile = profile; - } - } - } - - if (dominantProfile == null || validSamples <= 0) { - return null; - } - - int requiredVotes = Math.max(13, (int) Math.ceil(validSamples * 0.65D)); - if (dominantVotes < requiredVotes) { - return null; - } - - return dominantProfile; - } - - private IrisCaveProfile resolveActiveProfile(IrisCaveProfile dimensionProfile, IrisCaveProfile regionProfile, IrisCaveProfile surfaceBiomeProfile, IrisCaveProfile caveBiomeProfile) { - if (isProfileEnabled(caveBiomeProfile)) { - return caveBiomeProfile; - } - - if (isProfileEnabled(surfaceBiomeProfile)) { - return surfaceBiomeProfile; - } - - if (isProfileEnabled(regionProfile)) { - return regionProfile; - } - - return dimensionProfile; - } - protected int computeRadius() { - IrisDimension dimension = getDimension(); - int max = 0; + return 0; + } - max = Math.max(max, dimension.getCarving().getMaxRange(getData(), 0)); + private static final class WeightedProfile { + private final IrisCaveProfile profile; + private final double[] columnWeights; + private final double averageWeight; - for (IrisRegion i : dimension.getAllRegions(this::getData)) { - max = Math.max(max, i.getCarving().getMaxRange(getData(), 0)); + private WeightedProfile(IrisCaveProfile profile, double[] columnWeights, double averageWeight) { + this.profile = profile; + this.columnWeights = columnWeights; + this.averageWeight = averageWeight; } - for (IrisBiome i : dimension.getAllBiomes(this::getData)) { - max = Math.max(max, i.getCarving().getMaxRange(getData(), 0)); + private double averageWeight() { + return averageWeight; } - - return max; } } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IRare.java b/core/src/main/java/art/arcane/iris/engine/object/IRare.java index 9135d6064..a2dce98cc 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IRare.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IRare.java @@ -24,9 +24,9 @@ import art.arcane.iris.util.project.stream.interpolation.Interpolated; import java.util.List; public interface IRare { - static ProceduralStream stream(ProceduralStream noise, List possibilities, boolean legacyRarity) { - return ProceduralStream.of(legacyRarity ? (x, z) -> pickLegacy(possibilities, noise.get(x, z)) : (x, z) -> pick(possibilities, noise.get(x, z)), - legacyRarity ? (x, y, z) -> pickLegacy(possibilities, noise.get(x, y, z)) : (x, y, z) -> pick(possibilities, noise.get(x, y, z)), + static ProceduralStream stream(ProceduralStream noise, List possibilities) { + return ProceduralStream.of((x, z) -> pick(possibilities, noise.get(x, z)), + (x, y, z) -> pick(possibilities, noise.get(x, y, z)), new Interpolated() { @Override public double toDouble(T t) { @@ -89,63 +89,6 @@ public interface IRare { return possibilities.getLast(); } - static T pickLegacy(List possibilities, double noiseValue) { - if (possibilities.isEmpty()) { - return null; - } - - if (possibilities.size() == 1) { - return possibilities.get(0); - } - int totalWeight = 0; // This is he baseline - int buffer = 0; - for (T i : possibilities) { // Im adding all of the rarity together - totalWeight += i.getRarity(); - } - double threshold = totalWeight * (possibilities.size() - 1) * noiseValue; - for (T i : possibilities) { - buffer += totalWeight - i.getRarity(); - - if (buffer >= threshold) { - return i; - } - } - return possibilities.get(possibilities.size() - 1); - } - - - static T pickOld(List possibilities, double noiseValue) { - if (possibilities.isEmpty()) { - return null; - } - - if (possibilities.size() == 1) { - return possibilities.get(0); - } - - double completeWeight = 0.0; - double highestWeight = 0.0; - - for (T item : possibilities) { - double weight = Math.max(item.getRarity(), 1); - highestWeight = Math.max(highestWeight, weight); - completeWeight += weight; - } - - double r = noiseValue * completeWeight; - double countWeight = 0.0; - - for (T item : possibilities) { - double weight = Math.max(highestWeight - Math.max(item.getRarity(), 1), 1); - countWeight += weight; - if (countWeight >= r) { - return item; - } - } - - return possibilities.get(possibilities.size() - 1); - } - static int get(Object v) { return v instanceof IRare ? Math.max(1, ((IRare) v).getRarity()) : 1; } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java index 5621ae093..01e4f7aa6 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java @@ -100,8 +100,6 @@ public class IrisBiome extends IrisRegistrant implements IRare { private boolean lockLayers = false; @Desc("The max layers to iterate below the surface for locked layer biomes (mesa).") private int lockLayersMax = 7; - @Desc("Carving configuration for the dimension") - private IrisCarving carving = new IrisCarving(); @Desc("Profile-driven 3D cave configuration") private IrisCaveProfile caveProfile = new IrisCaveProfile(); @Desc("Configuration of fluid bodies such as rivers & lakes") diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisCarving.java b/core/src/main/java/art/arcane/iris/engine/object/IrisCarving.java deleted file mode 100644 index 33b39f44f..000000000 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisCarving.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.engine.object; - -import art.arcane.iris.core.loader.IrisData; -import art.arcane.iris.engine.framework.Engine; -import art.arcane.iris.engine.mantle.MantleWriter; -import art.arcane.iris.engine.object.annotations.ArrayType; -import art.arcane.iris.engine.object.annotations.Desc; -import art.arcane.iris.engine.object.annotations.Snippet; -import art.arcane.volmlib.util.collection.KList; -import art.arcane.volmlib.util.documentation.BlockCoordinates; -import art.arcane.volmlib.util.math.RNG; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@Snippet("carving") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Represents a carving configuration") -@Data -public class IrisCarving { - @ArrayType(type = IrisCavePlacer.class, min = 1) - @Desc("Define cave placers") - private KList caves = new KList<>(); - - @ArrayType(type = IrisRavinePlacer.class, min = 1) - @Desc("Define ravine placers") - private KList ravines = new KList<>(); - - @ArrayType(type = IrisElipsoid.class, min = 1) - @Desc("Define elipsoids") - private KList elipsoids = new KList<>(); - - @ArrayType(type = IrisSphere.class, min = 1) - @Desc("Define spheres") - private KList spheres = new KList<>(); - - @ArrayType(type = IrisPyramid.class, min = 1) - @Desc("Define pyramids") - private KList pyramids = new KList<>(); - - - @BlockCoordinates - public void doCarving(MantleWriter writer, RNG rng, Engine engine, int x, int y, int z, int depth) { - doCarving(writer, rng, new RNG(engine.getSeedManager().getCarve()), engine, x, y, z, depth, -1); - } - - @BlockCoordinates - public void doCarving(MantleWriter writer, RNG rng, RNG base, Engine engine, int x, int y, int z, int recursion, int waterHint) { - int nextRecursion = recursion + 1; - - if (caves.isNotEmpty()) { - for (IrisCavePlacer i : caves) { - if (recursion > i.getMaxRecursion()) continue; - i.generateCave(writer, rng, base, engine, x, y, z, nextRecursion, waterHint); - } - } - - if (ravines.isNotEmpty()) { - for (IrisRavinePlacer i : ravines) { - if (recursion > i.getMaxRecursion()) continue; - i.generateRavine(writer, rng, base, engine, x, y, z, nextRecursion, waterHint); - } - } - - if (spheres.isNotEmpty()) { - for (IrisSphere i : spheres) { - if (rng.nextInt(i.getRarity()) == 0) { - i.generate(base, engine, writer, x, y, z); - } - } - } - - if (elipsoids.isNotEmpty()) { - for (IrisElipsoid i : elipsoids) { - if (rng.nextInt(i.getRarity()) == 0) { - i.generate(base, engine, writer, x, y, z); - } - } - } - - if (pyramids.isNotEmpty()) { - for (IrisPyramid i : pyramids) { - if (rng.nextInt(i.getRarity()) == 0) { - i.generate(base, engine, writer, x, y, z); - } - } - } - } - - public int getMaxRange(IrisData data, int recursion) { - int max = 0; - int nextRecursion = recursion + 1; - - for (IrisCavePlacer i : caves) { - if (recursion > i.getMaxRecursion()) continue; - max = Math.max(max, i.getSize(data, nextRecursion)); - } - - for (IrisRavinePlacer i : ravines) { - if (recursion > i.getMaxRecursion()) continue; - max = Math.max(max, i.getSize(data, nextRecursion)); - } - - if (elipsoids.isNotEmpty()) { - max = (int) Math.max(elipsoids.stream().mapToDouble(IrisElipsoid::maxSize).max().getAsDouble(), max); - } - - if (spheres.isNotEmpty()) { - max = (int) Math.max(spheres.stream().mapToDouble(IrisSphere::maxSize).max().getAsDouble(), max); - } - - if (pyramids.isNotEmpty()) { - max = (int) Math.max(pyramids.stream().mapToDouble(IrisPyramid::maxSize).max().getAsDouble(), max); - } - - return max; - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisCave.java b/core/src/main/java/art/arcane/iris/engine/object/IrisCave.java deleted file mode 100644 index 94c5c822b..000000000 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisCave.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.engine.object; - -import art.arcane.iris.core.loader.IrisData; -import art.arcane.iris.core.loader.IrisRegistrant; -import art.arcane.iris.engine.framework.Engine; -import art.arcane.iris.engine.mantle.MantleWriter; -import art.arcane.iris.engine.object.annotations.Desc; -import art.arcane.iris.engine.object.annotations.RegistryListResource; -import art.arcane.volmlib.util.collection.KList; -import art.arcane.volmlib.util.collection.KSet; -import art.arcane.volmlib.util.json.JSONObject; -import art.arcane.volmlib.util.math.RNG; -import art.arcane.volmlib.util.matter.MatterCavern; -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; - -@EqualsAndHashCode(callSuper = true) -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Translate objects") -@Data -public class IrisCave extends IrisRegistrant { - @Desc("Define the shape of this cave") - private IrisWorm worm = new IrisWorm(); - - @Desc("Define potential forking features") - private IrisCarving fork = new IrisCarving(); - - @RegistryListResource(IrisBiome.class) - @Desc("Force this cave to only generate the specified custom biome") - private String customBiome = ""; - - @Desc("Limit the worm from ever getting higher or lower than this range") - private IrisRange verticalRange = new IrisRange(3, 255); - - @Desc("Shape of the caves") - private IrisCaveShape shape = new IrisCaveShape(); - - @Override - public String getFolderName() { - return "caves"; - } - - @Override - public String getTypeName() { - return "Cave"; - } - - public void generate(MantleWriter writer, RNG rng, Engine engine, int x, int y, int z) { - generate(writer, rng, new RNG(engine.getSeedManager().getCarve()), engine, x, y, z, 0, -1, true); - } - - public void generate(MantleWriter writer, RNG rng, RNG base, Engine engine, int x, int y, int z, int recursion, int waterHint, boolean breakSurface) { - double girth = getWorm().getGirth().get(base.nextParallelRNG(465156), x, z, engine.getData()); - KList points = getWorm().generate(base.nextParallelRNG(784684), engine.getData(), writer, verticalRange, x, y, z, breakSurface, girth + 9); - int highestWater = Math.max(waterHint, -1); - - if (highestWater == -1) { - for (IrisPosition i : points) { - double yy = i.getY() + girth; - int th = engine.getHeight(x, z, true); - - if (yy > th && th < engine.getDimension().getFluidHeight()) { - highestWater = Math.max(highestWater, (int) yy); - break; - } - } - } - - - int h = Math.min(highestWater, engine.getDimension().getFluidHeight()); - - for (IrisPosition i : points) { - fork.doCarving(writer, rng, base, engine, i.getX(), i.getY(), i.getZ(), recursion, h); - } - - MatterCavern c = new MatterCavern(true, customBiome, (byte) 0); - MatterCavern w = new MatterCavern(true, customBiome, (byte) 1); - - CNG cng = shape.getNoise(base.nextParallelRNG(8131545), engine); - KSet mask = shape.getMasked(rng, engine); - writer.setNoiseMasked(points, - girth, shape.getNoiseThreshold() < 0 ? cng.noise(x, y, z) : shape.getNoiseThreshold(), cng, mask, true, - (xf, yf, zf) -> yf <= h ? w : c); - } - - @Override - public void scanForErrors(JSONObject p, VolmitSender sender) { - - } - - public int getMaxSize(IrisData data, int depth) { - return (int) (Math.ceil(getWorm().getGirth().getMax() * 2) + getWorm().getMaxDistance() + fork.getMaxRange(data, depth)); - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisCavePlacer.java b/core/src/main/java/art/arcane/iris/engine/object/IrisCavePlacer.java deleted file mode 100644 index 321cc356f..000000000 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisCavePlacer.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.engine.object; - -import art.arcane.iris.Iris; -import art.arcane.iris.core.loader.IrisData; -import art.arcane.iris.engine.data.cache.AtomicCache; -import art.arcane.iris.engine.framework.Engine; -import art.arcane.iris.engine.mantle.MantleWriter; -import art.arcane.iris.engine.object.annotations.*; -import art.arcane.volmlib.util.math.RNG; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -import java.util.concurrent.atomic.AtomicBoolean; - -@Snippet("cave-placer") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Translate objects") -@Data -public class IrisCavePlacer implements IRare { - private transient final AtomicCache caveCache = new AtomicCache<>(); - private transient final AtomicBoolean fail = new AtomicBoolean(false); - @Required - @Desc("Typically a 1 in RARITY on a per chunk/fork basis") - @MinNumber(1) - private int rarity = 15; - @MinNumber(1) - @Required - @Desc("The cave to place") - @RegistryListResource(IrisCave.class) - private String cave; - @MinNumber(1) - @MaxNumber(256) - @Desc("The maximum recursion depth") - private int maxRecursion = 16; - @Desc("If set to true, this cave is allowed to break the surface") - private boolean breakSurface = true; - @Desc("The height range this cave can spawn at. If breakSurface is false, the output of this range will be clamped by the current world height to prevent surface breaking.") - private IrisStyledRange caveStartHeight = new IrisStyledRange(13, 120, new IrisGeneratorStyle(NoiseStyle.STATIC)); - - public IrisCave getRealCave(IrisData data) { - return caveCache.aquire(() -> data.getCaveLoader().load(getCave())); - } - - public void generateCave(MantleWriter mantle, RNG rng, Engine engine, int x, int y, int z) { - generateCave(mantle, rng, new RNG(engine.getSeedManager().getCarve()), engine, x, y, z, 0, -1); - } - - public void generateCave(MantleWriter mantle, RNG rng, RNG base, Engine engine, int x, int y, int z, int recursion, int waterHint) { - if (fail.get()) { - return; - } - - if (rng.nextInt(rarity) != 0) { - return; - } - - IrisData data = engine.getData(); - IrisCave cave = getRealCave(data); - - if (cave == null) { - Iris.warn("Unable to locate cave for generation!"); - fail.set(true); - return; - } - - if (y == -1) { - int h = (int) caveStartHeight.get(base, x, z, data); - int ma = breakSurface ? h : (int) (engine.getComplex().getHeightStream().get(x, z) - 9); - y = Math.min(h, ma); - } - - try { - cave.generate(mantle, rng, base, engine, x + rng.nextInt(15), y, z + rng.nextInt(15), recursion, waterHint, breakSurface); - } catch (Throwable e) { - e.printStackTrace(); - fail.set(true); - } - } - - public int getSize(IrisData data, int depth) { - IrisCave cave = getRealCave(data); - - if (cave != null) { - return cave.getMaxSize(data, depth); - } - - return 32; - } -} 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 c2358b046..5c9054229 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 @@ -140,16 +140,19 @@ 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("Carving configuration for the dimension") - private IrisCarving carving = new IrisCarving(); + @Desc("Add painted walls in post processing") + private boolean postProcessingWalls = true; + @Desc("Enable or disable all carving for this dimension") + private boolean carvingEnabled = true; @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("forceConvertTo320Height") - private Boolean forceConvertTo320Height = false; + @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) @@ -255,12 +258,10 @@ public class IrisDimension extends IrisRegistrant { @RegistryListResource(IrisScript.class) @ArrayType(type = String.class, min = 1) private KList dataScripts = new KList<>(); - @Desc("A list of scripts executed on chunk update\nFile extension: .update.kts") - @RegistryListResource(IrisScript.class) - @ArrayType(type = String.class, min = 1) - private KList chunkUpdateScripts = new KList<>(); - @Desc("Use legacy rarity instead of modern one\nWARNING: Changing this may break expressions and image maps") - private boolean legacyRarity = true; + @Desc("A list of scripts executed on chunk update\nFile extension: .update.kts") + @RegistryListResource(IrisScript.class) + @ArrayType(type = String.class, min = 1) + private KList chunkUpdateScripts = new KList<>(); public int getMaxHeight() { return (int) getDimensionHeight().getMax(); diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisElipsoid.java b/core/src/main/java/art/arcane/iris/engine/object/IrisElipsoid.java deleted file mode 100644 index d3ae1e130..000000000 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisElipsoid.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.engine.object; - -import art.arcane.iris.engine.data.cache.AtomicCache; -import art.arcane.iris.engine.framework.Engine; -import art.arcane.iris.engine.mantle.MantleWriter; -import art.arcane.iris.engine.object.annotations.*; -import art.arcane.volmlib.util.math.RNG; -import art.arcane.volmlib.util.matter.MatterCavern; -import art.arcane.volmlib.util.matter.slices.CavernMatter; -import lombok.Data; - -@Snippet("carving-elipsoid") -@Desc("Represents an procedural eliptical shape") -@Data -public class IrisElipsoid implements IRare { - private transient final AtomicCache matterNodeCache = new AtomicCache<>(); - @Required - @Desc("Typically a 1 in RARITY on a per fork basis") - @MinNumber(1) - private int rarity = 1; - @RegistryListResource(IrisBiome.class) - @Desc("Force this cave to only generate the specified custom biome") - private String customBiome = ""; - @Desc("The styled random radius for x") - private IrisStyledRange xRadius = new IrisStyledRange(1, 5, new IrisGeneratorStyle(NoiseStyle.STATIC)); - @Desc("The styled random radius for y") - private IrisStyledRange yRadius = new IrisStyledRange(1, 5, new IrisGeneratorStyle(NoiseStyle.STATIC)); - @Desc("The styled random radius for z") - private IrisStyledRange zRadius = new IrisStyledRange(1, 5, new IrisGeneratorStyle(NoiseStyle.STATIC)); - - @SuppressWarnings("SuspiciousNameCombination") - public void generate(RNG rng, Engine engine, MantleWriter writer, int x, int y, int z) { - writer.setElipsoid(x, y, z, - xRadius.get(rng, z, y, engine.getData()), - yRadius.get(rng, x, z, engine.getData()), - zRadius.get(rng, y, x, engine.getData()), true, matterNodeCache.aquire(() -> CavernMatter.get(getCustomBiome(), 0))); - } - - public double maxSize() { - return Math.max(xRadius.getMax(), Math.max(yRadius.getMax(), zRadius.getMax())); - } -} 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 new file mode 100644 index 000000000..39e7341b6 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisExternalDatapack.java @@ -0,0 +1,32 @@ +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 pack-scoped external datapack source for structure import and optional vanilla replacement") +public class IrisExternalDatapack { + @Desc("Stable id for this external datapack entry") + private String id = ""; + + @Desc("Datapack source URL. Modrinth version page URLs are supported.") + private String url = ""; + + @Desc("Enable or disable this external datapack entry") + private boolean enabled = true; + + @Desc("If true, Iris hard-fails startup when this external datapack cannot be synced/imported/installed") + private boolean required = false; + + @Desc("If true, minecraft namespace worldgen assets may replace vanilla targets listed in replaceTargets") + private boolean replaceVanilla = false; + + @Desc("Explicit replacement targets for minecraft namespace assets") + private IrisExternalDatapackReplaceTargets replaceTargets = new IrisExternalDatapackReplaceTargets(); +} 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 new file mode 100644 index 000000000..35d3ec07f --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisExternalDatapackReplaceTargets.java @@ -0,0 +1,44 @@ +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<>(); + + public boolean hasAnyTargets() { + return !structures.isEmpty() + || !structureSets.isEmpty() + || !templatePools.isEmpty() + || !processorLists.isEmpty() + || !biomeHasStructureTags.isEmpty(); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisPyramid.java b/core/src/main/java/art/arcane/iris/engine/object/IrisPyramid.java deleted file mode 100644 index f3904d8ce..000000000 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisPyramid.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.engine.object; - -import art.arcane.iris.engine.data.cache.AtomicCache; -import art.arcane.iris.engine.framework.Engine; -import art.arcane.iris.engine.mantle.MantleWriter; -import art.arcane.iris.engine.object.annotations.*; -import art.arcane.volmlib.util.math.RNG; -import art.arcane.volmlib.util.matter.MatterCavern; -import art.arcane.volmlib.util.matter.slices.CavernMatter; -import lombok.Data; - -@Snippet("carving-pyramid") -@Desc("Represents an procedural eliptical shape") -@Data -public class IrisPyramid implements IRare { - private transient final AtomicCache matterNodeCache = new AtomicCache<>(); - @Required - @Desc("Typically a 1 in RARITY on a per fork basis") - @MinNumber(1) - private int rarity = 1; - @RegistryListResource(IrisBiome.class) - @Desc("Force this cave to only generate the specified custom biome") - private String customBiome = ""; - @Desc("The styled random radius for x") - private IrisStyledRange baseWidth = new IrisStyledRange(1, 5, new IrisGeneratorStyle(NoiseStyle.STATIC)); - - public void generate(RNG rng, Engine engine, MantleWriter writer, int x, int y, int z) { - writer.setPyramid(x, y, z, matterNodeCache.aquire(() -> CavernMatter.get(getCustomBiome(), 0)), - (int) baseWidth.get(rng, z, y, engine.getData()), true); - } - - public double maxSize() { - return baseWidth.getMax(); - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisRavine.java b/core/src/main/java/art/arcane/iris/engine/object/IrisRavine.java deleted file mode 100644 index 8c2420106..000000000 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisRavine.java +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.engine.object; - -import art.arcane.iris.core.loader.IrisData; -import art.arcane.iris.core.loader.IrisRegistrant; -import art.arcane.iris.engine.framework.Engine; -import art.arcane.iris.engine.mantle.MantleWriter; -import art.arcane.iris.engine.object.annotations.Desc; -import art.arcane.iris.engine.object.annotations.MaxNumber; -import art.arcane.iris.engine.object.annotations.MinNumber; -import art.arcane.iris.engine.object.annotations.RegistryListResource; -import art.arcane.volmlib.util.collection.KList; -import art.arcane.volmlib.util.json.JSONObject; -import art.arcane.volmlib.util.math.RNG; -import art.arcane.volmlib.util.matter.MatterCavern; -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; - -@EqualsAndHashCode(callSuper = true) -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Translate objects") -@Data -public class IrisRavine extends IrisRegistrant { - @Desc("Define the shape of this ravine (2d, ignores Y)") - private IrisWorm worm = new IrisWorm(); - - @RegistryListResource(IrisBiome.class) - @Desc("Force this cave to only generate the specified custom biome") - private String customBiome = ""; - - @Desc("Define potential forking features") - private IrisCarving fork = new IrisCarving(); - - @Desc("The style used to determine the curvature of this worm's y") - private IrisShapedGeneratorStyle depthStyle = new IrisShapedGeneratorStyle(NoiseStyle.PERLIN, 5, 18); - - @Desc("The style used to determine the curvature of this worm's y") - private IrisShapedGeneratorStyle baseWidthStyle = new IrisShapedGeneratorStyle(NoiseStyle.PERLIN, 3, 6); - - @MinNumber(1) - @MaxNumber(100) - @Desc("The angle at which the ravine widens as it gets closer to the surface") - private double angle = 18; - - @MinNumber(1) - @MaxNumber(100) - @Desc("The angle at which the ravine widens as it gets closer to the surface") - private double topAngle = 38; - - @Desc("To fill this cave with lava, set the lava level to a height from the bottom most point of the cave.") - private int lavaLevel = -1; - - @Desc("How many worm nodes must be placed to actually generate a ravine? Higher reduces the chances but also reduces ravine 'holes'") - private int nodeThreshold = 5; - - @MinNumber(1) - @MaxNumber(8) - @Desc("The thickness of the ravine ribs") - private double ribThickness = 3; - - @Override - public String getFolderName() { - return "ravines"; - } - - @Override - public String getTypeName() { - return "Ravine"; - } - - public void generate(MantleWriter writer, RNG rng, Engine engine, int x, int y, int z) { - generate(writer, rng, new RNG(engine.getSeedManager().getCarve()), engine, x, y, z, 0, -1); - } - - public void generate(MantleWriter writer, RNG rng, RNG base, Engine engine, int x, int y, int z, int recursion, int waterHint) { - KList pos = getWorm().generate(base.nextParallelRNG(879615), engine.getData(), writer, null, x, y, z, true, 0); - CNG dg = depthStyle.getGenerator().create(base.nextParallelRNG(7894156), engine.getData()); - CNG bw = baseWidthStyle.getGenerator().create(base.nextParallelRNG(15315456), engine.getData()); - int highestWater = Math.max(waterHint, -1); - boolean water = false; - - if (highestWater == -1) { - for (IrisPosition i : pos) { - int rsurface = y == -1 ? engine.getComplex().getHeightStream().get(x, z).intValue() : y; - int depth = (int) Math.round(dg.fitDouble(depthStyle.getMin(), depthStyle.getMax(), i.getX(), i.getZ())); - int surface = (int) Math.round(rsurface - depth * 0.45); - int yy = surface + depth; - int th = engine.getHeight(x, z, true); - - if (yy > th && th < engine.getDimension().getFluidHeight()) { - highestWater = Math.max(highestWater, yy); - water = true; - break; - } - } - } else { - water = true; - } - - MatterCavern c = new MatterCavern(true, customBiome, (byte) (water ? 1 : 0)); - MatterCavern l = new MatterCavern(true, customBiome, (byte) 2); - - if (pos.size() < nodeThreshold) { - return; - } - - for (IrisPosition p : pos) { - int rsurface = y == -1 ? engine.getComplex().getHeightStream().get(x, z).intValue() : y; - int depth = (int) Math.round(dg.fitDouble(depthStyle.getMin(), depthStyle.getMax(), p.getX(), p.getZ())); - int width = (int) Math.round(bw.fitDouble(baseWidthStyle.getMin(), baseWidthStyle.getMax(), p.getX(), p.getZ())); - int surface = (int) Math.round(rsurface - depth * 0.45); - - fork.doCarving(writer, rng, base, engine, p.getX(), rng.i(surface - depth, surface), p.getZ(), recursion, highestWater); - - for (int i = surface + depth; i >= surface; i--) { - if (i % ribThickness == 0) { - double v = width + ((((surface + depth) - i) * (angle / 360D))); - - if (v <= 0.25) { - break; - } - - if (i <= ribThickness + 2) { - break; - } - - if (lavaLevel >= 0 && i <= lavaLevel + (surface - depthStyle.getMid())) { - writer.setElipsoid(p.getX(), i, p.getZ(), v, ribThickness, v, true, l); - } else { - writer.setElipsoid(p.getX(), i, p.getZ(), v, ribThickness, v, true, c); - } - } - } - - for (int i = surface - depth; i <= surface; i++) { - if (i % ribThickness == 0) { - double v = width - ((((surface - depth) - i) * (angle / 360D))); - - if (v <= 0.25) { - break; - } - - if (i <= ribThickness + 2) { - break; - } - - if (lavaLevel >= 0 && i <= lavaLevel + (surface - depthStyle.getMid())) { - writer.setElipsoid(p.getX(), i, p.getZ(), v, ribThickness, v, true, l); - } else { - writer.setElipsoid(p.getX(), i, p.getZ(), v, ribThickness, v, true, c); - } - } - } - } - } - - @Override - public void scanForErrors(JSONObject p, VolmitSender sender) { - - } - - public int getMaxSize(IrisData data, int depth) { - return getWorm().getMaxDistance() + fork.getMaxRange(data, depth); - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisRavinePlacer.java b/core/src/main/java/art/arcane/iris/engine/object/IrisRavinePlacer.java deleted file mode 100644 index cc562d6b7..000000000 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisRavinePlacer.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.engine.object; - -import art.arcane.iris.Iris; -import art.arcane.iris.core.loader.IrisData; -import art.arcane.iris.engine.data.cache.AtomicCache; -import art.arcane.iris.engine.framework.Engine; -import art.arcane.iris.engine.mantle.MantleWriter; -import art.arcane.iris.engine.object.annotations.*; -import art.arcane.volmlib.util.math.RNG; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -import java.util.concurrent.atomic.AtomicBoolean; - -@Snippet("ravine-placer") -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Desc("Translate objects") -@Data -public class IrisRavinePlacer implements IRare { - private transient final AtomicCache ravineCache = new AtomicCache<>(); - private transient final AtomicBoolean fail = new AtomicBoolean(false); - @Required - @Desc("Typically a 1 in RARITY on a per chunk/fork basis") - @MinNumber(1) - private int rarity = 15; - @MinNumber(1) - @Required - @Desc("The ravine to place") - @RegistryListResource(IrisRavine.class) - private String ravine; - @MinNumber(1) - @MaxNumber(256) - @Desc("The maximum recursion depth") - private int maxRecursion = 100; - - public IrisRavine getRealRavine(IrisData data) { - return ravineCache.aquire(() -> data.getRavineLoader().load(getRavine())); - } - - public void generateRavine(MantleWriter mantle, RNG rng, Engine engine, int x, int y, int z) { - generateRavine(mantle, rng, new RNG(engine.getSeedManager().getCarve()), engine, x, y, z, 0, -1); - } - - public void generateRavine(MantleWriter mantle, RNG rng, RNG base, Engine engine, int x, int y, int z, int recursion, int waterHint) { - if (fail.get()) { - return; - } - - if (rng.nextInt(rarity) != 0) { - return; - } - - IrisData data = engine.getData(); - IrisRavine ravine = getRealRavine(data); - - if (ravine == null) { - Iris.warn("Unable to locate ravine for generation!"); - fail.set(true); - return; - } - - try { - int xx = x + rng.nextInt(15); - int zz = z + rng.nextInt(15); - ravine.generate(mantle, rng, base, engine, xx, y, zz, recursion, waterHint); - } catch (Throwable e) { - e.printStackTrace(); - fail.set(true); - } - } - - public int getSize(IrisData data, int depth) { - return getRealRavine(data).getMaxSize(data, depth); - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisRegion.java b/core/src/main/java/art/arcane/iris/engine/object/IrisRegion.java index 1d942d7ad..a1d52dbdd 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisRegion.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisRegion.java @@ -112,8 +112,6 @@ public class IrisRegion extends IrisRegistrant implements IRare { @MinNumber(0.0001) @Desc("How large cave biomes are in this region") private double caveBiomeZoom = 1; - @Desc("Carving configuration for the dimension") - private IrisCarving carving = new IrisCarving(); @Desc("Profile-driven 3D cave configuration") private IrisCaveProfile caveProfile = new IrisCaveProfile(); @Desc("Configuration of fluid bodies such as rivers & lakes") diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisSphere.java b/core/src/main/java/art/arcane/iris/engine/object/IrisSphere.java deleted file mode 100644 index b917eb86f..000000000 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisSphere.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.engine.object; - -import art.arcane.iris.engine.data.cache.AtomicCache; -import art.arcane.iris.engine.framework.Engine; -import art.arcane.iris.engine.mantle.MantleWriter; -import art.arcane.iris.engine.object.annotations.*; -import art.arcane.volmlib.util.math.RNG; -import art.arcane.volmlib.util.matter.MatterCavern; -import art.arcane.volmlib.util.matter.slices.CavernMatter; -import lombok.Data; - -@Snippet("carving-sphere") -@Desc("Represents an procedural eliptical shape") -@Data -public class IrisSphere implements IRare { - private transient final AtomicCache matterNodeCache = new AtomicCache<>(); - @Required - @Desc("Typically a 1 in RARITY on a per fork basis") - @MinNumber(1) - private int rarity = 1; - @RegistryListResource(IrisBiome.class) - @Desc("Force this cave to only generate the specified custom biome") - private String customBiome = ""; - @Desc("The styled random radius for x") - private IrisStyledRange radius = new IrisStyledRange(1, 5, new IrisGeneratorStyle(NoiseStyle.STATIC)); - - public void generate(RNG rng, Engine engine, MantleWriter writer, int x, int y, int z) { - writer.setSphere(x, y, z, radius.get(rng, z, y, engine.getData()), true, matterNodeCache.aquire(() -> CavernMatter.get(getCustomBiome(), 0))); - } - - public double maxSize() { - return radius.getMax(); - } -} diff --git a/core/src/main/java/art/arcane/iris/util/common/director/handlers/CaveHandler.java b/core/src/main/java/art/arcane/iris/util/common/director/handlers/CaveHandler.java deleted file mode 100644 index 972de2d94..000000000 --- a/core/src/main/java/art/arcane/iris/util/common/director/handlers/CaveHandler.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Iris is a World Generator for Minecraft Bukkit Servers - * Copyright (c) 2022 Arcane Arts (Volmit Software) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package art.arcane.iris.util.common.director.handlers; - -import art.arcane.iris.engine.object.IrisCave; -import art.arcane.iris.util.common.director.specialhandlers.RegistrantHandler; - -public class CaveHandler extends RegistrantHandler { - public CaveHandler() { - super(IrisCave.class, true); - } - - @Override - public String getRandomDefault() { - return "cave"; - } -} diff --git a/core/src/main/java/art/arcane/iris/util/project/noise/NoiseType.java b/core/src/main/java/art/arcane/iris/util/project/noise/NoiseType.java index 242b3bdaa..1d0cc2a78 100644 --- a/core/src/main/java/art/arcane/iris/util/project/noise/NoiseType.java +++ b/core/src/main/java/art/arcane/iris/util/project/noise/NoiseType.java @@ -18,7 +18,6 @@ package art.arcane.iris.util.project.noise; -import art.arcane.iris.core.IrisSettings; import art.arcane.iris.util.project.interpolation.InterpolationMethod; public enum NoiseType { WHITE(WhiteNoise::new), @@ -97,7 +96,6 @@ public enum NoiseType { } public NoiseGenerator create(long seed) { - if (IrisSettings.get().getGenerator().offsetNoiseTypes) return f.create(seed).offset(seed); - else return f.create(seed); + return f.create(seed).offset(seed); } } diff --git a/core/src/main/java/art/arcane/iris/util/project/stream/ProceduralStream.java b/core/src/main/java/art/arcane/iris/util/project/stream/ProceduralStream.java index d3482f5d7..a759686d1 100644 --- a/core/src/main/java/art/arcane/iris/util/project/stream/ProceduralStream.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/ProceduralStream.java @@ -352,16 +352,16 @@ public interface ProceduralStream extends ProceduralLayer, Interpolated { return new SelectionStream(this, rarityTypes); } - default ProceduralStream selectRarity(List types, boolean legacy) { - return IRare.stream(this.forceDouble(), types, legacy); + default ProceduralStream selectRarity(List types) { + return IRare.stream(this.forceDouble(), types); } - default ProceduralStream selectRarity(List types, Function loader, boolean legacy) { + default ProceduralStream selectRarity(List types, Function loader) { List r = new ArrayList<>(); for (V f : types) { r.add(loader.apply(f)); } - return selectRarity(r, legacy); + return selectRarity(r); } default int countPossibilities(List types, Function loader) {