Amending / removing all structures will redesign later im over this

This commit is contained in:
Brian Neumann-Fopiano
2026-04-18 16:14:23 -04:00
parent 88bbce82fe
commit 787c728060
78 changed files with 2833 additions and 9435 deletions
+1 -1
View File
@@ -41,7 +41,7 @@ plugins {
}
group = 'art.arcane'
version = '4.0.0-1.21.11'
version = '4.0.0-26.1'
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate')
.orElse('com.github.VolmitSoftware:VolmLib:master-SNAPSHOT')
.get()
+74 -1
View File
@@ -36,15 +36,18 @@ import art.arcane.iris.core.pack.BrokenPackException;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.core.pack.PackValidationResult;
import art.arcane.iris.core.pack.PackValidator;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.EnginePanic;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisCompat;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
import art.arcane.iris.core.safeguard.IrisSafeguard;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.volmlib.integration.ReloadAware;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.exceptions.IrisException;
@@ -85,12 +88,17 @@ import java.io.*;
import java.lang.annotation.Annotation;
import java.net.URI;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@SuppressWarnings("CanBeFinal")
public class Iris extends VolmitPlugin implements Listener {
public class Iris extends VolmitPlugin implements Listener, ReloadAware {
private static final Queue<Runnable> syncJobs = new ShurikenQueue<>();
public static Iris instance;
@@ -115,6 +123,7 @@ public class Iris extends VolmitPlugin implements Listener {
}
private final KList<Runnable> postShutdown = new KList<>();
private final AtomicBoolean alreadyDrained = new AtomicBoolean(false);
private KMap<Class<? extends IrisService>, IrisService> services;
public static VolmitSender getSender() {
@@ -868,6 +877,9 @@ public class Iris extends VolmitPlugin implements Listener {
public void onDisable() {
if (IrisSafeguard.isForceShutdown()) return;
if (!alreadyDrained.get()) {
drainWorldGenerators("onDisable", 30L);
}
if (services != null) {
services.values().forEach(IrisService::onDisable);
}
@@ -883,6 +895,67 @@ public class Iris extends VolmitPlugin implements Listener {
J.attempt(new JarScanner(instance.getJarFile(), "", false)::scanAll);
}
@Override
public void onPreUnload(ReloadAware.PreUnloadReason reason) {
if (!alreadyDrained.compareAndSet(false, true)) {
Iris.info("Pre-unload hook skipped; Iris already drained.");
return;
}
Iris.info("BileTools pre-unload hook fired (" + reason + "). Freezing all Iris worlds.");
drainWorldGenerators("pre-unload:" + reason, 45L);
}
private void drainWorldGenerators(String reason, long timeoutSeconds) {
List<World> irisWorlds = new ArrayList<>();
for (World world : Bukkit.getWorlds()) {
if (IrisToolbelt.access(world) != null) {
irisWorlds.add(world);
}
}
if (irisWorlds.isEmpty()) {
Iris.info("No Iris worlds to freeze.");
return;
}
for (World world : irisWorlds) {
IrisToolbelt.beginWorldMaintenance(world, reason, true);
}
J.attempt(PregeneratorJob::shutdownInstance);
List<CompletableFuture<Void>> closes = new ArrayList<>();
for (World world : irisWorlds) {
PlatformChunkGenerator gen = IrisToolbelt.access(world);
if (gen == null) continue;
Engine engine = gen.getEngine();
if (engine != null) {
J.attempt(() -> engine.getMantle().saveAllNow());
}
try {
closes.add(gen.closeAsync());
} catch (Throwable t) {
Iris.reportError(t);
}
}
if (closes.isEmpty()) return;
try {
CompletableFuture.allOf(closes.toArray(new CompletableFuture<?>[0]))
.get(timeoutSeconds, TimeUnit.SECONDS);
Iris.info("All Iris chunk generators parked. Safe to unload.");
} catch (TimeoutException e) {
Iris.warn("Iris generator drain timed out after " + timeoutSeconds + "s; unload proceeding anyway.");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
Iris.warn("Iris generator drain interrupted; unload proceeding.");
} catch (ExecutionException e) {
Iris.reportError(e.getCause() == null ? e : e.getCause());
}
}
private void setupPapi() {
if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) {
new IrisPapiExpansion().register();
File diff suppressed because it is too large Load Diff
@@ -220,24 +220,6 @@ public class IrisSettings {
public boolean useConsoleCustomColors = true;
public boolean useCustomColorsIngame = true;
public boolean adjustVanillaHeight = false;
public boolean importExternalDatapacks = true;
public boolean autoGenerateIntrinsicStructures = true;
public boolean intrinsicStructureFoundations = true;
public int intrinsicFoundationMaxDepth = 96;
public java.util.List<String> intrinsicStructureAllowlist = new java.util.ArrayList<>(java.util.List.of(
"minecraft:village_plains",
"minecraft:village_desert",
"minecraft:village_savanna",
"minecraft:village_snowy",
"minecraft:village_taiga",
"minecraft:pillager_outpost",
"minecraft:desert_pyramid",
"minecraft:jungle_temple",
"minecraft:swamp_hut",
"minecraft:igloo",
"minecraft:mansion",
"minecraft:ruined_portal*"
));
public String forceMainWorld = "";
public int spinh = -20;
public int spins = 7;
@@ -20,13 +20,13 @@ package art.arcane.iris.core;
import art.arcane.iris.Iris;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.nms.datapack.DataVersion;
import art.arcane.iris.core.nms.datapack.IDataFixer;
import art.arcane.iris.engine.object.*;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.collection.KSet;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.misc.ServerProperties;
@@ -34,8 +34,6 @@ import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import lombok.NonNull;
import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey;
import org.bukkit.block.Biome;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
@@ -44,15 +42,8 @@ import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicIntegerArray;
import java.util.stream.Stream;
@@ -105,6 +96,7 @@ public class ServerConfigurator {
f.save(spigotConfig);
}
}
private static void increasePaperWatchdog() throws IOException, InvalidConfigurationException {
File spigotConfig = new File("config/paper-global.yml");
FileConfiguration f = new YamlConfiguration();
@@ -138,64 +130,28 @@ public class ServerConfigurator {
}
public static boolean installDataPacks(boolean fullInstall) {
return installDataPacks(fullInstall, true);
}
public static boolean installDataPacks(boolean fullInstall, boolean includeExternal) {
return installDataPacks(fullInstall, includeExternal, null);
}
public static boolean installDataPacks(
boolean fullInstall,
boolean includeExternal,
KMap<String, KList<File>> extraWorldDatapackFoldersByPack
) {
IDataFixer fixer = DataVersion.getDefault();
if (fixer == null) {
DataVersion fallback = DataVersion.getLatest();
Iris.warn("Primary datapack fixer was null, forcing latest fixer: " + fallback.getVersion());
fixer = fallback.get();
}
return installDataPacks(fixer, fullInstall, includeExternal, extraWorldDatapackFoldersByPack);
return installDataPacks(fixer, fullInstall);
}
public static boolean installDataPacks(IDataFixer fixer, boolean fullInstall) {
return installDataPacks(fixer, fullInstall, true);
}
public static boolean installDataPacks(IDataFixer fixer, boolean fullInstall, boolean includeExternal) {
return installDataPacks(fixer, fullInstall, includeExternal, null);
}
public static boolean installDataPacks(
IDataFixer fixer,
boolean fullInstall,
boolean includeExternal,
KMap<String, KList<File>> extraWorldDatapackFoldersByPack
) {
if (fixer == null) {
Iris.error("Unable to install datapacks, fixer is null!");
return false;
}
if (fullInstall || includeExternal) {
if (fullInstall) {
Iris.info("Checking Data Packs...");
} else {
Iris.verbose("Checking Data Packs...");
}
DimensionHeight height = new DimensionHeight(fixer);
KList<File> baseFolders = getDatapacksFolder();
KList<File> folders = collectInstallDatapackFolders(baseFolders, extraWorldDatapackFoldersByPack);
if (fullInstall) {
if (anyDimensionHasVanillaStructures()) {
VanillaDatapackDumper.dumpIfNeeded(baseFolders);
} else {
VanillaDatapackDumper.removeIfPresent(baseFolders);
}
}
if (includeExternal) {
installExternalDataPacks(baseFolders, extraWorldDatapackFoldersByPack);
}
KMap<String, KSet<String>> biomes = new KMap<>();
KList<File> folders = getDatapacksFolder();
java.util.concurrent.ConcurrentMap<String, KSet<String>> biomes = new java.util.concurrent.ConcurrentHashMap<>();
try (Stream<IrisData> stream = allPacks()) {
stream.flatMap(height::merge)
@@ -207,7 +163,7 @@ public class ServerConfigurator {
});
}
IrisDimension.writeShared(folders, height);
if (fullInstall || includeExternal) {
if (fullInstall) {
Iris.info("Data Packs Setup!");
} else {
Iris.verbose("Data Packs Setup!");
@@ -216,83 +172,6 @@ public class ServerConfigurator {
return fullInstall && verifyDataPacksPost(IrisSettings.get().getAutoConfiguration().isAutoRestartOnCustomBiomeInstall());
}
static KList<File> collectInstallDatapackFolders(
KList<File> baseFolders,
KMap<String, KList<File>> extraWorldDatapackFoldersByPack
) {
KList<File> folders = new KList<>();
if (baseFolders != null) {
for (File folder : baseFolders) {
if (folder != null && !folders.contains(folder)) {
folders.add(folder);
}
}
}
if (extraWorldDatapackFoldersByPack == null || extraWorldDatapackFoldersByPack.isEmpty()) {
return folders;
}
for (KList<File> extraFolders : extraWorldDatapackFoldersByPack.values()) {
if (extraFolders == null || extraFolders.isEmpty()) {
continue;
}
for (File folder : extraFolders) {
if (folder != null && !folders.contains(folder)) {
folders.add(folder);
}
}
}
return folders;
}
private static void installExternalDataPacks(
KList<File> folders,
KMap<String, KList<File>> extraWorldDatapackFoldersByPack
) {
if (!IrisSettings.get().getGeneral().isImportExternalDatapacks()) {
return;
}
KList<ExternalDataPackPipeline.DatapackRequest> requests = collectExternalDatapackRequests();
KMap<String, KList<File>> worldDatapackFoldersByPack = collectWorldDatapackFoldersByPack(folders, extraWorldDatapackFoldersByPack);
ExternalDataPackPipeline.PipelineSummary summary = ExternalDataPackPipeline.processDatapacks(requests, worldDatapackFoldersByPack);
if (summary.getLegacyDownloadRemovals() > 0) {
Iris.verbose("Removed " + summary.getLegacyDownloadRemovals() + " legacy global datapack downloads.");
}
if (summary.getLegacyWorldCopyRemovals() > 0) {
Iris.verbose("Removed " + summary.getLegacyWorldCopyRemovals() + " legacy managed world datapack copies.");
}
if (summary.getSkippedExistingRequests() > 0) {
Iris.verbose("Reused " + summary.getSkippedExistingRequests() + " already-installed external datapack(s) (no download/projection).");
}
int loadedDatapackCount = Math.max(0, summary.getRequests() - summary.getOptionalFailures() - summary.getRequiredFailures());
Iris.info("Loaded Datapacks into Iris: " + loadedDatapackCount + "!");
if (summary.getRequiredFailures() > 0) {
throw new IllegalStateException("Required external datapack setup failed for " + summary.getRequiredFailures() + " request(s).");
}
}
private static boolean anyDimensionHasVanillaStructures() {
try (Stream<IrisData> stream = allPacks()) {
return stream.anyMatch(data -> {
ResourceLoader<IrisDimension> loader = data.getDimensionLoader();
if (loader == null) {
return false;
}
String[] keys = loader.getPossibleKeys();
if (keys == null || keys.length == 0) {
return false;
}
for (String key : keys) {
IrisDimension dim = loader.load(key);
if (dim != null && dim.isVanillaStructures()) {
return true;
}
}
return false;
});
}
}
private static boolean shouldDeferInstallUntilWorldsReady() {
String forcedMainWorld = IrisSettings.get().getGeneral().forceMainWorld;
if (forcedMainWorld != null && !forcedMainWorld.isBlank()) {
@@ -302,722 +181,6 @@ public class ServerConfigurator {
return Bukkit.getServer().getWorlds().isEmpty();
}
private static KList<ExternalDataPackPipeline.DatapackRequest> collectExternalDatapackRequests() {
KMap<String, ExternalDataPackPipeline.DatapackRequest> deduplicated = new KMap<>();
try (Stream<IrisData> stream = allPacks()) {
stream.forEach(data -> collectExternalDatapackRequestsForPack(data, deduplicated));
}
return new KList<>(deduplicated.v());
}
private static void collectExternalDatapackRequestsForPack(IrisData data, KMap<String, ExternalDataPackPipeline.DatapackRequest> deduplicated) {
ResourceLoader<IrisDimension> loader = data.getDimensionLoader();
if (loader == null) {
Iris.warn("Skipping external datapack request discovery for pack " + data.getDataFolder().getName() + " because dimension loader is unavailable.");
return;
}
String[] possibleKeys = loader.getPossibleKeys();
if (possibleKeys == null || possibleKeys.length == 0) {
File dimensionsFolder = new File(data.getDataFolder(), "dimensions");
File[] dimensionFiles = dimensionsFolder.listFiles((dir, name) -> name != null && name.toLowerCase().endsWith(".json"));
int dimensionFileCount = dimensionFiles == null ? 0 : dimensionFiles.length;
Iris.warn("Pack " + data.getDataFolder().getName() + " has no loadable dimension keys. Dimension folder json files=" + dimensionFileCount + ". External datapacks in this pack cannot be discovered.");
return;
}
KList<IrisDimension> dimensions = loader.loadAll(possibleKeys);
int scannedDimensions = 0;
int dimensionsWithExternalEntries = 0;
int enabledEntries = 0;
int disabledEntries = 0;
int skippedBlankUrl = 0;
int scopedRequests = 0;
int unscopedRequests = 0;
int dedupeMerges = 0;
for (IrisDimension dimension : dimensions) {
if (dimension == null) {
continue;
}
scannedDimensions++;
KList<IrisExternalDatapack> externalDatapacks = dimension.getExternalDatapacks();
if (externalDatapacks == null || externalDatapacks.isEmpty()) {
continue;
}
dimensionsWithExternalEntries++;
String targetPack = sanitizePackName(dimension.getLoadKey());
if (targetPack.isBlank()) {
targetPack = sanitizePackName(data.getDataFolder().getName());
}
String environment = ExternalDataPackPipeline.normalizeEnvironmentValue(dimension.getEnvironment() == null ? null : dimension.getEnvironment().name());
LinkedHashMap<String, IrisExternalDatapack> definitionsById = new LinkedHashMap<>();
for (IrisExternalDatapack externalDatapack : externalDatapacks) {
if (externalDatapack == null) {
disabledEntries++;
continue;
}
if (!externalDatapack.isEnabled()) {
disabledEntries++;
continue;
}
String url = externalDatapack.getUrl() == null ? "" : externalDatapack.getUrl().trim();
if (url.isBlank()) {
skippedBlankUrl++;
continue;
}
enabledEntries++;
String requestId = normalizeExternalDatapackId(externalDatapack.getId(), url);
IrisExternalDatapack existingDefinition = definitionsById.put(requestId, externalDatapack);
if (existingDefinition != null) {
Iris.warn("Duplicate external datapack id '" + requestId + "' in dimension " + dimension.getLoadKey() + ". Latest entry wins.");
}
}
if (definitionsById.isEmpty()) {
continue;
}
KMap<String, KList<ScopedBindingGroup>> scopedGroups = resolveScopedBindingGroups(data, dimension, definitionsById);
for (Map.Entry<String, IrisExternalDatapack> entry : definitionsById.entrySet()) {
String requestId = entry.getKey();
IrisExternalDatapack definition = entry.getValue();
String url = definition.getUrl() == null ? "" : definition.getUrl().trim();
if (url.isBlank()) {
continue;
}
KList<ScopedBindingGroup> groups = scopedGroups.get(requestId);
if (groups == null || groups.isEmpty()) {
String scopeKey = buildRootScopeKey(dimension.getLoadKey(), requestId);
ExternalDataPackPipeline.DatapackRequest request = new ExternalDataPackPipeline.DatapackRequest(
requestId,
url,
targetPack,
environment,
definition.isRequired(),
definition.isReplace(),
Set.of(),
scopeKey
);
dedupeMerges += mergeDeduplicatedRequest(deduplicated, request);
unscopedRequests++;
Iris.verbose("External datapack scope resolved: id=" + requestId
+ ", targetPack=" + targetPack
+ ", dimension=" + dimension.getLoadKey()
+ ", scope=dimension-root"
+ ", forcedBiomes=0"
+ ", replace=" + definition.isReplace()
+ ", required=" + definition.isRequired());
continue;
}
for (ScopedBindingGroup group : groups) {
ExternalDataPackPipeline.DatapackRequest request = new ExternalDataPackPipeline.DatapackRequest(
requestId,
url,
targetPack,
environment,
group.required(),
group.replaceVanilla(),
group.forcedBiomeKeys(),
group.scopeKey()
);
dedupeMerges += mergeDeduplicatedRequest(deduplicated, request);
scopedRequests++;
Iris.verbose("External datapack scope resolved: id=" + requestId
+ ", targetPack=" + targetPack
+ ", dimension=" + dimension.getLoadKey()
+ ", scope=" + group.source()
+ ", forcedBiomes=" + group.forcedBiomeKeys().size()
+ ", replace=" + group.replaceVanilla()
+ ", required=" + group.required());
}
}
}
if (scannedDimensions == 0) {
Iris.warn("Pack " + data.getDataFolder().getName() + " did not resolve any dimensions during external datapack discovery.");
return;
}
if (dimensionsWithExternalEntries > 0 || enabledEntries > 0 || disabledEntries > 0 || skippedBlankUrl > 0) {
Iris.verbose("External datapack discovery for pack " + data.getDataFolder().getName()
+ ": dimensions=" + scannedDimensions
+ ", withEntries=" + dimensionsWithExternalEntries
+ ", enabled=" + enabledEntries
+ ", disabled=" + disabledEntries
+ ", skippedBlankUrl=" + skippedBlankUrl
+ ", scopedRequests=" + scopedRequests
+ ", unscopedRequests=" + unscopedRequests
+ ", dedupeMerges=" + dedupeMerges);
}
}
private static KMap<String, KList<ScopedBindingGroup>> resolveScopedBindingGroups(
IrisData data,
IrisDimension dimension,
Map<String, IrisExternalDatapack> definitionsById
) {
KMap<String, KList<ScopedBindingGroup>> groupedRequestsById = new KMap<>();
if (definitionsById == null || definitionsById.isEmpty()) {
return groupedRequestsById;
}
ResourceLoader<IrisRegion> regionLoader = data.getRegionLoader();
ResourceLoader<IrisBiome> biomeLoader = data.getBiomeLoader();
if (regionLoader == null || biomeLoader == null) {
return groupedRequestsById;
}
String biomeNamespace = resolveBiomeNamespace(dimension);
LinkedHashMap<String, IrisBiome> biomeCache = new LinkedHashMap<>();
LinkedHashMap<String, IrisRegion> regions = new LinkedHashMap<>();
KList<String> dimensionRegions = dimension.getRegions();
if (dimensionRegions != null) {
for (String regionKey : dimensionRegions) {
String normalizedRegion = normalizeResourceReference(regionKey);
if (normalizedRegion.isBlank()) {
continue;
}
IrisRegion region = regionLoader.load(normalizedRegion, false);
if (region != null) {
regions.put(normalizedRegion, region);
}
}
}
LinkedHashMap<String, KList<ScopedBindingCandidate>> candidatesById = new LinkedHashMap<>();
LinkedHashSet<String> discoveryBiomeKeys = new LinkedHashSet<>();
for (IrisRegion region : regions.values()) {
Set<String> expandedRegionBiomes = collectRegionBiomeKeys(region, true, biomeLoader, biomeCache);
discoveryBiomeKeys.addAll(expandedRegionBiomes);
KList<IrisExternalDatapackBinding> bindings = region.getExternalDatapacks();
if (bindings == null || bindings.isEmpty()) {
continue;
}
for (IrisExternalDatapackBinding binding : bindings) {
if (binding == null || !binding.isEnabled()) {
continue;
}
String id = normalizeExternalDatapackId(binding.getId(), "");
if (id.isBlank()) {
continue;
}
IrisExternalDatapack definition = definitionsById.get(id);
if (definition == null) {
Iris.warn("Ignoring region external datapack binding id '" + id + "' in " + region.getLoadKey() + " because no matching dimension externalDatapacks entry exists.");
continue;
}
boolean replaceVanilla = binding.getReplaceOverride() == null
? definition.isReplace()
: binding.getReplaceOverride();
boolean required = binding.getRequiredOverride() == null
? definition.isRequired()
: binding.getRequiredOverride();
Set<String> regionBiomeKeys = collectRegionBiomeKeys(region, binding.isIncludeChildren(), biomeLoader, biomeCache);
Set<String> runtimeBiomeKeys = resolveRuntimeBiomeKeys(regionBiomeKeys, biomeNamespace, biomeLoader, biomeCache);
if (runtimeBiomeKeys.isEmpty()) {
continue;
}
KList<ScopedBindingCandidate> candidates = candidatesById.computeIfAbsent(id, key -> new KList<>());
candidates.add(new ScopedBindingCandidate("region", region.getLoadKey(), 1, replaceVanilla, required, runtimeBiomeKeys));
}
}
for (String biomeKey : discoveryBiomeKeys) {
IrisBiome biome = loadBiomeFromCache(biomeKey, biomeLoader, biomeCache);
if (biome == null) {
continue;
}
KList<IrisExternalDatapackBinding> bindings = biome.getExternalDatapacks();
if (bindings == null || bindings.isEmpty()) {
continue;
}
for (IrisExternalDatapackBinding binding : bindings) {
if (binding == null || !binding.isEnabled()) {
continue;
}
String id = normalizeExternalDatapackId(binding.getId(), "");
if (id.isBlank()) {
continue;
}
IrisExternalDatapack definition = definitionsById.get(id);
if (definition == null) {
Iris.warn("Ignoring biome external datapack binding id '" + id + "' in " + biome.getLoadKey() + " because no matching dimension externalDatapacks entry exists.");
continue;
}
boolean replaceVanilla = binding.getReplaceOverride() == null
? definition.isReplace()
: binding.getReplaceOverride();
boolean required = binding.getRequiredOverride() == null
? definition.isRequired()
: binding.getRequiredOverride();
Set<String> biomeSelection = collectBiomeKeys(biome.getLoadKey(), binding.isIncludeChildren(), biomeLoader, biomeCache);
Set<String> runtimeBiomeKeys = resolveRuntimeBiomeKeys(biomeSelection, biomeNamespace, biomeLoader, biomeCache);
if (runtimeBiomeKeys.isEmpty()) {
continue;
}
KList<ScopedBindingCandidate> candidates = candidatesById.computeIfAbsent(id, key -> new KList<>());
candidates.add(new ScopedBindingCandidate("biome", biome.getLoadKey(), 2, replaceVanilla, required, runtimeBiomeKeys));
}
}
for (Map.Entry<String, KList<ScopedBindingCandidate>> entry : candidatesById.entrySet()) {
String id = entry.getKey();
KList<ScopedBindingCandidate> candidates = entry.getValue();
if (candidates == null || candidates.isEmpty()) {
continue;
}
LinkedHashMap<String, ScopedBindingSelection> selectedByBiome = new LinkedHashMap<>();
for (ScopedBindingCandidate candidate : candidates) {
if (candidate == null || candidate.forcedBiomeKeys() == null || candidate.forcedBiomeKeys().isEmpty()) {
continue;
}
ArrayList<String> sortedBiomeKeys = new ArrayList<>(candidate.forcedBiomeKeys());
sortedBiomeKeys.sort(String::compareTo);
for (String runtimeBiomeKey : sortedBiomeKeys) {
ScopedBindingSelection selected = selectedByBiome.get(runtimeBiomeKey);
if (selected == null) {
selectedByBiome.put(runtimeBiomeKey, new ScopedBindingSelection(
candidate.priority(),
candidate.replaceVanilla(),
candidate.required(),
candidate.sourceType(),
candidate.sourceKey()
));
continue;
}
if (candidate.priority() > selected.priority()) {
selectedByBiome.put(runtimeBiomeKey, new ScopedBindingSelection(
candidate.priority(),
candidate.replaceVanilla(),
candidate.required(),
candidate.sourceType(),
candidate.sourceKey()
));
continue;
}
if (candidate.priority() == selected.priority()
&& (candidate.replaceVanilla() != selected.replaceVanilla() || candidate.required() != selected.required())) {
Iris.warn("External datapack scope conflict for id=" + id
+ ", biomeKey=" + runtimeBiomeKey
+ ", kept=" + selected.sourceType() + "/" + selected.sourceKey()
+ ", ignored=" + candidate.sourceType() + "/" + candidate.sourceKey());
}
}
}
LinkedHashMap<String, LinkedHashSet<String>> groupedBiomes = new LinkedHashMap<>();
LinkedHashMap<String, ScopedBindingSelection> groupedSelection = new LinkedHashMap<>();
for (Map.Entry<String, ScopedBindingSelection> selectedEntry : selectedByBiome.entrySet()) {
String runtimeBiomeKey = selectedEntry.getKey();
ScopedBindingSelection selection = selectedEntry.getValue();
String groupKey = selection.replaceVanilla() + "|" + selection.required();
groupedBiomes.computeIfAbsent(groupKey, key -> new LinkedHashSet<>()).add(runtimeBiomeKey);
groupedSelection.putIfAbsent(groupKey, selection);
}
for (Map.Entry<String, LinkedHashSet<String>> groupedEntry : groupedBiomes.entrySet()) {
LinkedHashSet<String> runtimeBiomeKeys = groupedEntry.getValue();
if (runtimeBiomeKeys == null || runtimeBiomeKeys.isEmpty()) {
continue;
}
ScopedBindingSelection selection = groupedSelection.get(groupedEntry.getKey());
if (selection == null) {
continue;
}
Set<String> forcedBiomeKeys = Set.copyOf(runtimeBiomeKeys);
String scopeKey = buildScopedScopeKey(dimension.getLoadKey(), id, selection.sourceType(), selection.sourceKey(), forcedBiomeKeys);
String source = selection.sourceType() + ":" + selection.sourceKey();
KList<ScopedBindingGroup> groups = groupedRequestsById.computeIfAbsent(id, key -> new KList<>());
groups.add(new ScopedBindingGroup(selection.replaceVanilla(), selection.required(), forcedBiomeKeys, scopeKey, source));
}
}
return groupedRequestsById;
}
private static Set<String> collectRegionBiomeKeys(
IrisRegion region,
boolean includeChildren,
ResourceLoader<IrisBiome> biomeLoader,
Map<String, IrisBiome> biomeCache
) {
LinkedHashSet<String> regionBiomeKeys = new LinkedHashSet<>();
if (region == null) {
return regionBiomeKeys;
}
addAllResourceReferences(regionBiomeKeys, region.getLandBiomes());
addAllResourceReferences(regionBiomeKeys, region.getSeaBiomes());
addAllResourceReferences(regionBiomeKeys, region.getShoreBiomes());
addAllResourceReferences(regionBiomeKeys, region.getCaveBiomes());
if (!includeChildren) {
return regionBiomeKeys;
}
LinkedHashSet<String> expanded = new LinkedHashSet<>();
for (String biomeKey : regionBiomeKeys) {
expanded.addAll(collectBiomeKeys(biomeKey, true, biomeLoader, biomeCache));
}
return expanded;
}
private static Set<String> collectBiomeKeys(
String biomeKey,
boolean includeChildren,
ResourceLoader<IrisBiome> biomeLoader,
Map<String, IrisBiome> biomeCache
) {
LinkedHashSet<String> resolved = new LinkedHashSet<>();
String normalizedBiomeKey = normalizeResourceReference(biomeKey);
if (normalizedBiomeKey.isBlank()) {
return resolved;
}
if (!includeChildren) {
resolved.add(normalizedBiomeKey);
return resolved;
}
ArrayDeque<String> queue = new ArrayDeque<>();
queue.add(normalizedBiomeKey);
while (!queue.isEmpty()) {
String next = normalizeResourceReference(queue.removeFirst());
if (next.isBlank() || !resolved.add(next)) {
continue;
}
IrisBiome biome = loadBiomeFromCache(next, biomeLoader, biomeCache);
if (biome == null) {
continue;
}
addQueueResourceReferences(queue, biome.getChildren());
}
return resolved;
}
private static Set<String> resolveRuntimeBiomeKeys(
Set<String> irisBiomeKeys,
String biomeNamespace,
ResourceLoader<IrisBiome> biomeLoader,
Map<String, IrisBiome> biomeCache
) {
LinkedHashSet<String> resolved = new LinkedHashSet<>();
if (irisBiomeKeys == null || irisBiomeKeys.isEmpty()) {
return resolved;
}
for (String irisBiomeKey : irisBiomeKeys) {
String normalizedBiomeKey = normalizeResourceReference(irisBiomeKey);
if (normalizedBiomeKey.isBlank()) {
continue;
}
IrisBiome biome = loadBiomeFromCache(normalizedBiomeKey, biomeLoader, biomeCache);
if (biome == null) {
continue;
}
if (biome.isCustom() && biome.getCustomDerivitives() != null && !biome.getCustomDerivitives().isEmpty()) {
for (IrisBiomeCustom customDerivative : biome.getCustomDerivitives()) {
if (customDerivative == null) {
continue;
}
String customId = normalizeResourceReference(customDerivative.getId());
if (customId.isBlank()) {
continue;
}
resolved.add((biomeNamespace + ":" + customId).toLowerCase(Locale.ROOT));
}
continue;
}
Biome vanillaDerivative = biome.getVanillaDerivative();
NamespacedKey vanillaKey = vanillaDerivative == null ? null : vanillaDerivative.getKey();
if (vanillaKey != null) {
resolved.add(vanillaKey.toString().toLowerCase(Locale.ROOT));
}
}
return resolved;
}
private static String resolveBiomeNamespace(IrisDimension dimension) {
if (dimension == null) {
return "iris";
}
String namespace = dimension.getLoadKey() == null ? "" : dimension.getLoadKey().trim().toLowerCase(Locale.ROOT);
namespace = namespace.replaceAll("[^a-z0-9_\\-.]", "_");
namespace = namespace.replaceAll("_+", "_");
namespace = namespace.replaceAll("^_+", "");
namespace = namespace.replaceAll("_+$", "");
if (namespace.isBlank()) {
return "iris";
}
return namespace;
}
private static IrisBiome loadBiomeFromCache(
String biomeKey,
ResourceLoader<IrisBiome> biomeLoader,
Map<String, IrisBiome> biomeCache
) {
if (biomeLoader == null) {
return null;
}
String normalizedBiomeKey = normalizeResourceReference(biomeKey);
if (normalizedBiomeKey.isBlank()) {
return null;
}
if (biomeCache.containsKey(normalizedBiomeKey)) {
return biomeCache.get(normalizedBiomeKey);
}
IrisBiome biome = biomeLoader.load(normalizedBiomeKey, false);
if (biome != null) {
biomeCache.put(normalizedBiomeKey, biome);
}
return biome;
}
private static void addAllResourceReferences(Set<String> destination, KList<String> references) {
if (destination == null || references == null || references.isEmpty()) {
return;
}
for (String reference : references) {
String normalized = normalizeResourceReference(reference);
if (!normalized.isBlank()) {
destination.add(normalized);
}
}
}
private static void addQueueResourceReferences(ArrayDeque<String> queue, KList<String> references) {
if (queue == null || references == null || references.isEmpty()) {
return;
}
for (String reference : references) {
String normalized = normalizeResourceReference(reference);
if (!normalized.isBlank()) {
queue.addLast(normalized);
}
}
}
private static String normalizeResourceReference(String reference) {
if (reference == null) {
return "";
}
String normalized = reference.trim().replace('\\', '/');
normalized = normalized.replaceAll("/+", "/");
normalized = normalized.replaceAll("^/+", "");
normalized = normalized.replaceAll("/+$", "");
return normalized;
}
private static int mergeDeduplicatedRequest(
KMap<String, ExternalDataPackPipeline.DatapackRequest> deduplicated,
ExternalDataPackPipeline.DatapackRequest request
) {
if (request == null) {
return 0;
}
String dedupeKey = request.getDedupeKey();
ExternalDataPackPipeline.DatapackRequest existing = deduplicated.get(dedupeKey);
if (existing == null) {
deduplicated.put(dedupeKey, request);
return 0;
}
deduplicated.put(dedupeKey, existing.merge(request));
return 1;
}
private static String normalizeExternalDatapackId(String id, String fallbackUrl) {
String normalized = id == null ? "" : id.trim();
if (!normalized.isBlank()) {
return normalized.toLowerCase(Locale.ROOT);
}
String fallback = fallbackUrl == null ? "" : fallbackUrl.trim();
if (fallback.isBlank()) {
return "";
}
return fallback.toLowerCase(Locale.ROOT);
}
private static String buildRootScopeKey(String dimensionKey, String id) {
String normalizedDimension = ExternalDataPackPipeline.sanitizePackNameValue(dimensionKey);
if (normalizedDimension.isBlank()) {
normalizedDimension = "dimension";
}
String normalizedId = ExternalDataPackPipeline.sanitizePackNameValue(id);
if (normalizedId.isBlank()) {
normalizedId = "external";
}
return "root-" + normalizedDimension + "-" + normalizedId;
}
private static String buildScopedScopeKey(String dimensionKey, String id, String sourceType, String sourceKey, Set<String> forcedBiomeKeys) {
ArrayList<String> sortedBiomes = new ArrayList<>();
if (forcedBiomeKeys != null) {
sortedBiomes.addAll(forcedBiomeKeys);
}
sortedBiomes.sort(String::compareTo);
String biomeFingerprint = Integer.toHexString(String.join(",", sortedBiomes).hashCode());
String normalizedDimension = ExternalDataPackPipeline.sanitizePackNameValue(dimensionKey);
if (normalizedDimension.isBlank()) {
normalizedDimension = "dimension";
}
String normalizedId = ExternalDataPackPipeline.sanitizePackNameValue(id);
if (normalizedId.isBlank()) {
normalizedId = "external";
}
String normalizedSourceType = ExternalDataPackPipeline.sanitizePackNameValue(sourceType);
if (normalizedSourceType.isBlank()) {
normalizedSourceType = "scope";
}
String normalizedSourceKey = ExternalDataPackPipeline.sanitizePackNameValue(sourceKey);
if (normalizedSourceKey.isBlank()) {
normalizedSourceKey = "entry";
}
return normalizedDimension + "-" + normalizedId + "-" + normalizedSourceType + "-" + normalizedSourceKey + "-" + biomeFingerprint;
}
private record ScopedBindingCandidate(
String sourceType,
String sourceKey,
int priority,
boolean replaceVanilla,
boolean required,
Set<String> forcedBiomeKeys
) {
}
private record ScopedBindingSelection(
int priority,
boolean replaceVanilla,
boolean required,
String sourceType,
String sourceKey
) {
}
private record ScopedBindingGroup(
boolean replaceVanilla,
boolean required,
Set<String> forcedBiomeKeys,
String scopeKey,
String source
) {
}
private static KMap<String, KList<File>> collectWorldDatapackFoldersByPack(
KList<File> fallbackFolders,
KMap<String, KList<File>> extraWorldDatapackFoldersByPack
) {
KMap<String, KList<File>> foldersByPack = new KMap<>();
KMap<String, String> mappedWorlds = IrisWorlds.get().getWorlds();
for (String worldName : mappedWorlds.k()) {
String packName = sanitizePackName(mappedWorlds.get(worldName));
if (packName.isBlank()) {
continue;
}
org.bukkit.World world = Bukkit.getWorld(worldName);
File datapacksFolder = world == null
? new File(Bukkit.getWorldContainer(), worldName + File.separator + "datapacks")
: resolveDatapacksFolder(world.getWorldFolder());
addWorldDatapackFolder(foldersByPack, packName, datapacksFolder);
}
for (org.bukkit.World world : Bukkit.getWorlds()) {
String worldName = world.getName();
String mappedPack = mappedWorlds.get(worldName);
String packName = sanitizePackName(mappedPack);
if (packName.isBlank()) {
packName = sanitizePackName(IrisSettings.get().getGenerator().getDefaultWorldType());
}
if (packName.isBlank()) {
continue;
}
File datapacksFolder = resolveDatapacksFolder(world.getWorldFolder());
addWorldDatapackFolder(foldersByPack, packName, datapacksFolder);
}
String defaultPack = sanitizePackName(IrisSettings.get().getGenerator().getDefaultWorldType());
if (!defaultPack.isBlank()) {
for (File folder : fallbackFolders) {
addWorldDatapackFolder(foldersByPack, defaultPack, folder);
}
}
if (extraWorldDatapackFoldersByPack != null && !extraWorldDatapackFoldersByPack.isEmpty()) {
for (Map.Entry<String, KList<File>> entry : extraWorldDatapackFoldersByPack.entrySet()) {
String packName = sanitizePackName(entry.getKey());
if (packName.isBlank()) {
continue;
}
KList<File> folders = entry.getValue();
if (folders == null || folders.isEmpty()) {
continue;
}
for (File folder : folders) {
addWorldDatapackFolder(foldersByPack, packName, folder);
}
}
}
return foldersByPack;
}
private static void addWorldDatapackFolder(KMap<String, KList<File>> foldersByPack, String packName, File folder) {
if (folder == null || packName == null || packName.isBlank()) {
return;
}
KList<File> folders = foldersByPack.computeIfAbsent(packName, k -> new KList<>());
if (!folders.contains(folder)) {
folders.add(folder);
}
}
public static File resolveDatapacksFolder(File worldFolder) {
File rootFolder = resolveWorldRootFolder(worldFolder);
return new File(rootFolder, "datapacks");
@@ -1043,21 +206,6 @@ public class ServerConfigurator {
return worldFolder.getAbsoluteFile();
}
private static String sanitizePackName(String value) {
if (value == null) {
return "";
}
String sanitized = value.trim().toLowerCase().replace("\\", "/");
sanitized = sanitized.replaceAll("[^a-z0-9_\\-./]", "_");
sanitized = sanitized.replaceAll("/+", "/");
sanitized = sanitized.replaceAll("^/+", "");
sanitized = sanitized.replaceAll("/+$", "");
if (sanitized.contains("..")) {
sanitized = sanitized.replace("..", "_");
}
return sanitized.replace("/", "_");
}
private static boolean verifyDataPacksPost(boolean allowRestarting) {
try (Stream<IrisData> stream = allPacks()) {
boolean bad = stream
@@ -1,198 +0,0 @@
package art.arcane.iris.core;
import art.arcane.volmlib.util.nbt.io.NBTDeserializer;
import art.arcane.volmlib.util.nbt.io.NBTSerializer;
import art.arcane.volmlib.util.nbt.io.NamedTag;
import art.arcane.volmlib.util.nbt.tag.ByteTag;
import art.arcane.volmlib.util.nbt.tag.CompoundTag;
import art.arcane.volmlib.util.nbt.tag.IntTag;
import art.arcane.volmlib.util.nbt.tag.ListTag;
import art.arcane.volmlib.util.nbt.tag.NumberTag;
import art.arcane.volmlib.util.nbt.tag.ShortTag;
import art.arcane.volmlib.util.nbt.tag.Tag;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
final class StructureNbtJigsawPoolRewriter {
private StructureNbtJigsawPoolRewriter() {
}
static byte[] rewrite(byte[] bytes, Map<String, String> remappedKeys) {
if (bytes == null || bytes.length == 0 || remappedKeys == null || remappedKeys.isEmpty()) {
return bytes;
}
try {
NbtReadResult readResult = readNamedTagWithCompression(bytes);
Tag<?> rootTag = readResult.namedTag().getTag();
if (!(rootTag instanceof CompoundTag compoundTag)) {
return bytes;
}
boolean rewritten = rewriteJigsawPoolReferences(compoundTag, remappedKeys);
if (!rewritten) {
return bytes;
}
return writeNamedTag(readResult.namedTag(), readResult.compressed());
} catch (Throwable ignored) {
return bytes;
}
}
private static boolean rewriteJigsawPoolReferences(CompoundTag root, Map<String, String> remappedKeys) {
ListTag<?> palette = root.getListTag("palette");
ListTag<?> blocks = root.getListTag("blocks");
if (palette == null || blocks == null || palette.size() <= 0 || blocks.size() <= 0) {
return false;
}
Set<Integer> jigsawStates = new HashSet<>();
for (int paletteIndex = 0; paletteIndex < palette.size(); paletteIndex++) {
Object paletteRaw = palette.get(paletteIndex);
if (!(paletteRaw instanceof CompoundTag paletteEntry)) {
continue;
}
String blockName = paletteEntry.getString("Name");
if ("minecraft:jigsaw".equalsIgnoreCase(blockName)) {
jigsawStates.add(paletteIndex);
}
}
if (jigsawStates.isEmpty()) {
return false;
}
boolean rewritten = false;
for (Object blockRaw : blocks.getValue()) {
if (!(blockRaw instanceof CompoundTag blockTag)) {
continue;
}
Integer stateIndex = tagToInt(blockTag.get("state"));
if (stateIndex == null || !jigsawStates.contains(stateIndex)) {
continue;
}
CompoundTag blockNbt = blockTag.getCompoundTag("nbt");
if (blockNbt == null || blockNbt.size() <= 0) {
continue;
}
String poolValue = blockNbt.getString("pool");
if (poolValue == null || poolValue.isBlank()) {
continue;
}
String normalizedPool = normalizeResourceKey(poolValue);
if (normalizedPool == null || normalizedPool.isBlank()) {
continue;
}
String remappedPool = remappedKeys.get(normalizedPool);
if (remappedPool == null || remappedPool.isBlank()) {
continue;
}
blockNbt.putString("pool", remappedPool);
rewritten = true;
}
return rewritten;
}
private static Integer tagToInt(Tag<?> tag) {
if (tag == null) {
return null;
}
if (tag instanceof IntTag intTag) {
return intTag.asInt();
}
if (tag instanceof ShortTag shortTag) {
return (int) shortTag.asShort();
}
if (tag instanceof ByteTag byteTag) {
return (int) byteTag.asByte();
}
if (tag instanceof NumberTag<?> numberTag) {
Number value = numberTag.getValue();
if (value != null) {
return value.intValue();
}
}
Object value = tag.getValue();
if (value instanceof Number number) {
return number.intValue();
}
return null;
}
private static String normalizeResourceKey(String value) {
if (value == null) {
return null;
}
String normalized = value.trim();
if (normalized.isEmpty()) {
return "";
}
if (normalized.charAt(0) == '#') {
normalized = normalized.substring(1);
}
String namespace = "minecraft";
String path = normalized;
int separator = normalized.indexOf(':');
if (separator >= 0) {
namespace = normalized.substring(0, separator).trim().toLowerCase();
path = normalized.substring(separator + 1).trim();
}
if (path.startsWith("worldgen/template_pool/")) {
path = path.substring("worldgen/template_pool/".length());
}
path = path.replace('\\', '/');
while (path.startsWith("/")) {
path = path.substring(1);
}
while (path.endsWith("/")) {
path = path.substring(0, path.length() - 1);
}
if (path.isEmpty()) {
return "";
}
return namespace + ":" + path;
}
private static NbtReadResult readNamedTagWithCompression(byte[] bytes) throws IOException {
IOException primary = null;
try {
NamedTag uncompressed = new NBTDeserializer(false).fromStream(new ByteArrayInputStream(bytes));
return new NbtReadResult(uncompressed, false);
} catch (IOException e) {
primary = e;
}
try {
NamedTag compressed = new NBTDeserializer(true).fromStream(new ByteArrayInputStream(bytes));
return new NbtReadResult(compressed, true);
} catch (IOException e) {
if (primary != null) {
e.addSuppressed(primary);
}
throw e;
}
}
private static byte[] writeNamedTag(NamedTag namedTag, boolean compressed) throws IOException {
return new NBTSerializer(compressed).toBytes(namedTag);
}
private record NbtReadResult(NamedTag namedTag, boolean compressed) {
}
}
@@ -1,153 +0,0 @@
package art.arcane.iris.core;
import art.arcane.iris.Iris;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.nms.datapack.DataVersion;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.json.JSONObject;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
public final class VanillaDatapackDumper {
private static final String DUMP_ZIP_NAME = "00-iris-vanilla-worldgen.zip";
private static final String MARKER_FILE = "vanilla-datapack-version.txt";
private static final String PACK_DESCRIPTION = "Iris extracted vanilla worldgen datapack.";
private VanillaDatapackDumper() {
}
public static void dumpIfNeeded(KList<File> datapackFolders) {
if (datapackFolders == null || datapackFolders.isEmpty()) {
return;
}
String currentVersion = resolveVersionKey();
if (currentVersion == null) {
Iris.warn("Unable to determine server version for vanilla datapack dump.");
return;
}
boolean needsDump = false;
for (File folder : datapackFolders) {
File zip = new File(folder, DUMP_ZIP_NAME);
File marker = new File(folder, MARKER_FILE);
if (!zip.exists() || !marker.exists() || !currentVersion.equals(readMarker(marker))) {
needsDump = true;
break;
}
}
if (!needsDump) {
Iris.verbose("Vanilla datapack is up to date, skipping dump.");
return;
}
Iris.info("Dumping vanilla worldgen datapack...");
Map<String, byte[]> entries = INMS.get().extractVanillaDatapack();
if (entries.isEmpty()) {
Iris.warn("Vanilla datapack extraction returned no entries. Skipping dump.");
return;
}
byte[] zipBytes = buildZip(entries);
if (zipBytes == null) {
Iris.error("Failed to build vanilla datapack ZIP.");
return;
}
int written = 0;
for (File folder : datapackFolders) {
folder.mkdirs();
File zip = new File(folder, DUMP_ZIP_NAME);
File marker = new File(folder, MARKER_FILE);
try {
Files.write(zip.toPath(), zipBytes);
Files.writeString(marker.toPath(), currentVersion, StandardCharsets.UTF_8);
written++;
} catch (IOException e) {
Iris.error("Failed to write vanilla datapack to " + folder.getAbsolutePath());
e.printStackTrace();
}
}
Iris.info("Vanilla datapack written to " + written + " world(s) with " + entries.size() + " entries.");
}
public static void removeIfPresent(KList<File> datapackFolders) {
if (datapackFolders == null || datapackFolders.isEmpty()) {
return;
}
int removed = 0;
for (File folder : datapackFolders) {
File zip = new File(folder, DUMP_ZIP_NAME);
File marker = new File(folder, MARKER_FILE);
if (zip.exists() && zip.delete()) {
removed++;
}
if (marker.exists()) {
marker.delete();
}
}
if (removed > 0) {
Iris.info("Removed vanilla datapack from " + removed + " world(s) (vanillaStructures disabled).");
}
}
private static byte[] buildZip(Map<String, byte[]> entries) {
try {
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
try (ZipOutputStream zos = new ZipOutputStream(baos)) {
zos.putNextEntry(new ZipEntry("pack.mcmeta"));
zos.write(buildPackMeta());
zos.closeEntry();
for (Map.Entry<String, byte[]> entry : entries.entrySet()) {
zos.putNextEntry(new ZipEntry(entry.getKey()));
zos.write(entry.getValue());
zos.closeEntry();
}
}
return baos.toByteArray();
} catch (IOException e) {
Iris.error("Failed to build vanilla datapack ZIP");
e.printStackTrace();
return null;
}
}
private static byte[] buildPackMeta() {
int packFormat = INMS.get().getDataVersion().getPackFormat();
JSONObject root = new JSONObject();
JSONObject pack = new JSONObject();
pack.put("description", PACK_DESCRIPTION);
pack.put("pack_format", packFormat);
root.put("pack", pack);
return root.toString(4).getBytes(StandardCharsets.UTF_8);
}
private static String resolveVersionKey() {
try {
DataVersion dv = INMS.get().getDataVersion();
return dv.getVersion() + ":" + dv.getPackFormat();
} catch (Exception e) {
return null;
}
}
private static String readMarker(File marker) {
try {
return Files.readString(marker.toPath(), StandardCharsets.UTF_8).trim();
} catch (IOException e) {
return null;
}
}
}
@@ -43,6 +43,7 @@ import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.util.common.director.DirectorContext;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.director.DirectorHelp;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
@@ -90,10 +91,10 @@ import java.util.zip.GZIPOutputStream;
@Director(name = "Developer", origin = DirectorOrigin.BOTH, description = "Iris World Manager", aliases = {"dev"})
public class CommandDeveloper implements DirectorExecutor {
private static final long DELETE_CHUNK_HEARTBEAT_MS = 5000L;
private static final int DELETE_CHUNK_MAX_ATTEMPTS = 2;
private static final int DELETE_CHUNK_STACK_LIMIT = 20;
private static final Set<String> ACTIVE_DELETE_CHUNK_WORLDS = ConcurrentHashMap.newKeySet();
@Director(description = "Show help tree for this command group", aliases = {"?"})
public void help() {
DirectorHelp.print(sender(), getClass());
}
@Director(description = "Get Loaded TectonicPlates Count", origin = DirectorOrigin.BOTH, sync = true)
public void EngineStatus() {
@@ -185,7 +186,7 @@ public class CommandDeveloper implements DirectorExecutor {
folder.mkdirs();
if (freshDownload) {
Iris.service(StudioSVC.class).downloadSearch(sender(), pack.getLoadKey(), false, true);
Iris.service(StudioSVC.class).downloadSearch(sender(), pack.getLoadKey(), true);
}
Iris.service(StudioSVC.class).installIntoWorld(sender(), pack.getLoadKey(), folder);
@@ -294,53 +295,6 @@ public class CommandDeveloper implements DirectorExecutor {
Files.write(target.toPath(), bytes);
}
@Director(description = "Test")
public void dumpThreads() {
try {
File fi = Iris.instance.getDataFile("dump", "td-" + new java.sql.Date(M.ms()) + ".txt");
FileOutputStream fos = new FileOutputStream(fi);
Map<Thread, StackTraceElement[]> f = Thread.getAllStackTraces();
PrintWriter pw = new PrintWriter(fos);
pw.println(Thread.activeCount() + "/" + f.size());
var run = Runtime.getRuntime();
pw.println("Memory:");
pw.println("\tMax: " + run.maxMemory());
pw.println("\tTotal: " + run.totalMemory());
pw.println("\tFree: " + run.freeMemory());
pw.println("\tUsed: " + (run.totalMemory() - run.freeMemory()));
for (Thread i : f.keySet()) {
pw.println("========================================");
pw.println("Thread: '" + i.getName() + "' ID: " + i.threadId() + " STATUS: " + i.getState().name());
for (StackTraceElement j : f.get(i)) {
pw.println(" @ " + j.toString());
}
pw.println("========================================");
pw.println();
pw.println();
}
pw.close();
Iris.info("DUMPED! See " + fi.getAbsolutePath());
} catch (Throwable e) {
e.printStackTrace();
}
}
@Director(description = "Generate Iris structures for all loaded datapack structures")
public void generateStructures(
@Param(description = "The pack to add the generated structures to", aliases = "pack", defaultValue = "null", customHandler = NullableDimensionHandler.class)
IrisDimension dimension,
@Param(description = "Ignore existing structures", defaultValue = "false")
boolean force
) {
sender().sendMessage(C.YELLOW + "Legacy structure conversion hooks have been removed.");
sender().sendMessage(C.YELLOW + "Use intrinsic structure generation and datapack ingestion instead.");
}
@Director(description = "Test")
public void packBenchmark(
@Param(description = "The pack to bench", aliases = {"pack"}, defaultValue = "overworld")
@@ -378,9 +332,7 @@ public class CommandDeveloper implements DirectorExecutor {
@Director(description = "Delete nearby chunk blocks for regen testing", name = "delete-chunk", aliases = {"delchunk", "dc"}, origin = DirectorOrigin.PLAYER, sync = true)
public void deleteChunk(
@Param(description = "Radius in chunks around your current chunk", defaultValue = "0")
int radius,
@Param(description = "How many chunks to process in parallel (0 = auto)", aliases = {"threads", "concurrency"}, defaultValue = "0")
int parallelism
int radius
) {
if (radius < 0) {
sender().sendMessage(C.RED + "Radius must be 0 or greater.");
@@ -392,536 +344,54 @@ public class CommandDeveloper implements DirectorExecutor {
sender().sendMessage(C.RED + "This is not an Iris world.");
return;
}
String worldKey = world.getName().toLowerCase(Locale.ROOT);
if (!ACTIVE_DELETE_CHUNK_WORLDS.add(worldKey)) {
sender().sendMessage(C.RED + "A delete-chunk run is already active for this world.");
return;
}
int threads = resolveDeleteChunkThreadCount(parallelism);
int centerX = player().getLocation().getBlockX() >> 4;
int centerZ = player().getLocation().getBlockZ() >> 4;
List<Position2> targets = buildDeleteChunkTargets(centerX, centerZ, radius);
int totalChunks = targets.size();
String runId = world.getName() + "-" + System.currentTimeMillis();
PlatformChunkGenerator access = IrisToolbelt.access(world);
if (access == null || access.getEngine() == null) {
ACTIVE_DELETE_CHUNK_WORLDS.remove(worldKey);
sender().sendMessage(C.RED + "The engine access for this world is null.");
return;
}
art.arcane.volmlib.util.mantle.runtime.Mantle mantle = access.getEngine().getMantle().getMantle();
VolmitSender sender = sender();
int centerX = player().getLocation().getBlockX() >> 4;
int centerZ = player().getLocation().getBlockZ() >> 4;
int minY = world.getMinHeight();
int maxY = world.getMaxHeight();
int total = (radius * 2 + 1) * (radius * 2 + 1);
int processed = 0;
int failed = 0;
sender.sendMessage(C.GREEN + "Deleting blocks in " + C.GOLD + totalChunks + C.GREEN + " chunk(s) with " + C.GOLD + threads + C.GREEN + " worker(s).");
if (J.isFolia()) {
sender.sendMessage(C.YELLOW + "Folia maintenance mode enabled for lock-safe chunk wipe + mantle purge.");
}
sender.sendMessage(C.YELLOW + "Delete-chunk run id: " + C.GOLD + runId + C.YELLOW + ".");
Iris.info("Delete-chunk run start: id=" + runId
+ " world=" + world.getName()
+ " center=" + centerX + "," + centerZ
+ " radius=" + radius
+ " workers=" + threads
+ " chunks=" + totalChunks);
Set<Thread> workerThreads = ConcurrentHashMap.newKeySet();
AtomicInteger workerCounter = new AtomicInteger();
ThreadFactory threadFactory = runnable -> {
Thread thread = new Thread(runnable, "Iris-DeleteChunk-" + runId + "-" + workerCounter.incrementAndGet());
thread.setDaemon(true);
workerThreads.add(thread);
return thread;
};
Thread orchestrator = new Thread(() -> runDeleteChunkOrchestrator(
sender,
world,
mantle,
targets,
threads,
runId,
worldKey,
workerThreads,
threadFactory
), "Iris-DeleteChunk-Orchestrator-" + runId);
orchestrator.setDaemon(true);
try {
orchestrator.start();
if (IrisSettings.get().getGeneral().isDebug()) {
Iris.info("Delete-chunk worker dispatched on dedicated thread=" + orchestrator.getName() + " id=" + runId + ".");
}
} catch (Throwable e) {
ACTIVE_DELETE_CHUNK_WORLDS.remove(worldKey);
sender.sendMessage(C.RED + "Failed to start delete-chunk worker thread. See console.");
Iris.reportError(e);
}
}
private int resolveDeleteChunkThreadCount(int parallelism) {
int threads = parallelism <= 0 ? Runtime.getRuntime().availableProcessors() : parallelism;
if (J.isFolia() && parallelism <= 0) {
threads = 1;
}
return Math.max(1, threads);
}
private List<Position2> buildDeleteChunkTargets(int centerX, int centerZ, int radius) {
int expected = (radius * 2 + 1) * (radius * 2 + 1);
List<Position2> targets = new ArrayList<>(expected);
for (int ring = 0; ring <= radius; ring++) {
for (int x = -ring; x <= ring; x++) {
for (int z = -ring; z <= ring; z++) {
if (Math.max(Math.abs(x), Math.abs(z)) != ring) {
continue;
}
targets.add(new Position2(centerX + x, centerZ + z));
}
}
}
return targets;
}
private void runDeleteChunkOrchestrator(
VolmitSender sender,
World world,
art.arcane.volmlib.util.mantle.runtime.Mantle mantle,
List<Position2> targets,
int threadCount,
String runId,
String worldKey,
Set<Thread> workerThreads,
ThreadFactory threadFactory
) {
long runStart = System.currentTimeMillis();
AtomicReference<String> phase = new AtomicReference<>("bootstrap");
AtomicLong phaseSince = new AtomicLong(runStart);
AtomicBoolean runDone = new AtomicBoolean(false);
Thread watchdog = createDeleteChunkSetupWatchdog(world, runId, runDone, phase, phaseSince);
watchdog.start();
IrisToolbelt.beginWorldMaintenance(world, "delete-chunk");
try (ThreadPoolExecutor pool = (ThreadPoolExecutor) Executors.newFixedThreadPool(threadCount, threadFactory)) {
setDeleteChunkPhase(phase, phaseSince, "dispatch", world, runId);
DeleteChunkSummary summary = executeDeleteChunkQueue(world, mantle, targets, pool, workerThreads, runId);
if (summary.failedChunks() <= 0) {
sender.sendMessage(C.GREEN + "Deleted blocks in " + C.GOLD + summary.successChunks() + C.GREEN + "/" + C.GOLD + summary.totalChunks() + C.GREEN + " chunk(s).");
return;
}
sender.sendMessage(C.RED + "Delete-chunk completed with " + C.GOLD + summary.failedChunks() + C.RED + " failed chunk(s).");
sender.sendMessage(C.YELLOW + "Successful chunks: " + C.GOLD + summary.successChunks() + C.YELLOW + "/" + C.GOLD + summary.totalChunks() + C.YELLOW + ".");
sender.sendMessage(C.YELLOW + "Retry attempts used: " + C.GOLD + summary.retryCount() + C.YELLOW + ".");
if (!summary.failedPreview().isEmpty()) {
sender.sendMessage(C.YELLOW + "Failed chunks sample: " + C.GOLD + summary.failedPreview());
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
sender.sendMessage(C.RED + "Delete-chunk run was interrupted.");
Iris.warn("Delete-chunk run interrupted: id=" + runId + " world=" + world.getName());
} catch (Throwable e) {
sender.sendMessage(C.RED + "Delete-chunk run failed. See console.");
Iris.reportError(e);
} finally {
runDone.set(true);
watchdog.interrupt();
IrisToolbelt.endWorldMaintenance(world, "delete-chunk");
ACTIVE_DELETE_CHUNK_WORLDS.remove(worldKey);
if (IrisSettings.get().getGeneral().isDebug()) {
Iris.info("Delete-chunk run closed: id=" + runId + " world=" + world.getName() + " totalMs=" + (System.currentTimeMillis() - runStart));
}
}
}
private DeleteChunkSummary executeDeleteChunkQueue(
World world,
art.arcane.volmlib.util.mantle.runtime.Mantle mantle,
List<Position2> targets,
ThreadPoolExecutor pool,
Set<Thread> workerThreads,
String runId
) throws InterruptedException {
ArrayDeque<DeleteChunkTask> pending = new ArrayDeque<>(targets.size());
long queuedAt = System.currentTimeMillis();
for (Position2 target : targets) {
pending.addLast(new DeleteChunkTask(target.getX(), target.getZ(), 1, queuedAt));
}
ConcurrentMap<String, DeleteChunkActiveTask> activeTasks = new ConcurrentHashMap<>();
ExecutorCompletionService<DeleteChunkResult> completion = new ExecutorCompletionService<>(pool);
List<Position2> failedChunks = new ArrayList<>();
int totalChunks = targets.size();
int successChunks = 0;
int failedCount = 0;
int retryCount = 0;
long submittedTasks = 0L;
long finishedTasks = 0L;
int completedChunks = 0;
int inFlight = 0;
int unchangedHeartbeats = 0;
int lastCompleted = -1;
long lastDump = 0L;
while (inFlight < pool.getMaximumPoolSize() && !pending.isEmpty()) {
DeleteChunkTask task = pending.removeFirst();
completion.submit(() -> runDeleteChunkTask(task, world, mantle, activeTasks));
inFlight++;
submittedTasks++;
}
while (completedChunks < totalChunks) {
Future<DeleteChunkResult> future = completion.poll(DELETE_CHUNK_HEARTBEAT_MS, TimeUnit.MILLISECONDS);
if (future == null) {
if (completedChunks == lastCompleted) {
unchangedHeartbeats++;
} else {
unchangedHeartbeats = 0;
lastCompleted = completedChunks;
}
Iris.warn("Delete-chunk heartbeat: id=" + runId
+ " completed=" + completedChunks + "/" + totalChunks
+ " remaining=" + (totalChunks - completedChunks)
+ " queued=" + pending.size()
+ " inFlight=" + inFlight
+ " submitted=" + submittedTasks
+ " finishedTasks=" + finishedTasks
+ " retries=" + retryCount
+ " failed=" + failedCount
+ " poolActive=" + pool.getActiveCount()
+ " poolQueue=" + pool.getQueue().size()
+ " poolDone=" + pool.getCompletedTaskCount()
+ " activeTasks=" + formatDeleteChunkActiveTasks(activeTasks));
if (unchangedHeartbeats >= 3 && System.currentTimeMillis() - lastDump >= 10000L) {
lastDump = System.currentTimeMillis();
Iris.warn("Delete-chunk appears stalled; dumping worker stack traces for id=" + runId + ".");
dumpDeleteChunkWorkerStacks(workerThreads, world.getName());
}
continue;
}
DeleteChunkResult result;
try {
result = future.get();
} catch (ExecutionException e) {
Throwable cause = e.getCause() == null ? e : e.getCause();
throw new IllegalStateException("Delete-chunk worker failed unexpectedly for run " + runId, cause);
}
inFlight--;
finishedTasks++;
long duration = result.finishedAtMs() - result.startedAtMs();
if (result.success()) {
completedChunks++;
successChunks++;
if (result.task().attempt() > 1) {
Iris.warn("Delete-chunk recovered after retry: id=" + runId
+ " chunk=" + result.task().chunkX() + "," + result.task().chunkZ()
+ " attempt=" + result.task().attempt()
+ " durationMs=" + duration);
} else if (duration >= 5000L) {
Iris.warn("Delete-chunk slow: id=" + runId
+ " chunk=" + result.task().chunkX() + "," + result.task().chunkZ()
+ " durationMs=" + duration
+ " loadedAtStart=" + result.loadedAtStart());
}
} else if (result.task().attempt() < DELETE_CHUNK_MAX_ATTEMPTS) {
retryCount++;
DeleteChunkTask retryTask = result.task().retry(System.currentTimeMillis());
pending.addLast(retryTask);
Iris.warn("Delete-chunk retry scheduled: id=" + runId
+ " chunk=" + result.task().chunkX() + "," + result.task().chunkZ()
+ " failedAttempt=" + result.task().attempt()
+ " nextAttempt=" + retryTask.attempt()
+ " error=" + result.errorSummary());
} else {
completedChunks++;
failedCount++;
Position2 failed = new Position2(result.task().chunkX(), result.task().chunkZ());
failedChunks.add(failed);
Iris.warn("Delete-chunk terminal failure: id=" + runId
+ " chunk=" + result.task().chunkX() + "," + result.task().chunkZ()
+ " attempts=" + result.task().attempt()
+ " error=" + result.errorSummary());
if (result.error() != null) {
Iris.reportError(result.error());
}
}
while (inFlight < pool.getMaximumPoolSize() && !pending.isEmpty()) {
DeleteChunkTask task = pending.removeFirst();
completion.submit(() -> runDeleteChunkTask(task, world, mantle, activeTasks));
inFlight++;
submittedTasks++;
}
}
String preview = formatDeleteChunkFailedPreview(failedChunks);
Iris.info("Delete-chunk run complete: id=" + runId
+ " world=" + world.getName()
+ " total=" + totalChunks
+ " success=" + successChunks
+ " failed=" + failedCount
+ " retries=" + retryCount
+ " submittedTasks=" + submittedTasks
+ " finishedTasks=" + finishedTasks
+ " failedPreview=" + preview);
return new DeleteChunkSummary(totalChunks, successChunks, failedCount, retryCount, preview);
}
private DeleteChunkResult runDeleteChunkTask(
DeleteChunkTask task,
World world,
art.arcane.volmlib.util.mantle.runtime.Mantle mantle,
ConcurrentMap<String, DeleteChunkActiveTask> activeTasks
) {
String worker = Thread.currentThread().getName();
long startedAt = System.currentTimeMillis();
boolean loadedAtStart = false;
try {
loadedAtStart = world.isChunkLoaded(task.chunkX(), task.chunkZ());
} catch (Throwable ignored) {
}
activeTasks.put(worker, new DeleteChunkActiveTask(task.chunkX(), task.chunkZ(), task.attempt(), startedAt, loadedAtStart));
try {
DeleteChunkRegionResult regionResult = wipeChunkRegion(world, task.chunkX(), task.chunkZ());
if (!regionResult.success()) {
return DeleteChunkResult.failure(task, worker, startedAt, System.currentTimeMillis(), loadedAtStart, regionResult.error());
}
mantle.deleteChunk(task.chunkX(), task.chunkZ());
return DeleteChunkResult.success(task, worker, startedAt, System.currentTimeMillis(), loadedAtStart);
} catch (Throwable e) {
if (e instanceof InterruptedException) {
Thread.currentThread().interrupt();
}
return DeleteChunkResult.failure(task, worker, startedAt, System.currentTimeMillis(), loadedAtStart, e);
} finally {
activeTasks.remove(worker);
}
}
private DeleteChunkRegionResult wipeChunkRegion(World world, int chunkX, int chunkZ) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(1);
AtomicReference<Throwable> failure = new AtomicReference<>();
if (!J.runRegion(world, chunkX, chunkZ, () -> {
try {
Chunk chunk = world.getChunkAt(chunkX, chunkZ);
for (org.bukkit.entity.Entity entity : chunk.getEntities()) {
if (!(entity instanceof org.bukkit.entity.Player)) {
entity.remove();
}
}
int minY = world.getMinHeight();
int maxY = world.getMaxHeight();
for (int xx = 0; xx < 16; xx++) {
for (int zz = 0; zz < 16; zz++) {
for (int yy = minY; yy < maxY; yy++) {
chunk.getBlock(xx, yy, zz).setType(org.bukkit.Material.AIR, false);
for (int x = -radius; x <= radius; x++) {
for (int z = -radius; z <= radius; z++) {
int chunkX = centerX + x;
int chunkZ = centerZ + z;
try {
Chunk chunk = world.getChunkAt(chunkX, chunkZ);
for (org.bukkit.entity.Entity entity : chunk.getEntities()) {
if (!(entity instanceof Player)) {
entity.remove();
}
}
}
} catch (Throwable e) {
failure.set(e);
} finally {
latch.countDown();
}
})) {
return DeleteChunkRegionResult.fail(new IllegalStateException("Failed to schedule region task for chunk " + chunkX + "," + chunkZ));
}
if (!latch.await(30, TimeUnit.SECONDS)) {
return DeleteChunkRegionResult.fail(new TimeoutException("Timed out waiting for region task at chunk " + chunkX + "," + chunkZ));
}
Throwable thrown = failure.get();
if (thrown != null) {
return DeleteChunkRegionResult.fail(thrown);
}
return DeleteChunkRegionResult.ok();
}
private Thread createDeleteChunkSetupWatchdog(
World world,
String runId,
AtomicBoolean runDone,
AtomicReference<String> phase,
AtomicLong phaseSince
) {
Thread watchdog = new Thread(() -> {
while (!runDone.get()) {
try {
Thread.sleep(DELETE_CHUNK_HEARTBEAT_MS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
if (!runDone.get()) {
long elapsed = System.currentTimeMillis() - phaseSince.get();
Iris.warn("Delete-chunk setup heartbeat: id=" + runId
+ " phase=" + phase.get()
+ " elapsedMs=" + elapsed
+ " world=" + world.getName());
for (int xx = 0; xx < 16; xx++) {
for (int zz = 0; zz < 16; zz++) {
for (int yy = minY; yy < maxY; yy++) {
chunk.getBlock(xx, yy, zz).setType(org.bukkit.Material.AIR, false);
}
}
}
mantle.deleteChunk(chunkX, chunkZ);
processed++;
} catch (Throwable e) {
failed++;
Iris.reportError(e);
}
}
}, "Iris-DeleteChunk-SetupWatchdog-" + runId);
watchdog.setDaemon(true);
return watchdog;
}
private void setDeleteChunkPhase(
AtomicReference<String> phase,
AtomicLong phaseSince,
String next,
World world,
String runId
) {
phase.set(next);
phaseSince.set(System.currentTimeMillis());
if (IrisSettings.get().getGeneral().isDebug()) {
Iris.info("Delete-chunk phase: id=" + runId + " phase=" + next + " world=" + world.getName());
}
}
private String formatDeleteChunkFailedPreview(List<Position2> failedChunks) {
if (failedChunks.isEmpty()) {
return "[]";
}
StringBuilder builder = new StringBuilder("[");
int index = 0;
for (Position2 chunk : failedChunks) {
if (index > 0) {
builder.append(", ");
}
if (index >= 10) {
builder.append("...");
break;
}
builder.append(chunk.getX()).append(",").append(chunk.getZ());
index++;
}
builder.append("]");
return builder.toString();
}
private String formatDeleteChunkActiveTasks(ConcurrentMap<String, DeleteChunkActiveTask> activeTasks) {
if (activeTasks.isEmpty()) {
return "{}";
}
StringBuilder builder = new StringBuilder("{");
int count = 0;
long now = System.currentTimeMillis();
for (Map.Entry<String, DeleteChunkActiveTask> entry : activeTasks.entrySet()) {
if (count > 0) {
builder.append(", ");
}
if (count >= 8) {
builder.append("...");
break;
}
DeleteChunkActiveTask activeTask = entry.getValue();
builder.append(entry.getKey())
.append("=")
.append(activeTask.chunkX())
.append(",")
.append(activeTask.chunkZ())
.append("@")
.append(activeTask.attempt())
.append("/")
.append(now - activeTask.startedAtMs())
.append("ms")
.append(activeTask.loadedAtStart() ? ":loaded" : ":cold");
count++;
if (failed == 0) {
sender().sendMessage(C.GREEN + "Deleted blocks in " + C.GOLD + processed + C.GREEN + "/" + C.GOLD + total + C.GREEN + " chunk(s).");
} else {
sender().sendMessage(C.YELLOW + "Deleted blocks in " + C.GOLD + processed + C.YELLOW + "/" + C.GOLD + total + C.YELLOW + " chunk(s); " + C.RED + failed + C.YELLOW + " failed.");
}
builder.append("}");
return builder.toString();
}
private void dumpDeleteChunkWorkerStacks(Set<Thread> explicitThreads, String worldName) {
Set<Thread> threads = new LinkedHashSet<>();
threads.addAll(explicitThreads);
for (Thread thread : Thread.getAllStackTraces().keySet()) {
if (thread == null || !thread.isAlive()) {
continue;
}
String name = thread.getName();
if (name.startsWith("Iris-DeleteChunk-")
|| name.startsWith("Iris EngineSVC-")
|| name.startsWith("Iris World Manager")
|| name.contains(worldName)) {
threads.add(thread);
}
}
for (Thread thread : threads) {
if (thread == null || !thread.isAlive()) {
continue;
}
Iris.warn("Delete-chunk worker thread=" + thread.getName() + " state=" + thread.getState());
StackTraceElement[] trace = thread.getStackTrace();
int limit = Math.min(trace.length, DELETE_CHUNK_STACK_LIMIT);
for (int i = 0; i < limit; i++) {
Iris.warn(" at " + trace[i]);
}
}
}
private record DeleteChunkTask(int chunkX, int chunkZ, int attempt, long queuedAtMs) {
private DeleteChunkTask retry(long now) {
return new DeleteChunkTask(chunkX, chunkZ, attempt + 1, now);
}
}
private record DeleteChunkActiveTask(int chunkX, int chunkZ, int attempt, long startedAtMs, boolean loadedAtStart) {
}
private record DeleteChunkResult(
DeleteChunkTask task,
String worker,
long startedAtMs,
long finishedAtMs,
boolean loadedAtStart,
boolean success,
Throwable error
) {
private static DeleteChunkResult success(DeleteChunkTask task, String worker, long startedAtMs, long finishedAtMs, boolean loadedAtStart) {
return new DeleteChunkResult(task, worker, startedAtMs, finishedAtMs, loadedAtStart, true, null);
}
private static DeleteChunkResult failure(DeleteChunkTask task, String worker, long startedAtMs, long finishedAtMs, boolean loadedAtStart, Throwable error) {
return new DeleteChunkResult(task, worker, startedAtMs, finishedAtMs, loadedAtStart, false, error);
}
private String errorSummary() {
if (error == null) {
return "unknown";
}
String message = error.getMessage();
if (message == null || message.isEmpty()) {
return error.getClass().getSimpleName();
}
return error.getClass().getSimpleName() + ": " + message;
}
}
private record DeleteChunkRegionResult(boolean success, Throwable error) {
private static DeleteChunkRegionResult ok() {
return new DeleteChunkRegionResult(true, null);
}
private static DeleteChunkRegionResult fail(Throwable error) {
return new DeleteChunkRegionResult(false, error);
}
}
private record DeleteChunkSummary(int totalChunks, int successChunks, int failedChunks, int retryCount, String failedPreview) {
}
@Director(description = "UnloadChunks for good reasons.")
@@ -22,6 +22,7 @@ import art.arcane.iris.Iris;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.engine.object.*;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.director.DirectorHelp;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
@@ -32,6 +33,10 @@ import java.awt.*;
@Director(name = "edit", origin = DirectorOrigin.PLAYER, studio = true, description = "Edit something")
public class CommandEdit implements DirectorExecutor {
@Director(description = "Show help tree for this command group", aliases = {"?"})
public void help() {
DirectorHelp.print(sender(), getClass());
}
private boolean noStudio() {
if (!sender().isPlayer()) {
@@ -1,174 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.commands;
import art.arcane.iris.Iris;
import art.arcane.iris.core.ExternalDataPackPipeline;
import art.arcane.iris.core.service.ObjectStudioSaveService;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.iris.util.common.director.specialhandlers.ObjectHandler;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.util.Set;
@Director(name = "find", origin = DirectorOrigin.PLAYER, description = "Iris Find commands", aliases = "goto")
public class CommandFind implements DirectorExecutor {
@Director(description = "Find a biome")
public void biome(
@Param(description = "The biome to look for")
IrisBiome biome,
@Param(description = "Should you be teleported", defaultValue = "true")
boolean teleport
) {
Engine e = engine();
if (e == null) {
sender().sendMessage(C.GOLD + "Not in an Iris World!");
return;
}
e.gotoBiome(biome, player(), teleport);
}
@Director(description = "Find a region")
public void region(
@Param(description = "The region to look for")
IrisRegion region,
@Param(description = "Should you be teleported", defaultValue = "true")
boolean teleport
) {
Engine e = engine();
if (e == null) {
sender().sendMessage(C.GOLD + "Not in an Iris World!");
return;
}
e.gotoRegion(region, player(), teleport);
}
@Director(description = "Find a point of interest.")
public void poi(
@Param(description = "The type of PoI to look for.")
String type,
@Param(description = "Should you be teleported", defaultValue = "true")
boolean teleport
) {
Engine e = engine();
if (e == null) {
sender().sendMessage(C.GOLD + "Not in an Iris World!");
return;
}
e.gotoPOI(type, player(), teleport);
}
@Director(description = "Find an object")
public void object(
@Param(description = "The object to look for", customHandler = ObjectHandler.class)
String object,
@Param(description = "Should you be teleported", defaultValue = "true")
boolean teleport
) {
Engine e = engine();
if (e == null) {
sender().sendMessage(C.GOLD + "Not in an Iris World!");
return;
}
Player studioPlayer = player();
if (studioPlayer != null) {
try {
if (ObjectStudioSaveService.get().teleportTo(studioPlayer, object)) {
sender().sendMessage(C.GREEN + "Object Studio: teleporting to " + object);
return;
}
} catch (Throwable t) {
Iris.reportError(t);
}
}
if (e.hasObjectPlacement(object)) {
e.gotoObject(object, player(), teleport);
return;
}
Set<String> structures = ExternalDataPackPipeline.resolveLocateStructuresForObjectKey(object);
VolmitSender commandSender = sender();
if (structures.isEmpty()) {
if (commandSender != null) {
commandSender.sendMessage(C.RED + object + " is not configured in any region/biome object placements and has no external structure mapping.");
commandSender.sendMessage(C.GRAY + "Try /iris locateexternal <datapack-id> for external structure lookups.");
}
return;
}
Player target = player();
if (target == null) {
if (commandSender != null) {
commandSender.sendMessage(C.RED + "No active player sender was available for object lookup.");
}
return;
}
Runnable dispatchTask = () -> {
int dispatched = 0;
for (String structure : structures) {
String command = "locate structure " + structure;
boolean accepted = Bukkit.dispatchCommand(target, command);
if (!accepted) {
if (commandSender != null) {
commandSender.sendMessage(C.RED + "Failed to dispatch: /" + command);
}
} else {
if (commandSender != null) {
commandSender.sendMessage(C.GREEN + "Dispatched: /" + command);
}
dispatched++;
}
}
if (teleport) {
if (commandSender != null) {
commandSender.sendMessage(C.YELLOW + "External object lookups are structure-backed and dispatch locate commands instead of direct teleport.");
}
}
if (commandSender != null) {
commandSender.sendMessage(C.GREEN + "External object mapping matched locateTargets=" + structures.size() + ", dispatched=" + dispatched + ".");
}
};
if (!J.runEntity(target, dispatchTask)) {
if (commandSender != null) {
commandSender.sendMessage(C.RED + "Failed to schedule external object locate dispatch on your region thread.");
}
}
}
}
@@ -19,7 +19,6 @@
package art.arcane.iris.core.commands;
import art.arcane.iris.Iris;
import art.arcane.iris.core.ExternalDataPackPipeline;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.IrisWorlds;
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
@@ -29,7 +28,6 @@ import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisExternalDatapack;
import art.arcane.iris.engine.platform.ChunkReplacementListener;
import art.arcane.iris.engine.platform.ChunkReplacementOptions;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
@@ -37,11 +35,11 @@ import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.util.common.director.DirectorContext;
import art.arcane.volmlib.util.director.DirectorParameterHandler;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.director.DirectorHelp;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.volmlib.util.director.exceptions.DirectorParsingException;
import art.arcane.iris.util.common.director.specialhandlers.ExternalDatapackLocateHandler;
import art.arcane.iris.util.common.director.specialhandlers.NullablePlayerHandler;
import art.arcane.iris.util.common.format.C;
import art.arcane.volmlib.util.io.IO;
@@ -90,13 +88,17 @@ import static org.bukkit.Bukkit.getServer;
@Director(name = "iris", aliases = {"ir", "irs"}, description = "Basic Command")
public class CommandIris implements DirectorExecutor {
@Director(description = "Show help tree for this command group", aliases = {"?"})
public void help() {
DirectorHelp.print(sender(), getClass());
}
private CommandStudio studio;
private CommandPregen pregen;
private CommandSettings settings;
private CommandObject object;
private CommandWhat what;
private CommandEdit edit;
private CommandFind find;
private CommandDeveloper developer;
private CommandPack pack;
public static boolean worldCreation = false;
@@ -449,170 +451,6 @@ public class CommandIris implements DirectorExecutor {
sender().sendMessage(C.GREEN + "Iris v" + Iris.instance.getDescription().getVersion() + " by Volmit Software");
}
@Director(description = "Locate structure targets mapped to an external datapack id in this world's dimension config", aliases = {"locateexternal", "locateext"}, origin = DirectorOrigin.PLAYER, sync = true)
public void locateExternal(
@Param(description = "External datapack id or structure id (comma separated values supported)", customHandler = ExternalDatapackLocateHandler.class)
String id,
@Param(description = "Run locate for every structure mapped to the id(s)", defaultValue = "true")
boolean all
) {
if (id == null || id.trim().isBlank()) {
sender().sendMessage(C.RED + "You must provide an external datapack id.");
return;
}
Engine activeEngine = engine();
if (activeEngine == null || activeEngine.getDimension() == null) {
sender().sendMessage(C.RED + "You must be in an Iris world to use locateexternal.");
return;
}
IrisDimension dimension = activeEngine.getDimension();
KList<IrisExternalDatapack> externalDatapacks = dimension.getExternalDatapacks();
if (externalDatapacks == null || externalDatapacks.isEmpty()) {
sender().sendMessage(C.RED + "This dimension has no externalDatapacks entries.");
return;
}
LinkedHashSet<String> requestedTokens = new LinkedHashSet<>();
for (String token : id.split(",")) {
if (token == null) {
continue;
}
String normalizedToken = normalizeLocateExternalToken(token);
if (!normalizedToken.isBlank()) {
requestedTokens.add(normalizedToken);
}
}
if (requestedTokens.isEmpty()) {
sender().sendMessage(C.RED + "No valid external datapack ids or structure ids were provided.");
return;
}
Map<String, Set<String>> fallbackById = buildExternalLocateFallbackById(externalDatapacks);
LinkedHashSet<String> structures = new LinkedHashSet<>();
LinkedHashSet<String> matchedIds = new LinkedHashSet<>();
for (String token : requestedTokens) {
Set<String> resolvedStructures = ExternalDataPackPipeline.resolveLocateStructuresForId(token);
if (resolvedStructures.isEmpty()) {
Set<String> fallbackStructures = fallbackById.get(token);
if (fallbackStructures != null) {
resolvedStructures = fallbackStructures;
}
}
if (!resolvedStructures.isEmpty()) {
matchedIds.add(token);
structures.addAll(resolvedStructures);
continue;
}
String structureToken = normalizeLocateStructureToken(token);
if (!structureToken.isBlank()) {
matchedIds.add("structure:" + structureToken);
structures.add(structureToken);
}
}
if (structures.isEmpty()) {
sender().sendMessage(C.RED + "No external datapack entry matched value(s): " + String.join(", ", requestedTokens));
return;
}
VolmitSender commandSender = sender();
Runnable dispatchTask = () -> dispatchLocateExternalCommands(commandSender, structures, matchedIds, all);
if (commandSender.isPlayer()) {
Player player = commandSender.player();
if (player == null) {
commandSender.sendMessage(C.RED + "No active player sender was available for locateexternal.");
return;
}
if (!J.runEntity(player, dispatchTask)) {
commandSender.sendMessage(C.RED + "Failed to schedule locate command dispatch on the player's region thread.");
}
return;
}
J.s(dispatchTask);
}
private void dispatchLocateExternalCommands(
VolmitSender commandSender,
Set<String> structures,
Set<String> matchedIds,
boolean all
) {
org.bukkit.command.CommandSender locateSender = commandSender.isPlayer()
? commandSender.player()
: Bukkit.getConsoleSender();
int dispatched = 0;
for (String structure : structures) {
String command = "locate structure " + structure;
boolean accepted = Bukkit.dispatchCommand(locateSender, command);
if (!accepted) {
commandSender.sendMessage(C.RED + "Failed to dispatch: /" + command);
} else {
commandSender.sendMessage(C.GREEN + "Dispatched: /" + command);
dispatched++;
}
if (!all) {
break;
}
}
commandSender.sendMessage(C.GREEN + "Matched ids=" + matchedIds.size() + ", locateTargets=" + structures.size() + ", dispatched=" + dispatched + ".");
}
private static String normalizeLocateExternalToken(String token) {
if (token == null) {
return "";
}
String normalized = token.trim().toLowerCase(Locale.ROOT);
if (normalized.isBlank()) {
return "";
}
normalized = normalized.replace("minecraft:worldgen/structure/", "");
normalized = normalized.replace("worldgen/structure/", "");
if (!normalized.contains(":") && normalized.contains("/")) {
return normalized;
}
if (!normalized.contains(":")) {
return normalized;
}
return normalized;
}
private static Map<String, Set<String>> buildExternalLocateFallbackById(KList<IrisExternalDatapack> externalDatapacks) {
return new ConcurrentHashMap<>();
}
private static String normalizeLocateStructureToken(String structure) {
if (structure == null) {
return "";
}
String normalized = structure.trim().toLowerCase(Locale.ROOT);
if (normalized.isBlank()) {
return "";
}
normalized = normalized.replace("minecraft:worldgen/structure/", "");
normalized = normalized.replace("worldgen/structure/", "");
if (!normalized.contains(":")) {
normalized = "minecraft:" + normalized;
}
return normalized;
}
/*
/todo
@Director(description = "Benchmark a pack", origin = DirectorOrigin.CONSOLE)
@@ -763,25 +601,21 @@ public class CommandIris implements DirectorExecutor {
sender().sendMessage(C.GREEN + "Set debug to: " + to);
}
//TODO fix pack trimming
@Director(description = "Download a project.", aliases = "dl")
public void download(
@Param(name = "pack", description = "The pack to download", defaultValue = "overworld", aliases = "project")
String pack,
@Param(name = "branch", description = "The branch to download from", defaultValue = "stable")
String branch,
//@Param(name = "trim", description = "Whether or not to download a trimmed version (do not enable when editing)", defaultValue = "false")
//boolean trim,
@Param(name = "overwrite", description = "Whether or not to overwrite the pack with the downloaded one", aliases = "force", defaultValue = "false")
boolean overwrite
) {
boolean trim = false;
sender().sendMessage(C.GREEN + "Downloading pack: " + pack + "/" + branch + (trim ? " trimmed" : "") + (overwrite ? " overwriting" : ""));
sender().sendMessage(C.GREEN + "Downloading pack: " + pack + "/" + branch + (overwrite ? " overwriting" : ""));
if (pack.equals("overworld")) {
String url = "https://github.com/IrisDimensions/overworld/releases/download/" + INMS.OVERWORLD_TAG + "/overworld.zip";
Iris.service(StudioSVC.class).downloadRelease(sender(), url, trim, overwrite);
Iris.service(StudioSVC.class).downloadRelease(sender(), url, overwrite);
} else {
Iris.service(StudioSVC.class).downloadSearch(sender(), "IrisDimensions/" + pack + "/" + branch, trim, overwrite);
Iris.service(StudioSVC.class).downloadSearch(sender(), "IrisDimensions/" + pack + "/" + branch, overwrite);
}
}
@@ -22,10 +22,13 @@ import art.arcane.iris.Iris;
import art.arcane.iris.core.link.WorldEditLink;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.core.runtime.ObjectStudioActivation;
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
import art.arcane.iris.core.service.ObjectSVC;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.core.service.WandSVC;
import art.arcane.iris.core.tools.IrisConverter;
import art.arcane.iris.core.tools.PlausibilizeMode;
import art.arcane.iris.core.tools.TreePlausibilizer;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.*;
@@ -33,17 +36,23 @@ import art.arcane.volmlib.util.data.Cuboid;
import art.arcane.iris.util.common.data.IrisCustomData;
import art.arcane.iris.util.common.data.registry.Materials;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.director.DirectorHelp;
import art.arcane.iris.util.common.director.specialhandlers.NullableDimensionHandler;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.iris.util.common.director.specialhandlers.ObjectHandler;
import art.arcane.iris.util.common.director.specialhandlers.ObjectTargetHandler;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.math.Direction;
import art.arcane.volmlib.util.math.RNG;
import io.papermc.lib.PaperLib;
import org.bukkit.*;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.util.Vector;
@@ -54,6 +63,105 @@ import java.util.*;
@Director(name = "object", aliases = "o", origin = DirectorOrigin.PLAYER, studio = true, description = "Iris object manipulation")
public class CommandObject implements DirectorExecutor {
@Director(description = "Show help tree for this command group", aliases = {"?"})
public void help() {
DirectorHelp.print(sender(), getClass());
}
@Director(description = "Open an object studio world (grid of every object; dimension optional, defaults to all packs)", aliases = {"std", "s"}, sync = true)
public void studio(
@Param(defaultValue = "null", description = "Optional dimension whose object pack to lay out; omit to aggregate objects from every pack", aliases = "dim", customHandler = NullableDimensionHandler.class)
IrisDimension dimension,
@Param(defaultValue = "1337", description = "The seed to generate the studio with", aliases = "s")
long seed
) {
VolmitSender commandSender = sender();
Map<String, IrisData> sources = new LinkedHashMap<>();
IrisDimension hostDimension = dimension;
if (dimension != null) {
IrisData data = dimension.getLoader();
if (data == null) {
data = IrisData.get(dimension.getLoadFile().getParentFile().getParentFile());
}
sources.put(data.getDataFolder().getName(), data);
} else {
File workspace = Iris.service(StudioSVC.class).getWorkspaceFolder();
File[] packs = workspace == null ? null : workspace.listFiles();
if (packs != null) {
Arrays.sort(packs, Comparator.comparing(File::getName, String.CASE_INSENSITIVE_ORDER));
for (File pack : packs) {
if (!pack.isDirectory()) continue;
File dimensionsDir = new File(pack, "dimensions");
if (!dimensionsDir.isDirectory()) continue;
IrisData data = IrisData.get(pack);
String[] keys = data.getObjectLoader().getPossibleKeys();
if (keys == null || keys.length == 0) continue;
sources.put(pack.getName(), data);
if (hostDimension == null) {
File[] dimFiles = dimensionsDir.listFiles((f) -> f.isFile() && f.getName().endsWith(".json"));
if (dimFiles != null && dimFiles.length > 0) {
String loadKey = dimFiles[0].getName().replaceFirst("\\.json$", "");
IrisDimension loaded = data.getDimensionLoader().load(loadKey);
if (loaded != null) {
hostDimension = loaded;
}
}
}
}
}
}
if (hostDimension == null || sources.isEmpty()) {
commandSender.sendMessage(C.RED + "No packs with objects were found on this server.");
return;
}
int totalObjects = 0;
for (IrisData d : sources.values()) {
String[] k = d.getObjectLoader().getPossibleKeys();
if (k != null) totalObjects += k.length;
}
if (totalObjects == 0) {
commandSender.sendMessage(C.RED + "No objects to place across the selected pack(s).");
return;
}
hostDimension.setStudioMode(StudioMode.OBJECT_BUFFET);
ObjectStudioActivation.activate(hostDimension.getLoadKey());
ObjectStudioActivation.setSources(hostDimension.getLoadKey(), sources);
String scope = dimension == null
? ("all packs [" + sources.size() + "]")
: ("\"" + hostDimension.getName() + "\"");
commandSender.sendMessage(C.GREEN + "Opening Object Studio for " + scope + " ("
+ totalObjects + " objects)");
IrisDimension finalHost = hostDimension;
try {
Iris.service(StudioSVC.class).open(commandSender, seed, hostDimension.getLoadKey(), world -> {
if (world == null) return;
try {
WorldRuntimeControlService.get().applyObjectStudioWorldRules(world);
} catch (Throwable e) {
Iris.reportError("Failed to apply object studio world rules for " + world.getName(), e);
}
if (commandSender.isPlayer()) {
Player p = commandSender.player();
if (p != null) {
Location target = new Location(world, 0.5D, 66D, 0.5D);
J.runEntity(p, () -> {
PaperLib.teleportAsync(p, target).thenRun(() -> p.setGameMode(GameMode.CREATIVE));
});
}
}
});
} catch (Throwable e) {
Iris.reportError("Failed to open object studio world \"" + finalHost.getLoadKey() + "\".", e);
commandSender.sendMessage(C.RED + "Failed to open object studio: " + e.getMessage());
}
}
private static final Set<Material> skipBlocks = Set.of(Materials.GRASS, Material.SNOW, Material.VINE, Material.TORCH, Material.DEAD_BUSH,
Material.POPPY, Material.DANDELION);
@@ -225,21 +333,18 @@ public class CommandObject implements DirectorExecutor {
}
}
@Director(description = "Bridge unreachable leaves with hidden logs so trees are vanilla-decay-plausible",
@Director(description = "Make tree leaves vanilla-decay-plausible (every leaf within 6 blocks of a log)",
origin = DirectorOrigin.BOTH, studio = false)
public void plausibilize(
@Param(description = "Pack key (trees/bonsai/smbase1), pack prefix (trees/), or filesystem path to a .iob file or directory")
@Param(description = "Object key, prefix (trees/), or filesystem path",
customHandler = ObjectTargetHandler.class)
String target,
@Param(description = "DEFAULT: tentacle logs, delete orphans. NORMALIZE: + flip persistent=false. FOLIAGE_OVERATURE: add leaves to bridge orphans, no deletions. SMOKE: wipe & repaint canopy shell.",
defaultValue = "DEFAULT")
PlausibilizeMode mode,
@Param(description = "Analyze only, do not write", defaultValue = "false")
boolean dryRun,
@Param(description = "Flip persistent=true leaves to false and bridge them as well",
defaultValue = "false")
boolean normalize,
@Param(description = "Wipe scattered leaves and repaint a canopy shell around every log, then bridge any gaps with interior log tendrils",
defaultValue = "false")
boolean smoke,
@Param(description = "Canopy shell radius (smoke mode only), clamped to [0,5]",
defaultValue = "2")
@Param(description = "Canopy shell radius (SMOKE only), clamped [0,5]", defaultValue = "2")
int radius
) {
List<Target> targets = resolveTargets(target);
@@ -248,13 +353,13 @@ public class CommandObject implements DirectorExecutor {
return;
}
sender().sendMessage(C.IRIS + "Plausibilize: queued " + targets.size() + " object(s)"
+ (dryRun ? " [DRY RUN]" : "")
+ (normalize ? " [NORMALIZE]" : "")
+ (smoke ? " [SMOKE r=" + radius + "]" : ""));
sender().sendMessage(C.IRIS + "Plausibilize [" + mode.name()
+ (dryRun ? " DRY" : "")
+ (mode == PlausibilizeMode.SMOKE ? " r=" + radius : "")
+ "] queued " + targets.size() + " object(s)");
org.bukkit.command.CommandSender s = sender();
J.a(() -> runPlausibilize(targets, dryRun, normalize, smoke, radius, s));
J.a(() -> runPlausibilize(targets, dryRun, mode, radius, s));
}
private List<Target> resolveTargets(String target) {
@@ -330,8 +435,7 @@ public class CommandObject implements DirectorExecutor {
private static void runPlausibilize(
List<Target> targets,
boolean dryRun,
boolean normalize,
boolean smoke,
PlausibilizeMode mode,
int radius,
org.bukkit.command.CommandSender s
) {
@@ -340,9 +444,6 @@ public class CommandObject implements DirectorExecutor {
int failed = 0;
int changed = 0;
long totalLogsAdded = 0L;
long totalReachableBefore = 0L;
long totalLeaves = 0L;
long totalPersistentInput = 0L;
long totalLeavesAdded = 0L;
long totalLeavesRemoved = 0L;
long totalNormalized = 0L;
@@ -362,8 +463,8 @@ public class CommandObject implements DirectorExecutor {
}
TreePlausibilizer.Result r = dryRun
? TreePlausibilizer.analyze(o, normalize, smoke, radius)
: TreePlausibilizer.apply(o, normalize, smoke, radius);
? TreePlausibilizer.analyze(o, mode, radius)
: TreePlausibilizer.apply(o, mode, radius);
if (r.skipReason() != null) {
s.sendMessage(C.YELLOW + " skip " + t.key() + ": " + r.skipReason());
@@ -383,9 +484,6 @@ public class CommandObject implements DirectorExecutor {
processed++;
totalLogsAdded += r.logsAdded();
totalReachableBefore += r.reachableBefore();
totalLeaves += r.totalLeaves();
totalPersistentInput += r.persistentLeavesInput();
totalLeavesAdded += r.leavesAdded();
totalLeavesRemoved += r.leavesRemoved();
totalNormalized += r.leavesNormalized();
@@ -393,14 +491,11 @@ public class CommandObject implements DirectorExecutor {
if (touched || targets.size() == 1) {
s.sendMessage(C.GRAY + " " + t.key()
+ " leaves=" + r.totalLeaves()
+ " persistentIn=" + r.persistentLeavesInput()
+ " reachable=" + r.reachableBefore()
+ " logsAdded=" + r.logsAdded()
+ " leavesAdded=" + r.leavesAdded()
+ " leavesRemoved=" + r.leavesRemoved()
+ " normalized=" + r.leavesNormalized()
+ " remaining=" + r.unreachableAfter());
+ C.WHITE + " +" + r.logsAdded() + " logs"
+ C.WHITE + " +" + r.leavesAdded() + " leaves"
+ C.WHITE + " -" + r.leavesRemoved() + " removed"
+ (r.leavesNormalized() > 0 ? C.WHITE + " ~" + r.leavesNormalized() + " normalized" : "")
+ (r.unreachableAfter() > 0 ? C.YELLOW + " " + r.unreachableAfter() + " unreachable" : ""));
}
if (targets.size() > 1 && index % progressStep == 0) {
@@ -414,19 +509,11 @@ public class CommandObject implements DirectorExecutor {
}
}
s.sendMessage(C.IRIS + "Done."
+ " processed=" + processed
+ " changed=" + changed
+ " skipped=" + skipped
+ " failed=" + failed
+ " leaves=" + totalLeaves
+ " persistentIn=" + totalPersistentInput
+ " reachableBefore=" + totalReachableBefore
+ " logsAdded=" + totalLogsAdded
+ " leavesAdded=" + totalLeavesAdded
+ " leavesRemoved=" + totalLeavesRemoved
+ " normalized=" + totalNormalized
+ " remaining=" + totalUnreachableAfter);
s.sendMessage(C.IRIS + "Done: " + processed + " processed, " + changed + " changed, "
+ skipped + " skipped, " + failed + " failed");
s.sendMessage(C.IRIS + "Totals: +" + totalLogsAdded + " logs, +" + totalLeavesAdded + " leaves, -"
+ totalLeavesRemoved + " removed, ~" + totalNormalized + " normalized, "
+ totalUnreachableAfter + " unreachable");
}
private static IrisObject loadTarget(Target t) throws IOException {
@@ -23,6 +23,7 @@ import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.core.pack.PackValidationResult;
import art.arcane.iris.core.pack.PackValidator;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.director.DirectorHelp;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.volmlib.util.director.annotations.Director;
@@ -32,6 +33,10 @@ import java.io.File;
@Director(name = "pack", aliases = {"pk"}, description = "Pack validation and maintenance")
public class CommandPack implements DirectorExecutor {
@Director(description = "Show help tree for this command group", aliases = {"?"})
public void help() {
DirectorHelp.print(sender(), getClass());
}
@Director(description = "Validate a pack (or all packs) and re-publish results", aliases = {"v", "check"})
public void validate(
@@ -23,6 +23,7 @@ import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.pregenerator.PregenTask;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.director.DirectorHelp;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.iris.util.common.format.C;
@@ -32,6 +33,11 @@ import org.bukkit.util.Vector;
@Director(name = "pregen", aliases = "pregenerate", description = "Pregenerate your Iris worlds!")
public class CommandPregen implements DirectorExecutor {
@Director(description = "Show help tree for this command group", aliases = {"?"})
public void help() {
DirectorHelp.print(sender(), getClass());
}
@Director(description = "Pregenerate a world")
public void start(
@Param(description = "The radius of the pregen in blocks", aliases = "size")
@@ -24,8 +24,6 @@ import art.arcane.iris.core.gui.NoiseExplorerGUI;
import art.arcane.iris.core.gui.VisionGUI;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.project.IrisProject;
import art.arcane.iris.core.runtime.ObjectStudioActivation;
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
@@ -38,6 +36,7 @@ import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.collection.KSet;
import art.arcane.iris.util.common.director.DirectorContext;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.director.DirectorHelp;
import art.arcane.iris.util.common.director.handlers.DimensionHandler;
import art.arcane.iris.util.common.director.specialhandlers.NullableDimensionHandler;
import art.arcane.volmlib.util.director.DirectorOrigin;
@@ -92,7 +91,11 @@ import java.util.function.Supplier;
@Director(name = "studio", aliases = {"std", "s"}, description = "Studio Commands", studio = true)
public class CommandStudio implements DirectorExecutor {
private CommandFind find;
@Director(description = "Show help tree for this command group", aliases = {"?"})
public void help() {
DirectorHelp.print(sender(), getClass());
}
private CommandEdit edit;
//private CommandDeepSearch deepSearch;
@@ -125,101 +128,6 @@ public class CommandStudio implements DirectorExecutor {
Iris.service(StudioSVC.class).open(sender(), seed, dimension.getLoadKey());
}
@Director(description = "Open an object studio world (grid of every object; dimension optional, defaults to all packs)", aliases = {"obj", "objs"}, sync = true)
public void object(
@Param(defaultValue = "null", description = "Optional dimension whose object pack to lay out; omit to aggregate objects from every pack", aliases = "dim", customHandler = NullableDimensionHandler.class)
IrisDimension dimension,
@Param(defaultValue = "1337", description = "The seed to generate the studio with", aliases = "s")
long seed
) {
VolmitSender commandSender = sender();
java.util.Map<String, IrisData> sources = new java.util.LinkedHashMap<>();
IrisDimension hostDimension = dimension;
if (dimension != null) {
IrisData data = dimension.getLoader();
if (data == null) {
data = IrisData.get(dimension.getLoadFile().getParentFile().getParentFile());
}
sources.put(data.getDataFolder().getName(), data);
} else {
File workspace = Iris.service(StudioSVC.class).getWorkspaceFolder();
File[] packs = workspace == null ? null : workspace.listFiles();
if (packs != null) {
Arrays.sort(packs, java.util.Comparator.comparing(File::getName, String.CASE_INSENSITIVE_ORDER));
for (File pack : packs) {
if (!pack.isDirectory()) continue;
File dimensionsDir = new File(pack, "dimensions");
if (!dimensionsDir.isDirectory()) continue;
IrisData data = IrisData.get(pack);
String[] keys = data.getObjectLoader().getPossibleKeys();
if (keys == null || keys.length == 0) continue;
sources.put(pack.getName(), data);
if (hostDimension == null) {
File[] dimFiles = dimensionsDir.listFiles((f) -> f.isFile() && f.getName().endsWith(".json"));
if (dimFiles != null && dimFiles.length > 0) {
String loadKey = dimFiles[0].getName().replaceFirst("\\.json$", "");
IrisDimension loaded = data.getDimensionLoader().load(loadKey);
if (loaded != null) {
hostDimension = loaded;
}
}
}
}
}
}
if (hostDimension == null || sources.isEmpty()) {
commandSender.sendMessage(C.RED + "No packs with objects were found on this server.");
return;
}
int totalObjects = 0;
for (IrisData d : sources.values()) {
String[] k = d.getObjectLoader().getPossibleKeys();
if (k != null) totalObjects += k.length;
}
if (totalObjects == 0) {
commandSender.sendMessage(C.RED + "No objects to place across the selected pack(s).");
return;
}
hostDimension.setStudioMode(StudioMode.OBJECT_BUFFET);
ObjectStudioActivation.activate(hostDimension.getLoadKey());
ObjectStudioActivation.setSources(hostDimension.getLoadKey(), sources);
String scope = dimension == null
? ("all packs [" + sources.size() + "]")
: ("\"" + hostDimension.getName() + "\"");
commandSender.sendMessage(C.GREEN + "Opening Object Studio for " + scope + " ("
+ totalObjects + " objects)");
IrisDimension finalHost = hostDimension;
try {
Iris.service(StudioSVC.class).open(commandSender, seed, hostDimension.getLoadKey(), world -> {
if (world == null) return;
try {
WorldRuntimeControlService.get().applyObjectStudioWorldRules(world);
} catch (Throwable e) {
Iris.reportError("Failed to apply object studio world rules for " + world.getName(), e);
}
if (commandSender.isPlayer()) {
Player p = commandSender.player();
if (p != null) {
Location target = new Location(world, 0.5D, 66D, 0.5D);
J.runEntity(p, () -> {
PaperLib.teleportAsync(p, target).thenRun(() -> p.setGameMode(GameMode.CREATIVE));
});
}
}
});
} catch (Throwable e) {
Iris.reportError("Failed to open object studio world \"" + finalHost.getLoadKey() + "\".", e);
commandSender.sendMessage(C.RED + "Failed to open object studio: " + e.getMessage());
}
}
@Director(description = "Open VSCode for a dimension", aliases = {"vsc", "edit"})
public void vscode(
@Param(defaultValue = "default", description = "The dimension to open VSCode for", aliases = "dim", customHandler = DimensionHandler.class)
@@ -283,42 +191,23 @@ public class CommandStudio implements DirectorExecutor {
sender().sendMessage(C.GREEN + "The \"" + dimension.getName() + "\" pack has version: " + dimension.getVersion());
}
@Director(description = "Open the noise explorer (External GUI)", aliases = {"nmap", "n"})
public void noise() {
if (noGUI()) return;
sender().sendMessage(C.GREEN + "Opening Noise Explorer!");
NoiseExplorerGUI.launch();
}
@Director(description = "Charges all spawners in the area", aliases = "zzt", origin = DirectorOrigin.PLAYER)
public void charge() {
if (!IrisToolbelt.isIrisWorld(world())) {
sender().sendMessage(C.RED + "You must be in an Iris world to charge spawners!");
return;
}
sender().sendMessage(C.GREEN + "Charging spawners!");
engine().getWorldManager().chargeEnergy();
}
@Director(description = "Preview noise gens (External GUI)", aliases = {"generator", "gen"})
public void explore(
@Param(description = "The generator to explore", contextual = true)
@Director(description = "Open the noise explorer (External GUI)", aliases = {"nmap", "n", "generator", "gen"})
public void noise(
@Param(description = "Optional pack generator to preview", defaultValue = "null", contextual = true)
IrisGenerator generator,
@Param(description = "The seed to generate with", defaultValue = "12345")
@Param(description = "The seed to preview the generator with", defaultValue = "12345")
long seed
) {
if (noGUI()) return;
sender().sendMessage(C.GREEN + "Opening Noise Explorer!");
Supplier<Function2<Double, Double, Double>> l = () -> {
if (generator == null) {
NoiseExplorerGUI.launch();
return;
}
if (generator == null) {
return (x, z) -> 0D;
}
return (x, z) -> generator.getHeight(x, z, new RNG(seed).nextParallelRNG(3245).lmax());
};
NoiseExplorerGUI.launch(l, "Custom Generator");
Supplier<Function2<Double, Double, Double>> supplier = () -> (x, z) -> generator.getHeight(x, z, new RNG(seed).nextParallelRNG(3245).lmax());
NoiseExplorerGUI.launch(supplier, "Custom Generator");
}
@Director(description = "Show loot if a chest were right here", origin = DirectorOrigin.PLAYER, sync = true)
@@ -636,50 +525,6 @@ public class CommandStudio implements DirectorExecutor {
sender().sendMessage(C.GREEN + "Done! " + report.getPath());
}
@Director(description = "List pack noise generators as pack/generator", aliases = {"pack-noise", "packnoises"})
public void packnoise() {
LinkedHashSet<File> packFolders = new LinkedHashSet<>();
File packsFolder = Iris.instance.getDataFolder("packs");
File[] children = packsFolder.listFiles();
if (children != null) {
for (File child : children) {
if (child != null && child.isDirectory()) {
packFolders.add(child);
}
}
}
StudioSVC studioService = Iris.service(StudioSVC.class);
if (studioService != null && studioService.isProjectOpen()) {
IrisProject activeProject = studioService.getActiveProject();
if (activeProject != null && activeProject.getPath() != null && activeProject.getPath().isDirectory()) {
packFolders.add(activeProject.getPath());
}
}
ArrayList<String> entries = new ArrayList<>();
for (File packFolder : packFolders) {
IrisData packData = IrisData.get(packFolder);
String packName = packFolder.getName();
String[] keys = packData.getGeneratorLoader().getPossibleKeys();
for (String key : keys) {
entries.add(packName + "/" + key);
}
}
if (entries.isEmpty()) {
sender().sendMessage(C.YELLOW + "No pack noise generators were found.");
return;
}
Collections.sort(entries);
sender().sendMessage(C.GREEN + "Pack noise generators: " + C.GOLD + entries.size());
for (String entry : entries) {
sender().sendMessage(C.GRAY + entry);
}
}
private PlatformChunkGenerator resolveProfileGenerator(IrisDimension dimension) {
StudioSVC studioService = Iris.service(StudioSVC.class);
if (studioService != null && studioService.isProjectOpen()) {
@@ -27,6 +27,7 @@ import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.util.common.data.B;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.director.DirectorHelp;
import art.arcane.volmlib.util.director.DirectorOrigin;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
@@ -45,6 +46,11 @@ import java.util.concurrent.atomic.AtomicInteger;
@Director(name = "what", origin = DirectorOrigin.PLAYER, studio = true, description = "Iris What?")
public class CommandWhat implements DirectorExecutor {
@Director(description = "Show help tree for this command group", aliases = {"?"})
public void help() {
DirectorHelp.print(sender(), getClass());
}
@Director(description = "What is in my hand?", origin = DirectorOrigin.PLAYER)
public void hand() {
try {
@@ -124,6 +124,9 @@ public class DustRevealer {
}
private boolean is(BlockPosition a) {
if (a.getY() < world.getMinHeight() || a.getY() >= world.getMaxHeight()) {
return false;
}
int betterY = a.getY() - world.getMinHeight();
if (isValidTry(a) && engine.getObjectPlacementKey(a.getX(), betterY, a.getZ()) != null && engine.getObjectPlacementKey(a.getX(), betterY, a.getZ()).equals(key)) {
hits.add(a);
@@ -144,6 +144,14 @@ public class ImageResourceLoader extends ResourceLoader<IrisImage> {
}
public File findFile(String name) {
if (name == null || name.trim().isEmpty()) {
return null;
}
if (name.equals("null")) {
Iris.warn("Refusing " + resourceTypeName + " lookup for literal string \"null\" (called by " + callerHint() + ")");
return null;
}
for (File i : getFolders(name)) {
for (File j : i.listFiles()) {
if (j.isFile() && j.getName().endsWith(".png") && j.getName().split("\\Q.\\E")[0].equals(name)) {
@@ -158,7 +166,7 @@ public class ImageResourceLoader extends ResourceLoader<IrisImage> {
}
}
Iris.warn("Couldn't find " + resourceTypeName + ": " + name);
Iris.warn("Couldn't find " + resourceTypeName + ": " + name + " (called by " + callerHint() + ")");
return null;
}
@@ -182,12 +190,19 @@ public class ImageResourceLoader extends ResourceLoader<IrisImage> {
}
}
Iris.warn("Couldn't find " + resourceTypeName + ": " + name);
Iris.warn("Couldn't find " + resourceTypeName + ": " + name + " (called by " + callerHint() + ")");
return null;
}
public IrisImage load(String name, boolean warn) {
if (name == null || name.trim().isEmpty()) {
return null;
}
if (name.equals("null") && warn) {
Iris.warn("Refusing " + resourceTypeName + " load for literal string \"null\" (called by " + callerHint() + ")");
return null;
}
return loadCache.get(name);
}
}
@@ -0,0 +1,203 @@
package art.arcane.iris.core.loader;
import art.arcane.iris.Iris;
import art.arcane.iris.util.common.format.C;
import art.arcane.volmlib.util.json.JSONObject;
import com.google.gson.annotations.SerializedName;
import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
final class JsonSchemaValidator {
private static final ConcurrentHashMap<Class<?>, Set<String>> FIELD_CACHE = new ConcurrentHashMap<>();
private static final int SUGGESTION_MAX_DISTANCE = 4;
private JsonSchemaValidator() {
}
static void validateTopLevelKeys(JSONObject parsed, String rawText, File file, String resourceTypeName, Class<?> objectClass) {
if (parsed == null || objectClass == null) {
return;
}
Set<String> known = FIELD_CACHE.computeIfAbsent(objectClass, JsonSchemaValidator::collectFieldNames);
for (String key : parsed.keySet()) {
if (known.contains(key)) {
continue;
}
reportUnknownKey(key, rawText, file, resourceTypeName, known);
}
}
static void reportLoadFailure(File file, String rawText, String resourceTypeName, Throwable error) {
String message = error.getMessage();
if (message == null || message.isBlank()) {
message = error.getClass().getSimpleName();
}
int line = extractLineFromMessage(message);
String location = file.getPath();
if (line > 0) {
location = location + ":" + line;
}
StringBuilder out = new StringBuilder();
out.append("Couldn't load ").append(resourceTypeName)
.append(C.RED).append(" in ").append(C.WHITE).append(location).append(C.RED)
.append(" -> ").append(message);
String snippet = buildSnippet(rawText, line);
if (snippet != null) {
out.append('\n').append(snippet);
}
Iris.warn(out.toString());
}
private static void reportUnknownKey(String key, String rawText, File file, String resourceTypeName, Set<String> known) {
int line = findLineForKey(rawText, key);
String suggestion = closestMatch(key, known);
StringBuilder out = new StringBuilder();
out.append("Unknown ").append(resourceTypeName).append(" field ")
.append(C.WHITE).append('"').append(key).append('"').append(C.YELLOW)
.append(" in ").append(C.WHITE).append(file.getPath());
if (line > 0) {
out.append(":").append(line);
}
out.append(C.YELLOW).append(" (Gson will silently ignore this)");
if (suggestion != null) {
out.append(". Did you mean ").append(C.WHITE).append('"').append(suggestion).append('"').append(C.YELLOW).append("?");
}
String snippet = buildSnippet(rawText, line);
if (snippet != null) {
out.append('\n').append(snippet);
}
Iris.warn(out.toString());
}
private static Set<String> collectFieldNames(Class<?> cls) {
Set<String> names = new LinkedHashSet<>();
Class<?> c = cls;
while (c != null && c != Object.class) {
for (Field field : c.getDeclaredFields()) {
int mods = field.getModifiers();
if (Modifier.isStatic(mods) || Modifier.isTransient(mods)) {
continue;
}
if (field.isSynthetic()) {
continue;
}
SerializedName serialized = field.getAnnotation(SerializedName.class);
if (serialized != null) {
names.add(serialized.value());
Collections.addAll(names, serialized.alternate());
} else {
names.add(field.getName());
}
}
c = c.getSuperclass();
}
return Collections.unmodifiableSet(names);
}
private static int findLineForKey(String rawText, String key) {
if (rawText == null || key == null) {
return -1;
}
Pattern pattern = Pattern.compile("\"" + Pattern.quote(key) + "\"\\s*:");
Matcher matcher = pattern.matcher(rawText);
if (!matcher.find()) {
return -1;
}
int index = matcher.start();
int line = 1;
for (int i = 0; i < index; i++) {
if (rawText.charAt(i) == '\n') {
line++;
}
}
return line;
}
private static int extractLineFromMessage(String message) {
if (message == null) {
return -1;
}
Matcher m = Pattern.compile("line\\s+(\\d+)").matcher(message);
if (m.find()) {
try {
return Integer.parseInt(m.group(1));
} catch (NumberFormatException ignored) {
}
}
return -1;
}
private static String buildSnippet(String rawText, int line) {
if (rawText == null || line <= 0) {
return null;
}
String[] lines = rawText.split("\n", -1);
if (line > lines.length) {
return null;
}
int from = Math.max(0, line - 2);
int to = Math.min(lines.length, line + 1);
StringBuilder out = new StringBuilder();
int width = String.valueOf(to).length();
for (int i = from; i < to; i++) {
int n = i + 1;
boolean focus = n == line;
out.append(focus ? C.RED + "> " : C.GRAY + " ");
out.append(String.format("%" + width + "d", n)).append(" | ");
String content = lines[i];
if (content.length() > 200) {
content = content.substring(0, 200) + "...";
}
out.append(content);
if (i < to - 1) {
out.append('\n');
}
}
return out.toString();
}
private static String closestMatch(String key, Set<String> known) {
String lowerKey = key.toLowerCase();
String best = null;
int bestDistance = Integer.MAX_VALUE;
for (String candidate : known) {
int d = levenshtein(lowerKey, candidate.toLowerCase());
if (d < bestDistance) {
bestDistance = d;
best = candidate;
}
}
if (best == null) {
return null;
}
int threshold = Math.min(SUGGESTION_MAX_DISTANCE, Math.max(1, key.length() / 2));
return bestDistance <= threshold ? best : null;
}
private static int levenshtein(String a, String b) {
int[] prev = new int[b.length() + 1];
int[] curr = new int[b.length() + 1];
for (int j = 0; j <= b.length(); j++) {
prev[j] = j;
}
for (int i = 1; i <= a.length(); i++) {
curr[0] = i;
for (int j = 1; j <= b.length(); j++) {
int cost = a.charAt(i - 1) == b.charAt(j - 1) ? 0 : 1;
curr[j] = Math.min(Math.min(curr[j - 1] + 1, prev[j] + 1), prev[j - 1] + cost);
}
int[] tmp = prev;
prev = curr;
curr = tmp;
}
return prev[b.length()];
}
}
@@ -154,6 +154,14 @@ public class MatterObjectResourceLoader extends ResourceLoader<IrisMatterObject>
// }
public File findFile(String name) {
if (name == null || name.trim().isEmpty()) {
return null;
}
if (name.equals("null")) {
Iris.warn("Refusing " + resourceTypeName + " lookup for literal string \"null\" (called by " + callerHint() + ")");
return null;
}
for (File i : getFolders(name)) {
for (File j : i.listFiles()) {
if (j.isFile() && j.getName().endsWith(".mat") && j.getName().split("\\Q.\\E")[0].equals(name)) {
@@ -168,7 +176,7 @@ public class MatterObjectResourceLoader extends ResourceLoader<IrisMatterObject>
}
}
Iris.warn("Couldn't find " + resourceTypeName + ": " + name);
Iris.warn("Couldn't find " + resourceTypeName + ": " + name + " (called by " + callerHint() + ")");
return null;
}
@@ -192,12 +200,19 @@ public class MatterObjectResourceLoader extends ResourceLoader<IrisMatterObject>
}
}
Iris.warn("Couldn't find " + resourceTypeName + ": " + name);
Iris.warn("Couldn't find " + resourceTypeName + ": " + name + " (called by " + callerHint() + ")");
return null;
}
public IrisMatterObject load(String name, boolean warn) {
if (name == null || name.trim().isEmpty()) {
return null;
}
if (name.equals("null") && warn) {
Iris.warn("Refusing " + resourceTypeName + " load for literal string \"null\" (called by " + callerHint() + ")");
return null;
}
return loadCache.get(name);
}
}
@@ -123,6 +123,14 @@ public class ObjectResourceLoader extends ResourceLoader<IrisObject> {
}
public File findFile(String name) {
if (name == null || name.trim().isEmpty()) {
return null;
}
if (name.equals("null")) {
Iris.warn("Refusing " + resourceTypeName + " lookup for literal string \"null\" (called by " + callerHint() + ")");
return null;
}
for (File i : getFolders(name)) {
for (File j : i.listFiles()) {
if (j.isFile() && j.getName().endsWith(".iob") && j.getName().split("\\Q.\\E")[0].equals(name)) {
@@ -137,7 +145,7 @@ public class ObjectResourceLoader extends ResourceLoader<IrisObject> {
}
}
Iris.warn("Couldn't find " + resourceTypeName + ": " + name);
Iris.warn("Couldn't find " + resourceTypeName + ": " + name + " (called by " + callerHint() + ")");
return null;
}
@@ -161,12 +169,21 @@ public class ObjectResourceLoader extends ResourceLoader<IrisObject> {
}
}
Iris.warn("Couldn't find " + resourceTypeName + ": " + name);
return null;
}
public IrisObject load(String name, boolean warn) {
return loadCache.get(name);
if (name == null || name.trim().isEmpty()) {
return null;
}
if (name.equals("null") && warn) {
Iris.warn("Refusing " + resourceTypeName + " load for literal string \"null\" (called by " + callerHint() + ")");
return null;
}
IrisObject result = loadCache.get(name);
if (result == null && warn) {
Iris.warn("Couldn't find " + resourceTypeName + ": " + name + " (called by " + callerHint() + ")");
}
return result;
}
}
@@ -130,6 +130,14 @@ public class ResourceLoader<T extends IrisRegistrant> implements MeteredCache {
}
public File findFile(String name) {
if (name == null || name.trim().isEmpty()) {
return null;
}
if (name.equals("null")) {
Iris.warn("Refusing " + resourceTypeName + " lookup for literal string \"null\" (called by " + callerHint() + ")");
return null;
}
for (File i : getFolders(name)) {
for (File j : i.listFiles()) {
if (j.isFile() && j.getName().endsWith(".json") && j.getName().split("\\Q.\\E")[0].equals(name)) {
@@ -144,11 +152,34 @@ public class ResourceLoader<T extends IrisRegistrant> implements MeteredCache {
}
}
Iris.warn("Couldn't find " + resourceTypeName + ": " + name);
Iris.warn("Couldn't find " + resourceTypeName + ": " + name + " (called by " + callerHint() + ")");
return null;
}
protected static String describeName(String name) {
if (name == null) return "<java null>";
if (name.isEmpty()) return "<empty string>";
if (name.equals("null")) return "\"null\" (literal string)";
return "\"" + name + "\"";
}
protected static String callerHint() {
StackWalker walker = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE);
return walker.walk(frames -> frames
.filter(f -> {
String cn = f.getClassName();
return !cn.startsWith("art.arcane.iris.core.loader.")
&& !cn.startsWith("art.arcane.volmlib.util.data.")
&& !cn.startsWith("com.github.benmanes.caffeine.");
})
.limit(3)
.map(f -> f.getClassName().substring(f.getClassName().lastIndexOf('.') + 1)
+ "." + f.getMethodName() + ":" + f.getLineNumber())
.reduce((a, b) -> a + " <- " + b)
.orElse("<unknown>"));
}
public void logLoad(File path, T t) {
loads.getAndIncrement();
@@ -167,7 +198,11 @@ public class ResourceLoader<T extends IrisRegistrant> implements MeteredCache {
}
public void failLoad(File path, Throwable e) {
J.a(() -> Iris.warn("Couldn't Load " + resourceTypeName + " file: " + path.getPath() + ": " + e.getMessage()));
failLoad(path, null, e);
}
public void failLoad(File path, String rawText, Throwable e) {
J.a(() -> JsonSchemaValidator.reportLoadFailure(path, rawText, resourceTypeName, e));
}
private KList<File> matchAllFiles(File root, Predicate<File> f) {
@@ -241,10 +276,14 @@ public class ResourceLoader<T extends IrisRegistrant> implements MeteredCache {
}
protected T loadFile(File j, String name) {
String rawText = null;
try {
PrecisionStopwatch p = PrecisionStopwatch.start();
rawText = IO.readAll(j);
JSONObject parsed = new JSONObject(rawText);
JsonSchemaValidator.validateTopLevelKeys(parsed, rawText, j, resourceTypeName, objectClass);
T t = getManager().getGson()
.fromJson(preprocess(new JSONObject(IO.readAll(j))).toString(0), objectClass);
.fromJson(preprocess(parsed).toString(0), objectClass);
t.setLoadKey(name);
t.setLoadFile(j);
t.setLoader(manager);
@@ -254,7 +293,7 @@ public class ResourceLoader<T extends IrisRegistrant> implements MeteredCache {
return t;
} catch (Throwable e) {
Iris.reportError(e);
failLoad(j, e);
failLoad(j, rawText, e);
return null;
}
}
@@ -358,11 +397,11 @@ public class ResourceLoader<T extends IrisRegistrant> implements MeteredCache {
}
public T load(String name, boolean warn) {
if (name == null) {
if (name == null || name.trim().isEmpty()) {
return null;
}
if (name.trim().isEmpty()) {
if (name.equals("null") && warn) {
Iris.warn("Refusing " + resourceTypeName + " load for literal string \"null\" (called by " + callerHint() + ")");
return null;
}
@@ -24,7 +24,6 @@ import art.arcane.iris.core.lifecycle.WorldLifecycleRequest;
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
import art.arcane.iris.core.nms.container.BiomeColor;
import art.arcane.iris.core.nms.container.BlockProperty;
import art.arcane.iris.core.nms.container.StructurePlacement;
import art.arcane.iris.core.nms.datapack.DataVersion;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.iris.engine.framework.Engine;
@@ -155,12 +154,6 @@ public interface INMSBinding {
return 441;
}
KList<String> getStructureKeys();
default KMap<String, KList<String>> getVanillaStructureBiomeTags() {
return new KMap<>();
}
boolean missingDimensionTypes(String... keys);
default boolean injectBukkit() {
@@ -169,10 +162,6 @@ public interface INMSBinding {
KMap<Material, List<BlockProperty>> getBlockProperties();
void placeStructures(Chunk chunk);
KMap<Identifier, StructurePlacement> collectStructures();
default Map<String, byte[]> extractVanillaDatapack() {
return Map.of();
}
@@ -1,81 +0,0 @@
package art.arcane.iris.core.nms.container;
import com.google.gson.JsonObject;
import lombok.*;
import lombok.experimental.Accessors;
import lombok.experimental.SuperBuilder;
import org.apache.commons.math3.fraction.Fraction;
import java.util.List;
@Data
@SuperBuilder
@Accessors(fluent = true, chain = true)
public abstract class StructurePlacement {
private final int salt;
private final float frequency;
private final List<Structure> structures;
public abstract JsonObject toJson(String structure);
protected JsonObject createBase(String structure) {
JsonObject object = new JsonObject();
object.addProperty("structure", structure);
object.addProperty("salt", salt);
return object;
}
public int frequencyToSpacing() {
var frac = new Fraction(Math.max(Math.min(frequency, 1), 0.000000001f));
return (int) Math.round(Math.sqrt((double) frac.getDenominator() / frac.getNumerator()));
}
public enum SpreadType {
LINEAR,
TRIANGULAR
}
@Getter
@Accessors(chain = true, fluent = true)
@EqualsAndHashCode(callSuper = true)
@SuperBuilder
public static class RandomSpread extends StructurePlacement {
private final int spacing;
private final int separation;
private final SpreadType spreadType;
@Override
public JsonObject toJson(String structure) {
JsonObject object = createBase(structure);
object.addProperty("spacing", Math.max(spacing, frequencyToSpacing()));
object.addProperty("separation", separation);
object.addProperty("spreadType", spreadType.name());
return object;
}
}
@Getter
@EqualsAndHashCode(callSuper = true)
@SuperBuilder
public static class ConcentricRings extends StructurePlacement {
private final int distance;
private final int spread;
private final int count;
@Override
public JsonObject toJson(String structure) {
return null;
}
}
public record Structure(
int weight,
String key,
List<String> tags
) {
public boolean isValid() {
return weight > 0 && key != null;
}
}
}
@@ -19,13 +19,11 @@
package art.arcane.iris.core.nms.v1X;
import art.arcane.iris.Iris;
import art.arcane.iris.core.link.Identifier;
import art.arcane.iris.core.nms.INMSBinding;
import art.arcane.iris.core.nms.container.BiomeColor;
import art.arcane.iris.core.nms.container.BlockProperty;
import art.arcane.iris.core.nms.datapack.DataVersion;
import art.arcane.iris.core.nms.container.Pair;
import art.arcane.iris.core.nms.container.StructurePlacement;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
@@ -40,7 +38,6 @@ import org.bukkit.block.Biome;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.event.entity.CreatureSpawnEvent;
import org.bukkit.generator.structure.Structure;
import org.bukkit.inventory.ItemStack;
import java.awt.Color;
@@ -115,15 +112,6 @@ public class NMSBinding1X implements INMSBinding {
return Color.GREEN;
}
@Override
public KList<String> getStructureKeys() {
List<String> list = StreamSupport.stream(Registry.STRUCTURE.spliterator(), false)
.map(Structure::getKeyOrThrow)
.map(NamespacedKey::toString)
.toList();
return new KList<>(list);
}
@Override
public boolean missingDimensionTypes(String... keys) {
return false;
@@ -138,16 +126,6 @@ public class NMSBinding1X implements INMSBinding {
return map;
}
@Override
public void placeStructures(Chunk chunk) {
}
@Override
public KMap<Identifier, StructurePlacement> collectStructures() {
return new KMap<>();
}
@Override
public CompoundTag serializeEntity(Entity location) {
return null;
@@ -42,6 +42,9 @@ public final class PackValidator {
private static final String TRASH_ROOT = ".iris-trash";
private static final String DATAPACK_IMPORTS = "datapack-imports";
private static final String EXTERNAL_DATAPACKS = "externaldatapacks";
private static final String INTERNAL_DATAPACKS = "internaldatapacks";
private static final String DATAPACKS_FOLDER = "datapacks";
private static final String CACHE_FOLDER = "cache";
private static final String OBJECTS_FOLDER = "objects";
private static final String DIMENSIONS_FOLDER = "dimensions";
private static final List<String> MANAGED_RESOURCE_FOLDERS = List.of(
@@ -209,6 +212,15 @@ public final class PackValidator {
if (str.contains("/" + EXTERNAL_DATAPACKS + "/")) {
return false;
}
if (str.contains("/" + INTERNAL_DATAPACKS + "/")) {
return false;
}
if (str.contains("/" + DATAPACKS_FOLDER + "/")) {
return false;
}
if (str.contains("/" + CACHE_FOLDER + "/")) {
return false;
}
if (str.contains("/" + OBJECTS_FOLDER + "/")) {
return false;
}
@@ -322,6 +322,18 @@ public class SchemaBuilder {
fancyType = "Enchantment Type";
prop.put("$ref", "#/definitions/" + key);
description.add(SYMBOL_TYPE__N + " Must be a valid Enchantment Type (use ctrl+space for auto complete!)");
} else if (k.isAnnotationPresent(RegistryListPotionEffect.class)) {
String key = "enum-potion-effect-type";
if (!definitions.containsKey(key)) {
JSONObject j = new JSONObject();
j.put("enum", POTION_TYPES);
definitions.put(key, j);
}
fancyType = "Potion Effect Type";
prop.put("$ref", "#/definitions/" + key);
description.add(SYMBOL_TYPE__N + " Must be a valid Potion Effect Type (use ctrl+space for auto complete!)");
} else if (k.isAnnotationPresent(RegistryListFunction.class)) {
var functionClass = k.getDeclaredAnnotation(RegistryListFunction.class).value();
try {
@@ -537,6 +549,20 @@ public class SchemaBuilder {
items.put("$ref", "#/definitions/" + key);
prop.put("items", items);
description.add(SYMBOL_TYPE__N + " Must be a valid Enchantment Type (use ctrl+space for auto complete!)");
} else if (k.isAnnotationPresent(RegistryListPotionEffect.class)) {
fancyType = "List of Potion Effect Types";
String key = "enum-potion-effect-type";
if (!definitions.containsKey(key)) {
JSONObject j = new JSONObject();
j.put("enum", POTION_TYPES);
definitions.put(key, j);
}
JSONObject items = new JSONObject();
items.put("$ref", "#/definitions/" + key);
prop.put("items", items);
description.add(SYMBOL_TYPE__N + " Must be a valid Potion Effect Type (use ctrl+space for auto complete!)");
} else if (k.isAnnotationPresent(RegistryListFunction.class)) {
var functionClass = k.getDeclaredAnnotation(RegistryListFunction.class).value();
try {
@@ -1,97 +0,0 @@
package art.arcane.iris.core.runtime;
import com.google.gson.GsonBuilder;
import art.arcane.iris.core.ServerConfigurator;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import lombok.Data;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
@Data
public final class DatapackReadinessResult {
private final String requestedPackKey;
private final List<String> resolvedDatapackFolders;
private final String externalDatapackInstallResult;
private final boolean verificationPassed;
private final List<String> verifiedPaths;
private final List<String> missingPaths;
private final boolean restartRequired;
public String toJson() {
return new GsonBuilder().setPrettyPrinting().create().toJson(this);
}
public static DatapackReadinessResult installForStudioWorld(
String requestedPackKey,
String dimensionTypeKey,
File worldFolder,
boolean verifyDataPacks,
boolean includeExternalDataPacks,
KMap<String, KList<File>> extraWorldDatapackFoldersByPack
) {
ArrayList<String> resolvedFolders = new ArrayList<>();
File datapacksFolder = ServerConfigurator.resolveDatapacksFolder(worldFolder);
resolvedFolders.add(datapacksFolder.getAbsolutePath());
if (extraWorldDatapackFoldersByPack != null) {
KList<File> extraFolders = extraWorldDatapackFoldersByPack.get(requestedPackKey);
if (extraFolders != null) {
for (File extraFolder : extraFolders) {
if (extraFolder == null) {
continue;
}
String path = extraFolder.getAbsolutePath();
if (!resolvedFolders.contains(path)) {
resolvedFolders.add(path);
}
}
}
}
String externalResult = "ok";
boolean restartRequired = false;
try {
restartRequired = ServerConfigurator.installDataPacks(verifyDataPacks, includeExternalDataPacks, extraWorldDatapackFoldersByPack);
} catch (Throwable e) {
externalResult = e.getClass().getSimpleName() + ": " + String.valueOf(e.getMessage());
}
ArrayList<String> verifiedPaths = new ArrayList<>();
ArrayList<String> missingPaths = new ArrayList<>();
String verificationDimensionTypeKey = (dimensionTypeKey == null || dimensionTypeKey.isBlank())
? requestedPackKey
: dimensionTypeKey;
for (String folderPath : resolvedFolders) {
File folder = new File(folderPath);
collectVerificationPaths(folder, verificationDimensionTypeKey, verifiedPaths, missingPaths);
}
boolean verificationPassed = missingPaths.isEmpty() && "ok".equals(externalResult);
return new DatapackReadinessResult(
requestedPackKey,
List.copyOf(resolvedFolders),
externalResult,
verificationPassed,
List.copyOf(verifiedPaths),
List.copyOf(missingPaths),
restartRequired
);
}
static void collectVerificationPaths(File folder, String dimensionTypeKey, List<String> verifiedPaths, List<String> missingPaths) {
File packMeta = new File(folder, "iris/pack.mcmeta");
File dimensionType = new File(folder, "iris/data/iris/dimension_type/" + dimensionTypeKey + ".json");
if (packMeta.exists()) {
verifiedPaths.add(packMeta.getAbsolutePath());
} else {
missingPaths.add(packMeta.getAbsolutePath());
}
if (dimensionType.exists()) {
verifiedPaths.add(dimensionType.getAbsolutePath());
} else {
missingPaths.add(dimensionType.getAbsolutePath());
}
}
}
@@ -153,7 +153,7 @@ public final class StudioOpenCoordinator {
request.onDone().accept(world);
}
future.complete(new StudioOpenResult(world, safeEntry, creator.getLastDatapackReadinessResult()));
future.complete(new StudioOpenResult(world, safeEntry));
} catch (Throwable e) {
Iris.reportError("Studio open failed for world \"" + request.worldName() + "\".", e);
if (!request.retainOnFailure()) {
@@ -544,7 +544,7 @@ public final class StudioOpenCoordinator {
public record StudioOpenProgress(double progress, String stage) {
}
public record StudioOpenResult(World world, Location entryLocation, DatapackReadinessResult datapackReadiness) {
public record StudioOpenResult(World world, Location entryLocation) {
}
public record StudioCloseResult(
@@ -64,6 +64,15 @@ public class IrisEngineSVC implements IrisService {
@Override
public void onDisable() {
for (World world : worlds.keySet()) {
PlatformChunkGenerator gen = IrisToolbelt.access(world);
if (gen == null) continue;
try {
gen.close();
} catch (Throwable t) {
Iris.reportError(t);
}
}
if (service != null) {
service.shutdown();
}
@@ -26,6 +26,7 @@ import art.arcane.iris.core.runtime.ObjectStudioLayout.GridCell;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.platform.studio.generators.ObjectStudioGenerator;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.plugin.IrisService;
import art.arcane.iris.util.common.scheduling.J;
import io.papermc.lib.PaperLib;
@@ -41,22 +42,18 @@ import org.bukkit.event.entity.CreatureSpawnEvent;
import org.bukkit.event.entity.EntitySpawnEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.world.WorldUnloadEvent;
import org.bukkit.inventory.EquipmentSlot;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
public class ObjectStudioSaveService implements IrisService {
public static final int INTERVAL_TICKS = 100;
private static final int CELLS_PER_PASS = 50;
private static ObjectStudioSaveService INSTANCE;
private final Map<UUID, ActiveStudio> studios = new ConcurrentHashMap<>();
private int taskId = -1;
public static ObjectStudioSaveService get() {
ObjectStudioSaveService svc = INSTANCE;
@@ -68,15 +65,10 @@ public class ObjectStudioSaveService implements IrisService {
@Override
public void onEnable() {
INSTANCE = this;
taskId = J.ar(this::pass, INTERVAL_TICKS);
}
@Override
public void onDisable() {
if (taskId != -1) {
J.car(taskId);
taskId = -1;
}
studios.clear();
INSTANCE = null;
}
@@ -154,7 +146,9 @@ public class ObjectStudioSaveService implements IrisService {
@EventHandler(ignoreCancelled = true)
public void onPlayerInteract(PlayerInteractEvent event) {
if (event.getAction() != Action.RIGHT_CLICK_BLOCK && event.getAction() != Action.LEFT_CLICK_BLOCK) return;
if (event.getHand() != EquipmentSlot.HAND) return;
Action action = event.getAction();
if (action != Action.RIGHT_CLICK_BLOCK && action != Action.LEFT_CLICK_BLOCK) return;
Block clicked = event.getClickedBlock();
if (clicked == null) return;
@@ -162,20 +156,46 @@ public class ObjectStudioSaveService implements IrisService {
ActiveStudio studio = studios.get(world.getUID());
if (studio == null) return;
GridCell cell = studio.layout.findAt(clicked.getX(), clicked.getZ());
if (cell == null) return;
Player player = event.getPlayer();
Iris.info("Object Studio save triggered by %s for %s", player.getName(), cell.key());
GridCell cell = findCellNear(studio, clicked.getX(), clicked.getZ());
if (cell == null) {
player.sendMessage(C.GRAY + "Object Studio: no cell under click (x=" + clicked.getX() + " z=" + clicked.getZ() + ").");
return;
}
player.sendMessage(C.AQUA + "Object Studio: saving " + C.WHITE + cell.pack() + "/" + cell.key() + C.GRAY + " (" + cell.w() + "x" + cell.h() + "x" + cell.d() + ")");
Iris.info("Object Studio save triggered by %s for %s/%s", player.getName(), cell.pack(), cell.key());
J.runRegion(world, cell.chunkMinX(), cell.chunkMinZ(), () -> {
try {
captureAndSave(studio, world, cell);
captureAndSave(studio, world, cell, player);
} catch (Throwable e) {
Iris.reportError(e);
}
});
}
private static GridCell findCellNear(ActiveStudio studio, int x, int z) {
GridCell inside = studio.layout.findAt(x, z);
if (inside != null) return inside;
int reach = Math.max(1, studio.layout.padding() + 1);
GridCell best = null;
int bestDist = Integer.MAX_VALUE;
for (GridCell cell : studio.layout.cells()) {
int dx = 0;
if (x < cell.originX()) dx = cell.originX() - x;
else if (x >= cell.originX() + cell.w()) dx = x - (cell.originX() + cell.w() - 1);
int dz = 0;
if (z < cell.originZ()) dz = cell.originZ() - z;
else if (z >= cell.originZ() + cell.d()) dz = z - (cell.originZ() + cell.d() - 1);
int dist = Math.max(dx, dz);
if (dist <= reach && dist < bestDist) {
bestDist = dist;
best = cell;
}
}
return best;
}
public boolean teleportTo(Player player, String objectKey) {
if (player == null || objectKey == null) return false;
for (ActiveStudio studio : studios.values()) {
@@ -206,43 +226,7 @@ public class ObjectStudioSaveService implements IrisService {
return objects;
}
private void pass() {
if (studios.isEmpty()) return;
for (ActiveStudio studio : studios.values()) {
World world = Bukkit.getWorld(studio.worldId);
if (world == null) continue;
int budget = CELLS_PER_PASS;
int size = studio.layout.cells().size();
if (size == 0) continue;
while (budget-- > 0) {
int idx = studio.cursor.getAndIncrement();
if (idx >= size) {
studio.cursor.set(0);
idx = 0;
}
GridCell cell = studio.layout.cells().get(idx);
scheduleCapture(studio, world, cell);
if (size <= CELLS_PER_PASS && idx == size - 1) break;
}
}
}
private void scheduleCapture(ActiveStudio studio, World world, GridCell cell) {
int chunkX = cell.chunkMinX();
int chunkZ = cell.chunkMinZ();
J.runRegion(world, chunkX, chunkZ, () -> {
try {
captureAndSave(studio, world, cell);
} catch (Throwable e) {
Iris.reportError(e);
}
});
}
private void captureAndSave(ActiveStudio studio, World world, GridCell cell) {
private void captureAndSave(ActiveStudio studio, World world, GridCell cell, Player notify) {
if (!allChunksLoaded(world, cell)) {
return;
}
@@ -268,18 +252,29 @@ public class ObjectStudioSaveService implements IrisService {
long hash = hashOf(snapshot);
Long prior = studio.hashes.get(hashKey);
if (prior != null && prior == hash) {
if (notify != null) {
notify.sendMessage(C.GRAY + "Object Studio: no changes for " + cell.pack() + "/" + cell.key() + ".");
}
return;
}
if (!anyBlock && prior == null) {
studio.hashes.put(hashKey, hash);
if (notify != null) {
notify.sendMessage(C.GRAY + "Object Studio: empty cell " + cell.pack() + "/" + cell.key() + " (nothing to write).");
}
return;
}
studio.hashes.put(hashKey, hash);
File targetFile = objectFileFor(studio, cell);
if (targetFile == null) return;
if (targetFile == null) {
if (notify != null) {
notify.sendMessage(C.RED + "Object Studio: no target file for " + cell.pack() + "/" + cell.key() + ".");
}
return;
}
J.a(() -> {
try {
@@ -290,8 +285,14 @@ public class ObjectStudioSaveService implements IrisService {
snapshot.write(targetFile);
Iris.info("Object Studio saved: %s/%s (%dx%dx%d)",
cell.pack(), cell.key(), cell.w(), cell.h(), cell.d());
if (notify != null) {
J.runEntity(notify, () -> notify.sendMessage(C.GREEN + "Object Studio: saved " + C.WHITE + cell.pack() + "/" + cell.key()));
}
} catch (Throwable e) {
Iris.reportError(e);
if (notify != null) {
J.runEntity(notify, () -> notify.sendMessage(C.RED + "Object Studio: save failed for " + cell.pack() + "/" + cell.key() + " (" + e.getMessage() + ")"));
}
}
});
}
@@ -335,7 +336,6 @@ public class ObjectStudioSaveService implements IrisService {
final Map<String, File> objectsDirs;
final String packKey;
final Map<String, Long> hashes = new ConcurrentHashMap<>();
final AtomicInteger cursor = new AtomicInteger();
ActiveStudio(UUID worldId, ObjectStudioLayout layout, Map<String, File> objectsDirs, String packKey) {
this.worldId = worldId;
@@ -74,7 +74,7 @@ public class StudioSVC implements IrisService {
if (pack.equals("overworld")) {
Iris.info("Downloading Default Pack " + pack);
String url = "https://github.com/IrisDimensions/overworld/releases/download/" + INMS.OVERWORLD_TAG + "/overworld.zip";
Iris.service(StudioSVC.class).downloadRelease(Iris.getSender(), url, false, false);
Iris.service(StudioSVC.class).downloadRelease(Iris.getSender(), url, false);
} else {
Iris.warn("Default pack '" + pack + "' is not installed. Please download it manually with /iris download");
}
@@ -201,11 +201,11 @@ public class StudioSVC implements IrisService {
return dim;
}
public void downloadSearch(VolmitSender sender, String key, boolean trim) {
downloadSearch(sender, key, trim, false);
public void downloadSearch(VolmitSender sender, String key) {
downloadSearch(sender, key, false);
}
public void downloadSearch(VolmitSender sender, String key, boolean trim, boolean forceOverwrite) {
public void downloadSearch(VolmitSender sender, String key, boolean forceOverwrite) {
try {
String url = getListing(false).get(key);
@@ -219,7 +219,7 @@ public class StudioSVC implements IrisService {
String[] nodes = url.split("\\Q/\\E");
String repo = nodes.length == 1 ? "IrisDimensions/" + nodes[0] : nodes[0] + "/" + nodes[1];
String branch = nodes.length > 2 ? nodes[2] : "stable";
download(sender, repo, branch, trim, forceOverwrite, false);
download(sender, repo, branch, forceOverwrite, false);
} catch (Throwable e) {
Iris.reportError(e);
e.printStackTrace();
@@ -227,9 +227,9 @@ public class StudioSVC implements IrisService {
}
}
public void downloadRelease(VolmitSender sender, String url, boolean trim, boolean forceOverwrite) {
public void downloadRelease(VolmitSender sender, String url, boolean forceOverwrite) {
try {
download(sender, "IrisDimensions", url, trim, forceOverwrite, true);
download(sender, "IrisDimensions", url, forceOverwrite, true);
} catch (Throwable e) {
Iris.reportError(e);
e.printStackTrace();
@@ -237,14 +237,14 @@ public class StudioSVC implements IrisService {
}
}
public void download(VolmitSender sender, String repo, String branch, boolean trim) throws JsonSyntaxException, IOException {
download(sender, repo, branch, trim, false, false);
public void download(VolmitSender sender, String repo, String branch) throws JsonSyntaxException, IOException {
download(sender, repo, branch, false, false);
}
public void download(VolmitSender sender, String repo, String branch, boolean trim, boolean forceOverwrite, boolean directUrl) throws JsonSyntaxException, IOException {
public void download(VolmitSender sender, String repo, String branch, boolean forceOverwrite, boolean directUrl) throws JsonSyntaxException, IOException {
String url = directUrl ? branch : "https://codeload.github.com/" + repo + "/zip/refs/heads/" + branch;
sender.sendMessage("Downloading " + url + " "); //The extra space stops a bug in adventure API from repeating the last letter of the URL
File zip = Iris.getNonCachedFile("pack-" + trim + "-" + repo, url);
File zip = Iris.getNonCachedFile("pack-" + repo, url);
File temp = Iris.getTemp();
File work = new File(temp, "dl-" + UUID.randomUUID());
File packs = getWorkspaceFolder();
@@ -331,13 +331,6 @@ public class StudioSVC implements IrisService {
FileUtils.copyDirectory(dir, packEntry);
if (trim) {
sender.sendMessage("Trimming " + key);
File cp = compilePackage(sender, key, false, false);
IO.delete(packEntry);
packEntry.mkdirs();
ZipUtil.unpack(cp, packEntry);
}
IrisData.getLoaded(packEntry)
.ifPresent(IrisData::hotloaded);
@@ -32,7 +32,6 @@ import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.pregenerator.PregenTask;
import art.arcane.iris.core.service.BoardSVC;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.core.runtime.DatapackReadinessResult;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
@@ -113,11 +112,6 @@ public class IrisCreator {
*/
private boolean benchmark = false;
private BiConsumer<Double, String> studioProgressConsumer;
private DatapackReadinessResult lastDatapackReadinessResult;
public DatapackReadinessResult getLastDatapackReadinessResult() {
return lastDatapackReadinessResult;
}
public static boolean removeFromBukkitYml(String name) throws IOException {
YamlConfiguration yml = YamlConfiguration.loadConfiguration(BUKKIT_YML);
@@ -191,41 +185,7 @@ public class IrisCreator {
if (!studio()) {
IrisWorlds.get().put(name(), dimension());
}
boolean verifyDataPacks = !studio();
boolean includeExternalDataPacks = true;
KMap<String, KList<File>> extraWorldDatapackFoldersByPack = null;
if (studio()) {
File studioDatapackFolder = new File(new File(Bukkit.getWorldContainer(), name()), "datapacks");
KList<File> studioDatapackFolders = new KList<>();
studioDatapackFolders.add(studioDatapackFolder);
extraWorldDatapackFoldersByPack = new KMap<>();
extraWorldDatapackFoldersByPack.put(d.getLoadKey(), studioDatapackFolders);
}
lastDatapackReadinessResult = DatapackReadinessResult.installForStudioWorld(
d.getLoadKey(),
d.getDimensionTypeKey(),
new File(Bukkit.getWorldContainer(), name()),
verifyDataPacks,
includeExternalDataPacks,
extraWorldDatapackFoldersByPack
);
if (!"ok".equals(lastDatapackReadinessResult.getExternalDatapackInstallResult())) {
throw new IrisException("Datapack external install failed: " + lastDatapackReadinessResult.getExternalDatapackInstallResult());
}
if (lastDatapackReadinessResult.isRestartRequired()) {
throw new IrisException("Datapack install requested a server restart for "
+ d.getLoadKey()
+ ". folders="
+ lastDatapackReadinessResult.getResolvedDatapackFolders());
}
if (!lastDatapackReadinessResult.isVerificationPassed()) {
throw new IrisException("Datapack readiness verification failed for "
+ d.getLoadKey()
+ ". missingPaths="
+ lastDatapackReadinessResult.getMissingPaths()
+ ", folders="
+ lastDatapackReadinessResult.getResolvedDatapackFolders());
}
ServerConfigurator.installDataPacks(!studio());
reportStudioProgress(0.40D, "install_datapacks");
PlatformChunkGenerator access = (PlatformChunkGenerator) wc.generator();
@@ -95,7 +95,7 @@ public class IrisToolbelt {
}
if (!pack.exists()) {
Iris.service(StudioSVC.class).downloadSearch(new VolmitSender(Bukkit.getConsoleSender(), Iris.instance.getTag()), requested, false, false);
Iris.service(StudioSVC.class).downloadSearch(new VolmitSender(Bukkit.getConsoleSender(), Iris.instance.getTag()), requested, false);
File found = findCaseInsensitivePack(packsFolder, requested);
if (found != null) {
pack = found;
@@ -0,0 +1,26 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.tools;
public enum PlausibilizeMode {
DEFAULT,
NORMALIZE,
FOLIAGE_OVERATURE,
SMOKE
}
@@ -67,15 +67,18 @@ public final class TreePlausibilizer {
}
}
public static Result analyze(IrisObject obj, boolean normalize, boolean smoke, int shellRadius) {
return run(obj, false, normalize, smoke, shellRadius);
public static Result analyze(IrisObject obj, PlausibilizeMode mode, int shellRadius) {
return run(obj, false, mode, shellRadius);
}
public static Result apply(IrisObject obj, boolean normalize, boolean smoke, int shellRadius) {
return run(obj, true, normalize, smoke, shellRadius);
public static Result apply(IrisObject obj, PlausibilizeMode mode, int shellRadius) {
return run(obj, true, mode, shellRadius);
}
private static Result run(IrisObject obj, boolean mutate, boolean normalize, boolean smoke, int shellRadius) {
private static Result run(IrisObject obj, boolean mutate, PlausibilizeMode mode, int shellRadius) {
boolean normalize = mode == PlausibilizeMode.NORMALIZE;
boolean smoke = mode == PlausibilizeMode.SMOKE;
boolean foliageOverature = mode == PlausibilizeMode.FOLIAGE_OVERATURE;
VectorMap<BlockData> blocks = obj.getBlocks();
Map<Long, BlockData> positions = new HashMap<>(blocks.size() * 2);
Set<Long> logPositions = new HashSet<>();
@@ -154,6 +157,8 @@ public final class TreePlausibilizer {
Set<Long> unreached;
Map<Long, Integer> distances;
List<LogInsertion> inserts = new ArrayList<>();
Set<Long> orphanRemovals = new HashSet<>();
List<LeafAddition> leafAdds = new ArrayList<>();
if (!leafPositions.isEmpty() && !logPositions.isEmpty()) {
Set<Long> connectivityLeaves;
@@ -168,12 +173,24 @@ public final class TreePlausibilizer {
reachableBefore = countReachable(leafPositions, distances);
unreached = new HashSet<>(leafPositions);
unreached.removeAll(distances.keySet());
Set<Long> frontier = computeInitialFrontier(unreached, logPositions, distances);
logsAdded = bridgeLoop(
unreached, frontier, distances,
if (foliageOverature && !smoke && !unreached.isEmpty()) {
BlockData bridgeLeaf = pickDominantLeaf(leafTypeCounts);
foliageBridge(
unreached, logPositions, distances,
leafPositions, connectivityLeaves, positions,
bridgeLeaf, leafAdds,
minX, minY, minZ, maxX, maxY, maxZ
);
distances = seedDistances(logPositions, connectivityLeaves);
unreached = new HashSet<>(leafPositions);
unreached.removeAll(distances.keySet());
}
logsAdded = tentacleGrow(
unreached, distances,
logPositions, leafPositions, connectivityLeaves, positions,
inserts
inserts, orphanRemovals, !foliageOverature
);
} else {
distances = new HashMap<>();
@@ -181,8 +198,9 @@ public final class TreePlausibilizer {
reachableBefore = 0;
}
int leavesAdded = 0;
List<LeafAddition> leafAdds = new ArrayList<>();
leavesRemoved += orphanRemovals.size();
int leavesAdded = leafAdds.size();
if (smoke) {
BlockData leafTemplate = pickDominantLeaf(leafTypeCounts);
for (long key : leafPositions) {
@@ -213,6 +231,9 @@ public final class TreePlausibilizer {
}
}
}
for (long key : orphanRemovals) {
blocks.remove(unpackKey(key));
}
for (LeafAddition addition : leafAdds) {
blocks.put(unpackKey(addition.key()), addition.data());
}
@@ -288,127 +309,246 @@ public final class TreePlausibilizer {
}
}
private static int bridgeLoop(
private static int tentacleGrow(
Set<Long> unreached,
Set<Long> frontier,
Map<Long, Integer> distances,
Set<Long> logPositions,
Set<Long> leafPositions,
Set<Long> connectivityLeaves,
Map<Long, BlockData> positions,
List<LogInsertion> inserts
List<LogInsertion> inserts,
Set<Long> orphanRemovals,
boolean deleteOrphans
) {
int logsAdded = 0;
int safetyLimit = unreached.size() + 32;
while (!unreached.isEmpty() && logsAdded < safetyLimit) {
long candidateKey = pickInteriorCandidate(frontier, unreached, connectivityLeaves);
BlockData logData = pickLogVariant(candidateKey, positions, logPositions);
inserts.add(new LogInsertion(candidateKey, logData));
int safetyLimit = unreached.size() * 2 + 32;
long currentTarget = -1L;
logPositions.add(candidateKey);
leafPositions.remove(candidateKey);
connectivityLeaves.remove(candidateKey);
distances.remove(candidateKey);
unreached.remove(candidateKey);
frontier.remove(candidateKey);
positions.put(candidateKey, logData);
while (!unreached.isEmpty() && logsAdded < safetyLimit) {
if (currentTarget == -1L || !unreached.contains(currentTarget)) {
currentTarget = unreached.iterator().next();
}
long extensionLeaf = findWoodAdjacentLeafFrom(currentTarget, connectivityLeaves, logPositions);
if (extensionLeaf == -1L) {
if (deleteOrphans) {
removeOrphanCluster(currentTarget, connectivityLeaves, leafPositions, unreached, distances, positions, orphanRemovals);
} else {
skipOrphanCluster(currentTarget, unreached, connectivityLeaves);
}
currentTarget = -1L;
continue;
}
BlockData logData = pickLogVariant(extensionLeaf, positions, logPositions);
inserts.add(new LogInsertion(extensionLeaf, logData));
logPositions.add(extensionLeaf);
leafPositions.remove(extensionLeaf);
connectivityLeaves.remove(extensionLeaf);
distances.remove(extensionLeaf);
unreached.remove(extensionLeaf);
positions.put(extensionLeaf, logData);
logsAdded++;
int[] cx = unpack(candidateKey);
Deque<Long> q = new ArrayDeque<>();
for (int[] n : NEIGHBORS) {
long nk = packXYZ(cx[0] + n[0], cx[1] + n[1], cx[2] + n[2]);
if (connectivityLeaves.contains(nk)) {
Integer cur = distances.get(nk);
if (cur == null || cur > 1) {
if (cur == null) {
unreached.remove(nk);
frontier.remove(nk);
}
distances.put(nk, 1);
q.add(nk);
}
}
if (unreached.contains(nk)) {
frontier.add(nk);
}
}
while (!q.isEmpty()) {
long pos = q.poll();
int d = distances.get(pos);
if (d >= MAX_DISTANCE) {
continue;
}
int[] px = unpack(pos);
for (int[] n : NEIGHBORS) {
long nk = packXYZ(px[0] + n[0], px[1] + n[1], px[2] + n[2]);
if (connectivityLeaves.contains(nk)) {
Integer cur = distances.get(nk);
if (cur == null || cur > d + 1) {
if (cur == null) {
unreached.remove(nk);
frontier.remove(nk);
}
distances.put(nk, d + 1);
q.add(nk);
}
}
if (unreached.contains(nk)) {
frontier.add(nk);
}
}
}
propagateFromLog(extensionLeaf, distances, connectivityLeaves, unreached);
}
return logsAdded;
}
private static long pickInteriorCandidate(
Set<Long> frontier, Set<Long> unreached, Set<Long> connectivityLeaves
) {
Set<Long> pool = !frontier.isEmpty() ? frontier : unreached;
long best = -1L;
int bestScore = -1;
for (long pos : pool) {
private static long findWoodAdjacentLeafFrom(long start, Set<Long> leafPositions, Set<Long> logPositions) {
if (!leafPositions.contains(start)) return -1L;
if (hasLogNeighbor(start, logPositions)) return start;
Set<Long> visited = new HashSet<>();
Deque<Long> queue = new ArrayDeque<>();
queue.add(start);
visited.add(start);
while (!queue.isEmpty()) {
long pos = queue.poll();
int[] xyz = unpack(pos);
int score = 0;
for (int[] n : NEIGHBORS) {
long nk = packXYZ(xyz[0] + n[0], xyz[1] + n[1], xyz[2] + n[2]);
if (connectivityLeaves.contains(nk)) {
score++;
}
}
if (score > bestScore) {
bestScore = score;
best = pos;
if (score == 6) break;
if (!leafPositions.contains(nk) || !visited.add(nk)) continue;
if (hasLogNeighbor(nk, logPositions)) return nk;
queue.add(nk);
}
}
return best;
return -1L;
}
private static Set<Long> computeInitialFrontier(
Set<Long> unreached, Set<Long> logPositions, Map<Long, Integer> distances
) {
Set<Long> frontier = new HashSet<>();
for (long u : unreached) {
if (hasReachedNeighbor(u, distances, logPositions)) {
frontier.add(u);
}
}
return frontier;
}
private static boolean hasReachedNeighbor(long key, Map<Long, Integer> distances, Set<Long> logPositions) {
private static boolean hasLogNeighbor(long key, Set<Long> logPositions) {
int[] xyz = unpack(key);
for (int[] n : NEIGHBORS) {
long nk = packXYZ(xyz[0] + n[0], xyz[1] + n[1], xyz[2] + n[2]);
if (logPositions.contains(nk) || distances.containsKey(nk)) {
return true;
}
if (logPositions.contains(nk)) return true;
}
return false;
}
private static void propagateFromLog(
long logKey, Map<Long, Integer> distances,
Set<Long> connectivityLeaves, Set<Long> unreached
) {
int[] cx = unpack(logKey);
Deque<Long> q = new ArrayDeque<>();
for (int[] n : NEIGHBORS) {
long nk = packXYZ(cx[0] + n[0], cx[1] + n[1], cx[2] + n[2]);
if (connectivityLeaves.contains(nk)) {
Integer cur = distances.get(nk);
if (cur == null || cur > 1) {
if (cur == null) unreached.remove(nk);
distances.put(nk, 1);
q.add(nk);
}
}
}
while (!q.isEmpty()) {
long pos = q.poll();
int d = distances.get(pos);
if (d >= MAX_DISTANCE) continue;
int[] px = unpack(pos);
for (int[] n : NEIGHBORS) {
long nk = packXYZ(px[0] + n[0], px[1] + n[1], px[2] + n[2]);
if (connectivityLeaves.contains(nk)) {
Integer cur = distances.get(nk);
if (cur == null || cur > d + 1) {
if (cur == null) unreached.remove(nk);
distances.put(nk, d + 1);
q.add(nk);
}
}
}
}
}
private static void foliageBridge(
Set<Long> unreached,
Set<Long> logPositions,
Map<Long, Integer> distances,
Set<Long> leafPositions,
Set<Long> connectivityLeaves,
Map<Long, BlockData> positions,
BlockData leafTemplate,
List<LeafAddition> leafAdds,
int minX, int minY, int minZ, int maxX, int maxY, int maxZ
) {
Set<Long> pending = new HashSet<>(unreached);
while (!pending.isEmpty()) {
long seed = pending.iterator().next();
Set<Long> cluster = new HashSet<>();
Deque<Long> cq = new ArrayDeque<>();
cq.add(seed);
cluster.add(seed);
while (!cq.isEmpty()) {
long p = cq.poll();
int[] xyz = unpack(p);
for (int[] n : NEIGHBORS) {
long nk = packXYZ(xyz[0] + n[0], xyz[1] + n[1], xyz[2] + n[2]);
if (pending.contains(nk) && cluster.add(nk)) {
cq.add(nk);
}
}
}
Map<Long, Long> parent = new HashMap<>();
Deque<Long> q = new ArrayDeque<>();
for (long c : cluster) {
parent.put(c, -1L);
q.add(c);
}
long pathEnd = -1L;
while (!q.isEmpty() && pathEnd == -1L) {
long p = q.poll();
int[] xyz = unpack(p);
for (int[] n : NEIGHBORS) {
int nx = xyz[0] + n[0];
int ny = xyz[1] + n[1];
int nz = xyz[2] + n[2];
if (nx < minX || nx > maxX) continue;
if (ny < minY || ny > maxY) continue;
if (nz < minZ || nz > maxZ) continue;
long nk = packXYZ(nx, ny, nz);
if (parent.containsKey(nk)) continue;
if (logPositions.contains(nk) || distances.containsKey(nk)) {
pathEnd = p;
break;
}
if (positions.containsKey(nk)) continue;
parent.put(nk, p);
q.add(nk);
}
}
pending.removeAll(cluster);
if (pathEnd == -1L) {
continue;
}
long cur = pathEnd;
while (cur != -1L && !cluster.contains(cur)) {
if (!positions.containsKey(cur)) {
BlockData clone = leafTemplate.clone();
positions.put(cur, clone);
leafPositions.add(cur);
connectivityLeaves.add(cur);
leafAdds.add(new LeafAddition(cur, clone));
}
cur = parent.get(cur);
}
}
}
private static void skipOrphanCluster(long seed, Set<Long> unreached, Set<Long> connectivityLeaves) {
Deque<Long> queue = new ArrayDeque<>();
Set<Long> visited = new HashSet<>();
queue.add(seed);
visited.add(seed);
while (!queue.isEmpty()) {
long pos = queue.poll();
unreached.remove(pos);
int[] xyz = unpack(pos);
for (int[] n : NEIGHBORS) {
long nk = packXYZ(xyz[0] + n[0], xyz[1] + n[1], xyz[2] + n[2]);
if (visited.add(nk) && unreached.contains(nk) && connectivityLeaves.contains(nk)) {
queue.add(nk);
}
}
}
}
private static void removeOrphanCluster(
long seed,
Set<Long> connectivityLeaves, Set<Long> leafPositions, Set<Long> unreached,
Map<Long, Integer> distances, Map<Long, BlockData> positions, Set<Long> orphanRemovals
) {
Deque<Long> queue = new ArrayDeque<>();
Set<Long> visited = new HashSet<>();
queue.add(seed);
visited.add(seed);
while (!queue.isEmpty()) {
long pos = queue.poll();
int[] xyz = unpack(pos);
for (int[] n : NEIGHBORS) {
long nk = packXYZ(xyz[0] + n[0], xyz[1] + n[1], xyz[2] + n[2]);
if (visited.add(nk) && connectivityLeaves.contains(nk)) {
queue.add(nk);
}
}
orphanRemovals.add(pos);
connectivityLeaves.remove(pos);
leafPositions.remove(pos);
unreached.remove(pos);
distances.remove(pos);
positions.remove(pos);
}
}
private static Map<Long, Integer> seedDistances(Set<Long> logPositions, Set<Long> leafPositions) {
Map<Long, Integer> dist = new HashMap<>();
Deque<Long> queue = new ArrayDeque<>();
@@ -377,7 +377,7 @@ public class IrisEngine implements Engine {
setupEngine();
J.a(() -> {
synchronized (ServerConfigurator.class) {
ServerConfigurator.installDataPacks(false, false);
ServerConfigurator.installDataPacks(false);
}
});
}
@@ -28,6 +28,7 @@ import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.mantle.MantleComponent;
import art.arcane.iris.engine.mantle.components.MantleCarvingComponent;
import art.arcane.iris.engine.mantle.components.MantleFloatingObjectComponent;
import art.arcane.iris.engine.mantle.components.MantleFluidBodyComponent;
import art.arcane.iris.engine.mantle.components.MantleObjectComponent;
import art.arcane.iris.util.project.matter.IrisMatterSupport;
@@ -82,6 +83,7 @@ public class IrisEngineMantle implements EngineMantle {
registerComponent(new MantleFluidBodyComponent(this));
object = new MantleObjectComponent(this);
registerComponent(object);
registerComponent(new MantleFloatingObjectComponent(this));
}
@Override
@@ -80,7 +80,6 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
private final KList<Runnable> updateQueue = new KList<>();
private final ChronoLatch cl;
private final ChronoLatch clw;
private final ChronoLatch ecl;
private final ChronoLatch cln;
private final ChronoLatch chunkUpdater;
private final ChronoLatch chunkDiscovery;
@@ -90,9 +89,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
private final Set<Long> markerFlagQueue = ConcurrentHashMap.newKeySet();
private final Set<Long> discoveredFlagQueue = ConcurrentHashMap.newKeySet();
private final Set<Long> markerScanQueue = ConcurrentHashMap.newKeySet();
private double energy = 25;
private int entityCount = 0;
private long charge = 0;
private int actuallySpawned = 0;
private int cooldown = 0;
private List<Entity> precount = new KList<>();
@@ -101,7 +98,6 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
public IrisWorldManager() {
super(null);
cl = null;
ecl = null;
cln = null;
clw = null;
looper = null;
@@ -117,7 +113,6 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
chunkDiscovery = new ChronoLatch(5000);
cln = new ChronoLatch(60000);
cl = new ChronoLatch(3000);
ecl = new ChronoLatch(250);
clw = new ChronoLatch(1000, true);
cleanupService = Executors.newSingleThreadScheduledExecutor(runnable -> {
var thread = new Thread(runnable, "Iris Mantle Cleanup " + getTarget().getWorld().name());
@@ -125,7 +120,6 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
return thread;
});
id = engine.getCacheID();
energy = 25;
looper = new Looper() {
@Override
protected long loop() {
@@ -158,16 +152,6 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
return 3000;
}
if (getDimension().isInfiniteEnergy()) {
energy += 1000;
fixEnergy();
}
if (M.ms() < charge) {
energy += 70;
fixEnergy();
}
if (precount != null) {
entityCount = 0;
for (Entity i : precount) {
@@ -181,13 +165,6 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
precount = null;
}
if (energy < 650) {
if (ecl.flip()) {
energy *= 1 + (0.02 * M.clip((1D - getEntitySaturation()), 0D, 1D));
fixEnergy();
}
}
onAsyncTick();
}
@@ -413,11 +390,6 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
actuallySpawned = 0;
if (energy < 100) {
J.sleep(200);
return false;
}
if (!getEngine().getWorld().hasRealWorld()) {
Iris.debug("Can't spawn. No real world");
J.sleep(5000);
@@ -471,7 +443,6 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
spawnChunkSafely(world, c.getX(), c.getZ(), false);
}
energy -= (actuallySpawned / 2D);
return actuallySpawned > 0;
}
@@ -581,10 +552,6 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
}
}
private void fixEnergy() {
energy = M.clip(energy, 1D, getDimension().getMaximumEnergy());
}
private void spawnIn(Chunk c, boolean initial) {
if (getEngine().isClosed()) {
return;
@@ -599,10 +566,6 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
return;
}
if (initial) {
energy += 1.2;
}
if (IrisSettings.get().getWorld().isMarkerEntitySpawningSystem()) {
forEachMarkerSpawner(c, (block, spawners) -> {
IrisSpawner s = new KList<>(spawners).getRandom();
@@ -663,7 +626,6 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
actuallySpawned += s;
if (s > 0) {
ref.spawn(getEngine(), c.getX(), c.getZ());
energy -= s * ((i.getEnergyMultiplier() * ref.getEnergyMultiplier() * 1));
}
}
@@ -676,7 +638,6 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
actuallySpawned += s;
if (s > 0) {
ref.spawn(getEngine(), PowerOfTwoCoordinates.blockToChunkFloor(pos.getX()), PowerOfTwoCoordinates.blockToChunkFloor(pos.getZ()));
energy -= s * ((i.getEnergyMultiplier() * ref.getEnergyMultiplier() * 1));
}
}
@@ -736,8 +697,6 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
Long key = Cache.key(e);
cleanup.put(key, cleanupService.schedule(() -> {
cleanup.remove(key);
energy += 0.3;
fixEnergy();
getEngine().cleanupMantleChunk(cX, cZ);
}, Math.max(IrisSettings.get().getPerformance().mantleCleanupDelay * 50L, 0), TimeUnit.MILLISECONDS));
@@ -816,11 +775,6 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
return getEngine().getMantle().getMantle();
}
@Override
public void chargeEnergy() {
charge = M.ms() + 3000;
}
@Override
public void teleportAsync(PlayerTeleportEvent e) {
if (IrisSettings.get().getWorld().getAsyncTeleport().isEnabled()) {
@@ -25,12 +25,14 @@ public class UpperDimensionContext implements DataProvider {
private final int chunkHeight;
private final ProceduralStream<Double> heightStream;
private final ProceduralStream<IrisBiome> biomeStream;
private final ProceduralStream<IrisRegion> regionStream;
private final ProceduralStream<BlockData> rockStream;
private final boolean selfReferencing;
private UpperDimensionContext(IrisDimension dimension, IrisData data, int chunkHeight,
ProceduralStream<Double> heightStream,
ProceduralStream<IrisBiome> biomeStream,
ProceduralStream<IrisRegion> regionStream,
ProceduralStream<BlockData> rockStream,
boolean selfReferencing) {
this.dimension = dimension;
@@ -38,6 +40,7 @@ public class UpperDimensionContext implements DataProvider {
this.chunkHeight = chunkHeight;
this.heightStream = heightStream;
this.biomeStream = biomeStream;
this.regionStream = regionStream;
this.rockStream = rockStream;
this.selfReferencing = selfReferencing;
}
@@ -59,6 +62,7 @@ public class UpperDimensionContext implements DataProvider {
chunkHeight,
complex.getHeightStream(),
complex.getBaseBiomeStream(),
complex.getRegionStream(),
complex.getRockStream(),
true
);
@@ -233,6 +237,7 @@ public class UpperDimensionContext implements DataProvider {
chunkHeight,
heightStream,
baseBiomeStream,
regionStream,
rockStream,
false
);
@@ -247,6 +252,10 @@ public class UpperDimensionContext implements DataProvider {
return biomeStream.get((double) x, (double) z);
}
public IrisRegion getUpperRegion(int x, int z) {
return regionStream == null ? null : regionStream.get((double) x, (double) z);
}
public BlockData getRockBlock(int x, int z) {
return rockStream.get((double) x, (double) z);
}
@@ -23,6 +23,7 @@ import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineAssignedActuator;
import art.arcane.iris.engine.framework.EngineDecorator;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.util.common.data.B;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.volmlib.util.documentation.BlockCoordinates;
import art.arcane.iris.util.project.hunk.Hunk;
@@ -87,7 +88,8 @@ public class IrisDecorantActuator extends EngineAssignedActuator<BlockData> {
continue;
}
if (height < getDimension().getFluidHeight() && PREDICATE_SOLID.test(output.get(i, height, j))) {
if (height < getDimension().getFluidHeight() && PREDICATE_SOLID.test(output.get(i, height, j))
&& height + 1 < output.getHeight() && B.isWater(output.get(i, height + 1, j))) {
getSeaSurfaceDecorator().decorate(i, j,
realX, Math.round(i + 1), Math.round(x + i - 1),
realZ, Math.round(z + j + 1), Math.round(z + j - 1),
@@ -0,0 +1,33 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine.decorator;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisDecorator;
public class IrisFloatingSurfaceDecorator extends IrisSurfaceDecorator {
public IrisFloatingSurfaceDecorator(Engine engine) {
super(engine, "Floating Surface");
}
@Override
protected boolean isSlopeValid(IrisDecorator decorator, int realX, int realZ) {
return true;
}
}
@@ -39,6 +39,17 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator {
super(engine, "Surface", IrisDecorationPart.NONE);
}
protected IrisSurfaceDecorator(Engine engine, String name) {
super(engine, name, IrisDecorationPart.NONE);
}
protected boolean isSlopeValid(IrisDecorator decorator, int realX, int realZ) {
if (decorator.isForcePlace() || decorator.getSlopeCondition().isDefault()) {
return true;
}
return decorator.getSlopeCondition().isValid(getComplex().getSlopeStream().get(realX, realZ));
}
@BlockCoordinates
@Override
public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1, Hunk<BlockData> data, IrisBiome biome, int height, int max) {
@@ -54,8 +65,7 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator {
boolean caveSkipFluid = biome.getInferredType() == InferredType.CAVE;
if (decorator != null) {
if (!decorator.isForcePlace() && !decorator.getSlopeCondition().isDefault()
&& !decorator.getSlopeCondition().isValid(getComplex().getSlopeStream().get(realX, realZ))) {
if (!isSlopeValid(decorator, realX, realZ)) {
return;
}
@@ -916,7 +916,13 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat
return o.getObject().getLoadKey() + "@" + o.getId();
}
return null;
MantleChunk<Matter> chunk = getMantle().getMantle().getChunk(x >> 4, z >> 4).use();
try {
String raw = chunk.get(x & 15, y, z & 15, String.class);
return (raw == null || raw.isEmpty()) ? null : raw;
} finally {
chunk.release();
}
}
default PlacedObject getObjectPlacement(int x, int y, int z) {
@@ -936,6 +942,9 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat
String[] v = objectAt.split("\\Q@\\E");
String object = v[0];
if (object.isEmpty() || object.equals("null")) {
return null;
}
int id = Integer.parseInt(v[1]);
@@ -27,8 +27,6 @@ import org.bukkit.event.player.PlayerTeleportEvent;
public interface EngineWorldManager {
void close();
double getEnergy();
int getEntityCount();
int getChunkCount();
@@ -47,7 +45,5 @@ public interface EngineWorldManager {
void onChunkUnload(Chunk e);
void chargeEnergy();
void teleportAsync(PlayerTeleportEvent e);
}
@@ -163,6 +163,10 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable {
return;
}
if (y == 0 && t instanceof BlockData && engineMantle.getEngine().getDimension().isBedrock()) {
return;
}
MantleChunk<Matter> chunk = acquireChunk(cx, cz);
if (chunk == null) return;
@@ -0,0 +1,274 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine.mantle.components;
import art.arcane.iris.Iris;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.data.cache.Cache;
import art.arcane.iris.engine.mantle.ComponentFlag;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.mantle.IrisMantleComponent;
import art.arcane.iris.engine.mantle.MantleWriter;
import art.arcane.iris.engine.modifier.IrisFloatingChildBiomeModifier;
import art.arcane.iris.engine.object.CarvingMode;
import art.arcane.iris.engine.object.FloatingIslandSample;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisFloatingChildBiomes;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.ObjectPlaceMode;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
import art.arcane.volmlib.util.math.RNG;
@ComponentFlag(ReservedFlag.FLOATING_OBJECT)
public class MantleFloatingObjectComponent extends IrisMantleComponent {
public MantleFloatingObjectComponent(EngineMantle engineMantle) {
super(engineMantle, ReservedFlag.FLOATING_OBJECT, 2);
}
@Override
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
IrisComplex complex = context.getComplex();
IrisData data = getData();
int chunkHeight = getEngineMantle().getEngine().getHeight();
int minX = x << 4;
int minZ = z << 4;
long baseSeed = getEngineMantle().getEngine().getSeedManager().getTerrain() ^ IrisFloatingChildBiomeModifier.FLOATING_BASE_SEED_SALT;
RNG chunkRng = new RNG(Cache.key(x, z) + seed() + 0x0FA710BEL);
FloatingIslandSample.clearChunkMemo();
FloatingIslandSample[] samples = new FloatingIslandSample[256];
for (int xf = 0; xf < 16; xf++) {
for (int zf = 0; zf < 16; zf++) {
int wx = minX + xf;
int wz = minZ + zf;
IrisBiome parent = complex.getTrueBiomeStream().get(wx, wz);
if (parent == null || parent.getFloatingChildBiomes() == null || parent.getFloatingChildBiomes().isEmpty()) {
continue;
}
FloatingIslandSample sample = FloatingIslandSample.sampleMemoized(parent, wx, wz, chunkHeight, baseSeed, data, complex, getEngineMantle().getEngine());
if (sample != null) {
samples[(zf << 4) | xf] = sample;
}
}
}
java.util.IdentityHashMap<IrisFloatingChildBiomes, KList<Integer>> entryColumns = new java.util.IdentityHashMap<>();
for (int i = 0; i < 256; i++) {
FloatingIslandSample s = samples[i];
if (s == null || s.entry == null) {
continue;
}
entryColumns.computeIfAbsent(s.entry, e -> new KList<>()).add(i);
}
for (java.util.Map.Entry<IrisFloatingChildBiomes, KList<Integer>> ec : entryColumns.entrySet()) {
IrisFloatingChildBiomes entry = ec.getKey();
KList<Integer> columns = ec.getValue();
if (columns.isEmpty()) {
continue;
}
IrisBiome parent = complex.getTrueBiomeStream().get(minX + (columns.get(0) & 15), minZ + (columns.get(0) >> 4));
IrisBiome target = entry.getRealBiome(parent, data);
KList<IrisObjectPlacement> floating = entry.getFloatingObjects();
if (floating != null && !floating.isEmpty()) {
for (IrisObjectPlacement placement : floating) {
tryPlaceFloatingChunk(writer, complex, chunkRng, data, placement, samples, columns, minX, minZ, entry);
}
}
if (entry.isInheritObjects() && target != null) {
for (IrisObjectPlacement placement : target.getSurfaceObjects()) {
tryPlaceAnchoredChunk(writer, complex, chunkRng, data, placement, samples, columns, minX, minZ, entry);
}
}
KList<IrisObjectPlacement> extras = entry.getExtraObjects();
if (extras != null && !extras.isEmpty()) {
for (IrisObjectPlacement placement : extras) {
tryPlaceAnchoredChunk(writer, complex, chunkRng, data, placement, samples, columns, minX, minZ, entry);
}
}
}
}
@ChunkCoordinates
private void tryPlaceFloatingChunk(MantleWriter writer, IrisComplex complex, RNG rng, IrisData data, IrisObjectPlacement placement, FloatingIslandSample[] samples, KList<Integer> columns, int minX, int minZ, IrisFloatingChildBiomes entry) {
if (placement == null || columns == null || columns.isEmpty()) {
return;
}
int density = placement.getDensity(rng, minX, minZ, data);
double perAttempt = placement.getChance();
for (int i = 0; i < density; i++) {
if (!rng.chance(perAttempt + rng.d(-0.005, 0.005))) {
continue;
}
IrisObject raw = placement.getObject(complex, rng);
if (raw == null) {
continue;
}
IrisObject obj0 = placement.getScale().get(rng, raw);
if (obj0 == null) {
continue;
}
if (entry != null && entry.hasObjectShrink()) {
obj0 = entry.getShrinkScale().get(rng, obj0);
if (obj0 == null) {
continue;
}
}
final IrisObject obj = obj0;
int key = columns.get(rng.i(0, columns.size() - 1));
int xx = minX + (key & 15);
int zz = minZ + (key >> 4);
IrisObjectPlacement floatingPlacement = placement.toPlacement(obj.getLoadKey());
int id = rng.i(0, Integer.MAX_VALUE);
try {
obj.place(xx, -1, zz, writer, floatingPlacement, rng, (b, bd) -> {
String marker = placementMarker(obj, id);
if (marker != null) {
writer.setData(b.getX(), b.getY(), b.getZ(), marker);
}
}, null, data);
} catch (Throwable e) {
Iris.reportError(e);
}
}
}
@ChunkCoordinates
private void tryPlaceAnchoredChunk(MantleWriter writer, IrisComplex complex, RNG rng, IrisData data, IrisObjectPlacement placement, FloatingIslandSample[] samples, KList<Integer> columns, int minX, int minZ, IrisFloatingChildBiomes entry) {
if (placement == null || columns.isEmpty()) {
return;
}
KList<Integer> interior = interiorColumns(samples, columns);
KList<Integer> pickPool = interior.isEmpty() ? columns : interior;
int density = placement.getDensity(rng, minX, minZ, data);
double perAttempt = placement.getChance();
for (int i = 0; i < density; i++) {
if (!rng.chance(perAttempt + rng.d(-0.005, 0.005))) {
continue;
}
IrisObject raw = placement.getObject(complex, rng);
if (raw == null) {
continue;
}
IrisObject obj0 = placement.getScale().get(rng, raw);
if (obj0 == null) {
continue;
}
if (entry != null && entry.hasObjectShrink()) {
obj0 = entry.getShrinkScale().get(rng, obj0);
if (obj0 == null) {
continue;
}
}
final IrisObject obj = obj0;
int key = pickPool.get(rng.i(0, pickPool.size() - 1));
int xf = key & 15;
int zf = key >> 4;
FloatingIslandSample sample = samples[(zf << 4) | xf];
if (sample == null) {
continue;
}
int wx = minX + xf;
int wz = minZ + zf;
int anchorY = sample.topY() + 1 + obj.getCenter().getBlockY();
int id = rng.i(0, Integer.MAX_VALUE);
IrisObjectPlacement anchored = placement.toPlacement(obj.getLoadKey());
anchored.setCarvingSupport(CarvingMode.ANYWHERE);
anchored.setForcePlace(true);
anchored.setMode(ObjectPlaceMode.STRUCTURE_PIECE);
anchored.setBore(false);
anchored.setMeld(false);
try {
obj.place(wx, anchorY, wz, writer, anchored, rng, (b, bd) -> {
String marker = placementMarker(obj, id);
if (marker != null) {
writer.setData(b.getX(), b.getY(), b.getZ(), marker);
}
}, null, data);
} catch (Throwable e) {
Iris.reportError(e);
}
}
}
private static KList<Integer> interiorColumns(FloatingIslandSample[] samples, KList<Integer> columns) {
KList<Integer> interior = new KList<>();
for (int key : columns) {
int xf = key & 15;
int zf = key >> 4;
if (xf <= 0 || xf >= 15 || zf <= 0 || zf >= 15) {
continue;
}
if (samples[(zf << 4) | (xf + 1)] == null) continue;
if (samples[(zf << 4) | (xf - 1)] == null) continue;
if (samples[((zf + 1) << 4) | xf] == null) continue;
if (samples[((zf - 1) << 4) | xf] == null) continue;
interior.add(key);
}
return interior;
}
private static String placementMarker(IrisObject object, int id) {
if (object == null) {
return null;
}
String key = object.getLoadKey();
if (key == null || key.isEmpty() || key.equals("null")) {
return null;
}
return key + "@" + id;
}
@Override
protected int computeRadius() {
int maxThickness = 0;
int maxHeightAbove = 0;
try {
for (IrisBiome biome : getDimension().getAllBiomes(this::getData)) {
KList<IrisFloatingChildBiomes> entries = biome.getFloatingChildBiomes();
if (entries == null || entries.isEmpty()) {
continue;
}
for (IrisFloatingChildBiomes entry : entries) {
maxThickness = Math.max(maxThickness, entry.getMaxThickness());
maxHeightAbove = Math.max(maxHeightAbove, entry.getMaxHeightAboveSurface());
}
}
} catch (Throwable ignored) {
}
return Math.max(1, (maxThickness + maxHeightAbove) >> 4);
}
}
@@ -22,6 +22,7 @@ import art.arcane.iris.Iris;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.engine.data.cache.Cache;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.UpperDimensionContext;
import art.arcane.iris.engine.mantle.ComponentFlag;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.mantle.IrisMantleComponent;
@@ -60,10 +61,27 @@ public class MantleObjectComponent extends IrisMantleComponent {
private static final long CAVE_REJECT_LOG_THROTTLE_MS = 5000L;
private static final int SURFACE_HEIGHT_CHUNK_FILL_THRESHOLD = 128;
private static final Map<String, CaveRejectLogState> CAVE_REJECT_LOG_STATE = new ConcurrentHashMap<>();
private static final Set<String> MISSING_LOAD_KEY_WARNED = ConcurrentHashMap.newKeySet();
public MantleObjectComponent(EngineMantle engineMantle) {
super(engineMantle, ReservedFlag.OBJECT, 1);
}
private static String placementMarker(IrisObject object, int id, String context) {
String key = object == null ? null : object.getLoadKey();
if (key == null || key.isEmpty() || key.equals("null")) {
String fingerprint = context + "|" + (object == null ? "<null>" : object.getClass().getSimpleName());
if (MISSING_LOAD_KEY_WARNED.add(fingerprint)) {
java.io.File file = object == null ? null : object.getLoadFile();
Iris.warn("Skipping placement marker write: IrisObject has no loadKey (context=" + context
+ ", file=" + (file == null ? "<unknown>" : file.getPath()) + "). "
+ "This would previously produce 'Couldn't find Object: null' warnings on chunk reload.");
}
return null;
}
return key + "@" + id;
}
@Override
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
IrisComplex complex = context.getComplex();
@@ -106,6 +124,11 @@ public class MantleObjectComponent extends IrisMantleComponent {
+ " regionCavePlacers=" + region.getCarvingObjects().size());
}
ObjectPlacementSummary summary = placeObjects(writer, rng, x, z, surfaceBiome, caveBiome, region, complex, traceRegen, surfaceHeightLookup);
UpperDimensionContext upperCtx = getEngineMantle().getEngine().getUpperContext();
IrisDimension dimension = getDimension();
if (upperCtx != null && dimension.isUpperDimensionObjects()) {
placeUpperObjects(writer, rng, x, z, xxx, zzz, surfaceY, upperCtx, dimension, complex, traceRegen);
}
if (traceRegen) {
Iris.info("Regen object layer done: chunk=" + x + "," + z
+ " biomeSurfacePlacersChecked=" + summary.biomeSurfacePlacersChecked()
@@ -374,6 +397,9 @@ public class MantleObjectComponent extends IrisMantleComponent {
boolean overCave = surfaceObjectExclusionDepth > 0 && hasSurfaceCarveExposure(writer, surfaceHeightLookup, xx, zz, surfaceObjectExclusionDepth, surfaceObjectExclusionRadius);
int id = rng.i(0, Integer.MAX_VALUE);
IrisObjectPlacement effectivePlacement = resolveEffectivePlacement(objectPlacement, v);
if (effectivePlacement.getMode() == ObjectPlaceMode.FLOATING) {
overCave = false;
}
try {
int result = -1;
String fallbackPath = "surface";
@@ -384,7 +410,10 @@ public class MantleObjectComponent extends IrisMantleComponent {
IrisObjectPlacement floorPlacement = effectivePlacement.toPlacement(v.getLoadKey());
floorPlacement.setMode(ObjectPlaceMode.FAST_MIN_HEIGHT);
result = v.place(xx, caveFloorY, zz, writer, floorPlacement, rng, (b, data) -> {
writer.setData(b.getX(), b.getY(), b.getZ(), v.getLoadKey() + "@" + id);
String marker = placementMarker(v, id, "cave-floor");
if (marker != null) {
writer.setData(b.getX(), b.getY(), b.getZ(), marker);
}
if (effectivePlacement.isDolphinTarget() && effectivePlacement.isUnderwater() && B.isStorageChest(data)) {
writer.setData(b.getX(), b.getY(), b.getZ(), MatterStructurePOI.BURIED_TREASURE);
}
@@ -396,7 +425,10 @@ public class MantleObjectComponent extends IrisMantleComponent {
IrisObjectPlacement stiltPlacement = effectivePlacement.toPlacement(v.getLoadKey());
stiltPlacement.setMode(ObjectPlaceMode.FAST_MIN_STILT);
result = v.place(xx, -1, zz, writer, stiltPlacement, rng, (b, data) -> {
writer.setData(b.getX(), b.getY(), b.getZ(), v.getLoadKey() + "@" + id);
String marker = placementMarker(v, id, "stilt");
if (marker != null) {
writer.setData(b.getX(), b.getY(), b.getZ(), marker);
}
if (effectivePlacement.isDolphinTarget() && effectivePlacement.isUnderwater() && B.isStorageChest(data)) {
writer.setData(b.getX(), b.getY(), b.getZ(), MatterStructurePOI.BURIED_TREASURE);
}
@@ -405,7 +437,10 @@ public class MantleObjectComponent extends IrisMantleComponent {
}
} else {
result = v.place(xx, -1, zz, writer, effectivePlacement, rng, (b, data) -> {
writer.setData(b.getX(), b.getY(), b.getZ(), v.getLoadKey() + "@" + id);
String marker = placementMarker(v, id, "surface");
if (marker != null) {
writer.setData(b.getX(), b.getY(), b.getZ(), marker);
}
if (effectivePlacement.isDolphinTarget() && effectivePlacement.isUnderwater() && B.isStorageChest(data)) {
writer.setData(b.getX(), b.getY(), b.getZ(), MatterStructurePOI.BURIED_TREASURE);
}
@@ -557,7 +592,10 @@ public class MantleObjectComponent extends IrisMantleComponent {
try {
int result = object.place(x, y, z, writer, effectivePlacement, rng, (b, data) -> {
wrotePlacementData.set(true);
writer.setData(b.getX(), b.getY(), b.getZ(), object.getLoadKey() + "@" + id);
String marker = placementMarker(object, id, "cave");
if (marker != null) {
writer.setData(b.getX(), b.getY(), b.getZ(), marker);
}
if (effectivePlacement.isDolphinTarget() && effectivePlacement.isUnderwater() && B.isStorageChest(data)) {
writer.setData(b.getX(), b.getY(), b.getZ(), MatterStructurePOI.BURIED_TREASURE);
}
@@ -629,6 +667,141 @@ public class MantleObjectComponent extends IrisMantleComponent {
return new ObjectPlacementResult(attempts, placed, rejected, nullObjects, errors);
}
@ChunkCoordinates
private void placeUpperObjects(
MantleWriter writer,
RNG rng,
int chunkX,
int chunkZ,
int centerX,
int centerZ,
int lowerSurfaceCenterY,
UpperDimensionContext upperCtx,
IrisDimension dimension,
IrisComplex complex,
boolean traceRegen
) {
IrisBiome upperBiome = upperCtx.getUpperBiome(centerX, centerZ);
IrisRegion upperRegion = upperCtx.getUpperRegion(centerX, centerZ);
if (upperBiome == null && upperRegion == null) {
return;
}
boolean forcePlace = dimension.isUpperObjectsForcePlace();
if (upperBiome != null) {
for (IrisObjectPlacement i : upperBiome.getSurfaceObjects()) {
if (!rng.chance(i.getChance() + rng.d(-0.005, 0.005))) {
continue;
}
try {
placeUpperObject(writer, rng, chunkX, chunkZ, i, upperCtx, dimension, complex, forcePlace, traceRegen, "upper-biome-surface");
} catch (Throwable e) {
Iris.reportError(e);
Iris.error("Failed to place upper-dimension objects in biome " + upperBiome.getName()
+ ": " + i.getPlace().toString(", ") + " (" + e.getClass().getSimpleName() + ")");
e.printStackTrace();
}
}
}
if (upperRegion != null) {
for (IrisObjectPlacement i : upperRegion.getSurfaceObjects()) {
if (!rng.chance(i.getChance() + rng.d(-0.005, 0.005))) {
continue;
}
try {
placeUpperObject(writer, rng, chunkX, chunkZ, i, upperCtx, dimension, complex, forcePlace, traceRegen, "upper-region-surface");
} catch (Throwable e) {
Iris.reportError(e);
Iris.error("Failed to place upper-dimension objects in region " + upperRegion.getName()
+ ": " + i.getPlace().toString(", ") + " (" + e.getClass().getSimpleName() + ")");
e.printStackTrace();
}
}
}
}
@ChunkCoordinates
private void placeUpperObject(
MantleWriter writer,
RNG rng,
int chunkX,
int chunkZ,
IrisObjectPlacement objectPlacement,
UpperDimensionContext upperCtx,
IrisDimension dimension,
IrisComplex complex,
boolean forcePlace,
boolean traceRegen,
String scope
) {
int chunkHeight = getEngineMantle().getEngine().getHeight();
int upperGap = dimension.getUpperDimensionGap();
int minX = chunkX << 4;
int minZ = chunkZ << 4;
int density = objectPlacement.getDensity(rng, minX, minZ, getData());
for (int i = 0; i < density; i++) {
IrisObject v = objectPlacement.getScale().get(rng, objectPlacement.getObject(complex, rng));
if (v == null) {
continue;
}
int xx = rng.i(minX, minX + 15);
int zz = rng.i(minZ, minZ + 15);
int columnLowerSurfaceY = getEngineMantle().getEngine().getHeight(xx, zz, true);
int rawUpperSurface = upperCtx.getUpperSurfaceY(xx, zz);
int upperSurfaceY = Math.max(rawUpperSurface, columnLowerSurfaceY + upperGap);
if (upperSurfaceY >= chunkHeight - 2) {
continue;
}
int halfH = Math.floorDiv(v.getH(), 2);
int anchorY = upperSurfaceY - 1 - halfH;
if (anchorY <= 1) {
continue;
}
int id = rng.i(0, Integer.MAX_VALUE);
IrisObjectPlacement placement = objectPlacement.toPlacement(v.getLoadKey());
placement.setMode(ObjectPlaceMode.CENTER_HEIGHT);
placement.setRotation(buildUpsideDownRotation());
placement.setCarvingSupport(CarvingMode.ANYWHERE);
if (forcePlace) {
placement.setForcePlace(true);
}
int result = v.place(xx, anchorY, zz, writer, placement, rng, (b, data) -> {
String marker = placementMarker(v, id, "upper");
if (marker != null) {
writer.setData(b.getX(), b.getY(), b.getZ(), marker);
}
if (placement.isDolphinTarget() && placement.isUnderwater() && B.isStorageChest(data)) {
writer.setData(b.getX(), b.getY(), b.getZ(), MatterStructurePOI.BURIED_TREASURE);
}
}, null, getData());
if (traceRegen) {
Iris.info("Upper object placement: chunk=" + chunkX + "," + chunkZ
+ " scope=" + scope
+ " object=" + v.getLoadKey()
+ " anchorY=" + anchorY
+ " upperSurfaceY=" + upperSurfaceY
+ " resultY=" + result
+ " forcePlace=" + forcePlace);
}
}
}
private IrisObjectRotation buildUpsideDownRotation() {
IrisObjectRotation rt = new IrisObjectRotation();
rt.setEnabled(true);
rt.setXAxis(new IrisAxisRotationClamp(true, true, 180D, 180D, 90D));
rt.setYAxis(new IrisAxisRotationClamp(true, false, 0D, 0D, 90D));
rt.setZAxis(new IrisAxisRotationClamp());
return rt;
}
private void logCaveReject(
String scope,
String reason,
@@ -698,18 +871,18 @@ public class MantleObjectComponent extends IrisMantleComponent {
}
String normalized = loadKey.toLowerCase(Locale.ROOT);
boolean legacyImported = normalized.startsWith("imports/")
boolean imported = normalized.startsWith("imports/")
|| normalized.contains("/imports/")
|| normalized.contains("imports/");
IrisExternalDatapack externalDatapack = resolveExternalDatapackForObjectKey(normalized);
boolean externalImported = externalDatapack != null;
boolean imported = legacyImported || externalImported;
if (!imported) {
return objectPlacement;
}
ObjectPlaceMode mode = objectPlacement.getMode();
if (mode == ObjectPlaceMode.FLOATING || mode == ObjectPlaceMode.STRUCTURE_PIECE) {
return objectPlacement;
}
boolean needsModeChange = mode != ObjectPlaceMode.FAST_MIN_STILT;
if (!needsModeChange) {
return objectPlacement;
@@ -720,42 +893,6 @@ public class MantleObjectComponent extends IrisMantleComponent {
return effectivePlacement;
}
private IrisExternalDatapack resolveExternalDatapackForObjectKey(String normalizedLoadKey) {
if (normalizedLoadKey == null || normalizedLoadKey.isBlank()) {
return null;
}
int slash = normalizedLoadKey.indexOf('/');
if (slash <= 0) {
return null;
}
String candidateId = normalizedLoadKey.substring(0, slash);
if (candidateId.isBlank()) {
return null;
}
IrisDimension dimension = getDimension();
if (dimension == null || dimension.getExternalDatapacks() == null || dimension.getExternalDatapacks().isEmpty()) {
return null;
}
for (IrisExternalDatapack externalDatapack : dimension.getExternalDatapacks()) {
if (externalDatapack == null || !externalDatapack.isEnabled()) {
continue;
}
String id = externalDatapack.getId();
if (id == null || id.isBlank()) {
continue;
}
if (candidateId.equals(id.toLowerCase(Locale.ROOT))) {
return externalDatapack;
}
}
return null;
}
private int findNearestCaveFloor(MantleWriter writer, int x, int z) {
KList<Integer> anchors = scanCaveAnchorColumn(writer, IrisCaveAnchorMode.FLOOR, 1, 0, x, z);
if (anchors.isEmpty()) {
@@ -45,6 +45,7 @@ public class ModeOverworld extends IrisEngineMode implements EngineMode {
var deposit = new IrisDepositModifier(getEngine());
var perfection = new IrisPerfectionModifier(getEngine());
var custom = new IrisCustomModifier(getEngine());
var floatingChildBiomes = new IrisFloatingChildBiomeModifier(getEngine());
EngineStage sBiome = (x, z, k, p, m, c) -> biome.actuate(x, z, p, m, c);
EngineStage sGenMatter = (x, z, k, p, m, c) -> {
if (shouldBypassMantleStages(getEngine())) {
@@ -78,6 +79,8 @@ public class ModeOverworld extends IrisEngineMode implements EngineMode {
}
getMantle().insertMatter(x >> 4, z >> 4, BlockData.class, K, m);
};
EngineStage sFloatingTerrainSolid = (x, z, k, p, m, c) -> floatingChildBiomes.modify(x, z, k, m, c);
EngineStage sFloatingDecorate = (x, z, k, p, m, c) -> floatingChildBiomes.decorateColumns(x, z, k, m, c);
EngineStage sPerfection = (x, z, k, p, m, c) -> perfection.modify(x, z, k, m, c);
EngineStage sCustom = (x, z, k, p, m, c) -> {
if (shouldBypassMantleStages(getEngine())) {
@@ -94,11 +97,13 @@ public class ModeOverworld extends IrisEngineMode implements EngineMode {
sCave,
sPost
));
registerStage(sFloatingTerrainSolid);
registerStage(burst(
sDeposit,
sInsertMatter,
sDecorant
));
registerStage(sFloatingDecorate);
registerStage(sPerfection);
registerStage(sCustom);
}
@@ -0,0 +1,305 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine.modifier;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.decorator.IrisFloatingSurfaceDecorator;
import art.arcane.iris.engine.decorator.IrisSeaSurfaceDecorator;
import static art.arcane.iris.engine.mantle.EngineMantle.AIR;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineAssignedModifier;
import art.arcane.iris.engine.framework.EngineDecorator;
import art.arcane.iris.engine.object.FloatingIslandSample;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisFloatingChildBiomes;
import art.arcane.iris.util.common.data.B;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.matter.MatterBiomeInject;
import art.arcane.volmlib.util.matter.slices.BiomeInjectMatter;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import org.bukkit.block.Biome;
import org.bukkit.block.data.BlockData;
public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<BlockData> {
public static final long FLOATING_BASE_SEED_SALT = 0x5EED_F107_00F1B10CL;
private static final java.util.concurrent.atomic.AtomicLong columnsChecked = new java.util.concurrent.atomic.AtomicLong();
private static final java.util.concurrent.atomic.AtomicLong samplesAccepted = new java.util.concurrent.atomic.AtomicLong();
private static final java.util.concurrent.atomic.AtomicLong decorateInvocations = new java.util.concurrent.atomic.AtomicLong();
private static final java.util.concurrent.atomic.AtomicLong decorateSkippedNotAir = new java.util.concurrent.atomic.AtomicLong();
private static final java.util.concurrent.atomic.AtomicLong decorateSkippedNoInherit = new java.util.concurrent.atomic.AtomicLong();
private static final java.util.concurrent.atomic.AtomicLong decoratePhaseColumns = new java.util.concurrent.atomic.AtomicLong();
private static final java.util.concurrent.atomic.AtomicLong decoratePlaced = new java.util.concurrent.atomic.AtomicLong();
private static final java.util.concurrent.atomic.AtomicLong decorateNoChange = new java.util.concurrent.atomic.AtomicLong();
private static final java.util.concurrent.atomic.AtomicLong decorateFloorNull = new java.util.concurrent.atomic.AtomicLong();
private static final java.util.concurrent.ConcurrentHashMap<String, java.util.concurrent.atomic.AtomicLong> floorMatHisto = new java.util.concurrent.ConcurrentHashMap<>();
private static final java.util.concurrent.atomic.AtomicLong lastReportMs = new java.util.concurrent.atomic.AtomicLong(0L);
private final RNG rng;
private final EngineDecorator surfaceDecorator;
private final EngineDecorator seaSurfaceDecorator;
public static void reportFloatingStats() {
StringBuilder topFloors = new StringBuilder();
floorMatHisto.entrySet().stream()
.sorted((a, b) -> Long.compare(b.getValue().get(), a.getValue().get()))
.limit(5)
.forEach(e -> topFloors.append(' ').append(e.getKey()).append('=').append(e.getValue().get()));
art.arcane.iris.Iris.info("[floating-debug] columns=" + columnsChecked.get()
+ " samples=" + samplesAccepted.get()
+ " decInvoke=" + decorateInvocations.get()
+ " decPlaced=" + decoratePlaced.get()
+ " decNoChange=" + decorateNoChange.get()
+ " decFloorNull=" + decorateFloorNull.get()
+ " decSkipNonAir=" + decorateSkippedNotAir.get()
+ " decSkipNoInherit=" + decorateSkippedNoInherit.get()
+ " decPhaseCols=" + decoratePhaseColumns.get()
+ " topFloors:" + (topFloors.length() == 0 ? " <none>" : topFloors.toString()));
}
private static void maybeReport() {
long now = System.currentTimeMillis();
long last = lastReportMs.get();
if (now - last >= 10000L && lastReportMs.compareAndSet(last, now)) {
reportFloatingStats();
}
}
public IrisFloatingChildBiomeModifier(Engine engine) {
super(engine, "FloatingChildBiomes");
rng = new RNG(engine.getSeedManager().getTerrain() ^ 0x7EB0A73F1DCE514DL);
surfaceDecorator = new IrisFloatingSurfaceDecorator(engine);
seaSurfaceDecorator = new IrisSeaSurfaceDecorator(engine);
}
@Override
public void onModify(int x, int z, Hunk<BlockData> output, boolean multicore, ChunkContext context) {
PrecisionStopwatch p = PrecisionStopwatch.start();
int chunkHeight = output.getHeight();
IrisData data = getData();
IrisDimension dimension = getDimension();
IrisComplex complex = getComplex();
long baseSeed = getEngine().getSeedManager().getTerrain() ^ FLOATING_BASE_SEED_SALT;
for (int xf = 0; xf < 16; xf++) {
for (int zf = 0; zf < 16; zf++) {
int wx = x + xf;
int wz = z + zf;
IrisBiome parent = complex.getTrueBiomeStream().get(wx, wz);
if (parent == null || parent.getFloatingChildBiomes() == null || parent.getFloatingChildBiomes().isEmpty()) {
continue;
}
columnsChecked.incrementAndGet();
FloatingIslandSample sample = FloatingIslandSample.sampleMemoized(parent, wx, wz, chunkHeight, baseSeed, data, complex, getEngine());
if (sample == null) {
continue;
}
samplesAccepted.incrementAndGet();
IrisFloatingChildBiomes entry = sample.entry;
IrisBiome target = entry.getRealBiome(parent, data);
long colSeed = FloatingIslandSample.columnSeed(baseSeed, wx, wz);
RNG layerRng = rng.nextParallelRNG((int) (colSeed ^ 0x7A4E));
int paletteDepth = Math.max(4, sample.solidCount + 4);
KList<BlockData> blocks = target.generateLayers(dimension, wx, wz, layerRng, paletteDepth, paletteDepth, data, complex);
if (blocks == null || blocks.isEmpty()) {
blocks = parent.generateLayers(dimension, wx, wz, layerRng, paletteDepth, paletteDepth, data, complex);
}
BlockData fallbackSolid = B.get("minecraft:stone");
int depth = 0;
for (int k = sample.topIdx; k >= 0; k--) {
if (!sample.solidMask[k]) {
continue;
}
int y = sample.islandBaseY + k;
if (y < 0 || y >= chunkHeight) {
continue;
}
BlockData block = null;
if (blocks != null && !blocks.isEmpty()) {
block = blocks.hasIndex(depth) ? blocks.get(depth) : blocks.getLast();
}
if (block == null) {
block = fallbackSolid;
}
if (block != null) {
output.set(xf, y, zf, block);
}
depth++;
}
Integer localFluidHeight = entry.getLocalFluidHeight();
if (localFluidHeight != null && localFluidHeight > 0) {
BlockData fluid = B.get(entry.getFluidBlock());
if (fluid == null) {
fluid = B.get("minecraft:water");
}
int fluidCap = Math.min(sample.thickness - 1, localFluidHeight);
for (int k = 1; k <= fluidCap; k++) {
if (sample.solidMask[k]) {
continue;
}
int y = sample.islandBaseY + k;
if (y < 0 || y >= chunkHeight) {
continue;
}
boolean hasSolidBelow = false;
for (int kb = k - 1; kb >= 0; kb--) {
if (sample.solidMask[kb]) {
hasSolidBelow = true;
break;
}
}
if (hasSolidBelow) {
output.set(xf, y, zf, fluid);
}
}
}
if (target != null) {
writeIslandSkyBiome(target, wx, wz, sample, chunkHeight);
}
}
}
getEngine().getMetrics().getDeposit().put(p.getMilliseconds());
}
public void decorateColumns(int x, int z, Hunk<BlockData> output, boolean multicore, ChunkContext context) {
int chunkHeight = output.getHeight();
IrisData data = getData();
IrisComplex complex = getComplex();
long baseSeed = getEngine().getSeedManager().getTerrain() ^ FLOATING_BASE_SEED_SALT;
for (int xf = 0; xf < 16; xf++) {
for (int zf = 0; zf < 16; zf++) {
int wx = x + xf;
int wz = z + zf;
IrisBiome parent = complex.getTrueBiomeStream().get(wx, wz);
if (parent == null || parent.getFloatingChildBiomes() == null || parent.getFloatingChildBiomes().isEmpty()) {
continue;
}
FloatingIslandSample sample = FloatingIslandSample.sampleMemoized(parent, wx, wz, chunkHeight, baseSeed, data, complex, getEngine());
if (sample == null) {
continue;
}
decoratePhaseColumns.incrementAndGet();
IrisFloatingChildBiomes entry = sample.entry;
IrisBiome target = entry.getRealBiome(parent, data);
if (!entry.isInheritDecorators() || target == null) {
decorateSkippedNoInherit.incrementAndGet();
continue;
}
int topY = sample.topY();
int max = Math.max(1, chunkHeight - topY);
if (topY + 1 < chunkHeight) {
BlockData above = output.get(xf, topY + 1, zf);
if (above == null || B.isAir(above)) {
decorateInvocations.incrementAndGet();
BlockData floor = topY >= 0 && topY < chunkHeight ? output.get(xf, topY, zf) : null;
if (floor == null) {
decorateFloorNull.incrementAndGet();
} else {
String matKey = floor.getMaterial().getKey().getKey();
floorMatHisto.computeIfAbsent(matKey, k -> new java.util.concurrent.atomic.AtomicLong()).incrementAndGet();
}
try {
surfaceDecorator.decorate(xf, zf, wx, wz, output, target, topY, max);
} catch (Throwable e) {
art.arcane.iris.Iris.reportError(e);
}
BlockData afterAbove = output.get(xf, topY + 1, zf);
if (afterAbove != null && !B.isAir(afterAbove)) {
decoratePlaced.incrementAndGet();
} else {
decorateNoChange.incrementAndGet();
}
} else {
decorateSkippedNotAir.incrementAndGet();
}
}
Integer localFluidHeight = entry.getLocalFluidHeight();
if (localFluidHeight != null && localFluidHeight > 0) {
int fluidCap = Math.min(sample.thickness - 1, localFluidHeight);
int fluidTopY = -1;
for (int k = 1; k <= fluidCap; k++) {
if (sample.solidMask[k]) {
continue;
}
int y = sample.islandBaseY + k;
if (y < 0 || y >= chunkHeight) {
continue;
}
boolean hasSolidBelow = false;
for (int kb = k - 1; kb >= 0; kb--) {
if (sample.solidMask[kb]) {
hasSolidBelow = true;
break;
}
}
if (hasSolidBelow && y > fluidTopY) {
fluidTopY = y;
}
}
if (fluidTopY > 0 && fluidTopY + 1 < chunkHeight && B.isAir(output.get(xf, fluidTopY + 1, zf))) {
try {
seaSurfaceDecorator.decorate(xf, zf,
wx, wx + 1, wx - 1,
wz, wz + 1, wz - 1,
output, target, fluidTopY, chunkHeight);
} catch (Throwable e) {
art.arcane.iris.Iris.reportError(e);
}
}
}
}
}
maybeReport();
}
private void writeIslandSkyBiome(IrisBiome target, int wx, int wz, FloatingIslandSample sample, int chunkHeight) {
try {
MatterBiomeInject matter;
if (target.isCustom()) {
IrisBiomeCustom custom = target.getCustomBiome(rng, wx, 0, wz);
matter = BiomeInjectMatter.get(INMS.get().getBiomeBaseIdForKey(getDimension().getLoadKey() + ":" + custom.getId()));
} else {
Biome v = target.getSkyBiome(rng, wx, 0, wz);
matter = BiomeInjectMatter.get(v);
}
int yFrom = Math.max(0, sample.islandBaseY);
int yTo = Math.min(chunkHeight - 1, sample.islandBaseY + sample.topIdx);
for (int y = yFrom; y <= yTo; y += 4) {
getEngine().getMantle().getMantle().set(wx, y, wz, matter);
}
} catch (Throwable e) {
art.arcane.iris.Iris.reportError(e);
}
}
}
@@ -0,0 +1,304 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine.object;
import art.arcane.iris.Iris;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.volmlib.util.collection.KList;
import java.util.HashMap;
import java.util.concurrent.atomic.AtomicBoolean;
public final class FloatingIslandSample {
public static final int REJECT_NONE = 0;
public static final int REJECT_NO_ENTRIES = 1;
public static final int REJECT_NO_SEED = 2;
public static final int REJECT_NO_PICK = 3;
public static final int REJECT_ABOVE_HEIGHT = 4;
public static final int REJECT_NO_THICKNESS = 5;
public static final int REJECT_NO_SOLID = 6;
public static final int REJECT_COUNT = 7;
public static final int REJECT_CLUSTER = REJECT_NO_SEED;
private static final ThreadLocal<int[]> LAST_REJECT = ThreadLocal.withInitial(() -> new int[1]);
private static final ThreadLocal<double[]> LAST_DENSITY = ThreadLocal.withInitial(() -> new double[2]);
private static final ThreadLocal<HashMap<Long, FloatingIslandSample>> CHUNK_MEMO = ThreadLocal.withInitial(HashMap::new);
private static final AtomicBoolean NULL_CNG_WARNED = new AtomicBoolean(false);
public static int getLastReject() {
return LAST_REJECT.get()[0];
}
public static double getLastClusterValue() {
return LAST_DENSITY.get()[0];
}
public static double getLastClusterThreshold() {
return LAST_DENSITY.get()[1];
}
public static void clearThreadCaches() {
}
public static void clearChunkMemo() {
CHUNK_MEMO.get().clear();
}
public static FloatingIslandSample sampleMemoized(IrisBiome parent, int wx, int wz, int chunkHeight, long baseSeed, IrisData data, IrisComplex complex, Engine engine) {
long key = (((long) wx) << 32) ^ (wz & 0xFFFFFFFFL);
HashMap<Long, FloatingIslandSample> memo = CHUNK_MEMO.get();
if (memo.containsKey(key)) {
return memo.get(key);
}
FloatingIslandSample result = sample(parent, wx, wz, chunkHeight, baseSeed, data, complex, engine);
memo.put(key, result);
return result;
}
private static FloatingIslandSample reject(int code) {
LAST_REJECT.get()[0] = code;
return null;
}
private static void warnNullCng(String styleField, IrisBiome parent) {
if (NULL_CNG_WARNED.compareAndSet(false, true)) {
String biomeKey = parent == null ? "<unknown>" : parent.getLoadKey();
Iris.warn("Floating child biome on " + biomeKey + " has a null CNG for " + styleField
+ " (style factory returned null or AtomicCache swallowed an exception); skipping floating sampling until pack is fixed");
}
}
public final IrisFloatingChildBiomes entry;
public final int islandBaseY;
public final int thickness;
public final int topIdx;
public final int solidCount;
public final boolean[] solidMask;
private FloatingIslandSample(IrisFloatingChildBiomes entry, int islandBaseY, int thickness, int topIdx, int solidCount, boolean[] solidMask) {
this.entry = entry;
this.islandBaseY = islandBaseY;
this.thickness = thickness;
this.topIdx = topIdx;
this.solidCount = solidCount;
this.solidMask = solidMask;
}
public int topY() {
return islandBaseY + topIdx;
}
public static long columnSeed(long baseSeed, int wx, int wz) {
return baseSeed ^ ((long) wx * 341873128712L) ^ ((long) wz * 132897987541L);
}
public static FloatingIslandSample sample(IrisBiome parent, int wx, int wz, int chunkHeight, long baseSeed, IrisData data, IrisComplex complex, Engine engine) {
KList<IrisFloatingChildBiomes> entries = parent.getFloatingChildBiomes();
if (entries == null || entries.isEmpty()) {
return reject(REJECT_NO_ENTRIES);
}
IrisFloatingChildBiomes entry;
if (entries.size() == 1) {
entry = entries.getFirst();
} else {
IrisFloatingChildBiomes reference = entries.getFirst();
CNG picker = reference.getPickerCng(baseSeed, data);
if (picker == null) {
warnNullCng("pickerStyle", parent);
return reject(REJECT_NO_PICK);
}
double pickerValue = picker.noise(wx, wz);
double clamped = Math.max(0, Math.min(1, pickerValue));
entry = IRare.pick(entries, clamped);
if (entry == null) {
return reject(REJECT_NO_PICK);
}
}
CNG footprintCng = entry.getFootprintCng(baseSeed, data);
if (footprintCng == null) {
warnNullCng("footprintStyle", parent);
return reject(REJECT_NO_SEED);
}
double footprintValue = footprintCng.noise(wx, wz);
double signed = (Math.max(0, Math.min(1, footprintValue)) * 2.0) - 1.0;
double threshold = Math.max(0, Math.min(1, entry.getFootprintThreshold()));
double signedCut = (threshold * 2.0) - 1.0;
double[] diag = LAST_DENSITY.get();
diag[0] = signed;
diag[1] = signedCut;
if (signed <= signedCut) {
return reject(REJECT_NO_SEED);
}
int surfaceY = (int) Math.round(complex.getHeightStream().get(wx & ~63, wz & ~63));
CNG altitudeCng = entry.getAltitudeCng(baseSeed, data);
if (altitudeCng == null) {
warnNullCng("altitudeStyle", parent);
return reject(REJECT_NO_SEED);
}
double altNoise = altitudeCng.noise(wx, wz);
double altClamped = Math.max(0, Math.min(1, altNoise));
int minAlt = Math.max(0, entry.getMinHeightAboveSurface());
int maxAlt = Math.max(minAlt, entry.getMaxHeightAboveSurface());
int baseY = surfaceY + minAlt + (int) Math.round(altClamped * (maxAlt - minAlt));
IrisBiome target = entry.getRealBiome(parent, data);
int topH = computeTopHeight(entry, target, engine, baseSeed, wx, wz, data);
int topY = baseY + topH;
double edge = (signed - signedCut) / 0.15;
double edgeClamped = Math.max(0, Math.min(1, edge));
double edgeFade = edgeClamped * edgeClamped * (3.0 - 2.0 * edgeClamped);
CNG bottomCng = entry.getBottomCng(baseSeed, data);
if (bottomCng == null) {
warnNullCng("bottomStyle", parent);
return reject(REJECT_NO_SEED);
}
double bottomNoise = bottomCng.noise(wx, wz);
double bottomClamped = Math.max(0, Math.min(1, bottomNoise));
double bottomShaped = Math.pow(bottomClamped, Math.max(0.1, entry.getBottomExponent()));
int minDepth = Math.max(0, entry.getBottomDepthMin());
int maxDepth = Math.max(minDepth, entry.getBottomDepthMax());
int depth = minDepth + (int) Math.round(bottomShaped * (maxDepth - minDepth) * edgeFade);
int botY = baseY - depth;
Integer minAbsoluteY = entry.getMinAbsoluteY();
if (minAbsoluteY != null && botY < minAbsoluteY) {
botY = minAbsoluteY;
}
Integer maxAbsoluteY = entry.getMaxAbsoluteY();
if (maxAbsoluteY != null && topY > maxAbsoluteY) {
topY = maxAbsoluteY;
}
if (botY < 0) {
botY = 0;
}
if (topY >= chunkHeight) {
topY = chunkHeight - 1;
}
if (topY < botY) {
return reject(REJECT_ABOVE_HEIGHT);
}
int thickness = topY - botY + 1;
int maxThickness = Math.max(1, entry.getMaxThickness());
if (thickness > maxThickness) {
botY = topY - maxThickness + 1;
if (botY < 0) {
botY = 0;
}
thickness = topY - botY + 1;
}
if (thickness <= 0) {
return reject(REJECT_NO_THICKNESS);
}
boolean[] solidMask = new boolean[thickness];
CNG wallWarp = entry.getWallWarpCng(baseSeed, data);
double warpAmp = Math.max(0, entry.getWallWarpAmplitude());
CNG carve = entry.getCarveCng(baseSeed, data);
double carveThreshold = entry.getCarveThreshold();
boolean useWarp = wallWarp != null && warpAmp > 0;
boolean useCarve = carve != null && carveThreshold < 1.0;
int solidCount = 0;
int highestSolidIdx = -1;
for (int k = 0; k < thickness; k++) {
int wy = botY + k;
double sx = wx;
double sz = wz;
if (useWarp) {
double wnX = wallWarp.noise(wx, wy, wz);
double signedWarpX = (Math.max(0, Math.min(1, wnX)) * 2.0) - 1.0;
sx = wx + signedWarpX * warpAmp;
double wnZ = wallWarp.noise(wx + 1987.3, wy, wz + 2341.1);
double signedWarpZ = (Math.max(0, Math.min(1, wnZ)) * 2.0) - 1.0;
sz = wz + signedWarpZ * warpAmp;
}
double layerFoot = footprintCng.noise(sx, sz);
double layerSigned = (Math.max(0, Math.min(1, layerFoot)) * 2.0) - 1.0;
if (layerSigned <= signedCut) {
continue;
}
if (useCarve) {
double cn = carve.noise(wx, wy, wz);
double cnClamped = Math.max(0, Math.min(1, cn));
if (cnClamped > carveThreshold) {
continue;
}
}
solidMask[k] = true;
solidCount++;
if (k > highestSolidIdx) {
highestSolidIdx = k;
}
}
if (solidCount == 0 || highestSolidIdx < 0) {
return reject(REJECT_NO_SOLID);
}
int topIdx = highestSolidIdx;
LAST_REJECT.get()[0] = REJECT_NONE;
return new FloatingIslandSample(entry, botY, thickness, topIdx, solidCount, solidMask);
}
private static int computeTopHeight(IrisFloatingChildBiomes entry, IrisBiome target, Engine engine, long baseSeed, int wx, int wz, IrisData data) {
int maxTopHeight = Math.max(0, entry.getMaxTopHeight());
if (maxTopHeight == 0) {
return 0;
}
return switch (entry.getTopShapeMode()) {
case FLAT -> maxTopHeight;
case NOISE -> {
CNG topCng = entry.getTopShapeCng(baseSeed, data);
if (topCng == null) {
warnNullCng("topShapeStyle", null);
yield maxTopHeight / 2;
}
double n = topCng.noise(wx, wz);
double clamped = Math.max(0, Math.min(1, n));
double amp = Math.max(0, Math.min(1, entry.getTopShapeAmp()));
yield (int) Math.round(clamped * amp * maxTopHeight);
}
case BIOME -> {
if (target == null) {
yield maxTopHeight / 2;
}
double h = target.getHeight(engine, wx, wz, baseSeed);
int rounded = (int) Math.round(h);
if (rounded < 0) {
yield 0;
}
yield Math.min(maxTopHeight, rounded);
}
};
}
}
@@ -26,6 +26,7 @@ import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.annotations.*;
import com.google.gson.annotations.SerializedName;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.collection.KSet;
@@ -106,9 +107,6 @@ public class IrisBiome extends IrisRegistrant implements IRare {
private IrisCaveProfile caveProfile = new IrisCaveProfile();
@Desc("Configuration of fluid bodies such as rivers & lakes")
private IrisFluidBodies fluidBodies = new IrisFluidBodies();
@ArrayType(type = IrisExternalDatapackBinding.class, min = 1)
@Desc("Scoped external datapack bindings for this biome")
private KList<IrisExternalDatapackBinding> externalDatapacks = new KList<>();
@MinNumber(1)
@MaxNumber(512)
@Desc("The rarity of this biome (integer)")
@@ -165,6 +163,9 @@ public class IrisBiome extends IrisRegistrant implements IRare {
@ArrayType(min = 1, type = IrisObjectPlacement.class)
@Desc("Objects define what schematics (iob files) iris will place in this biome")
private KList<IrisObjectPlacement> objects = new KList<>();
@ArrayType(min = 1, type = IrisFloatingChildBiomes.class)
@Desc("Floating child biomes that procedurally generate above this biome's terrain. Each entry references a target biome whose layers, decorators, and objects drive the floating island's visual design, while the config here drives size, shape, altitude, rarity, and water level. Multiple entries are supported and selected by rarity per column.")
private KList<IrisFloatingChildBiomes> floatingChildBiomes = new KList<>();
@Required
@ArrayType(min = 1, type = IrisBiomeGeneratorLink.class)
@Desc("Generators for this biome. Multiple generators with different interpolation sizes will mix with other biomes how you would expect. This defines your biome height relative to the fluid height. Use negative for oceans.")
@@ -29,6 +29,7 @@ import art.arcane.iris.core.nms.datapack.IDataFixer.Dimension;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.object.annotations.*;
import art.arcane.iris.engine.object.annotations.functions.ComponentFlagFunction;
import com.google.gson.annotations.SerializedName;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.collection.KSet;
@@ -154,11 +155,6 @@ public class IrisDimension extends IrisRegistrant {
private IrisCaveProfile caveProfile = new IrisCaveProfile();
@Desc("Configuration of fluid bodies such as rivers & lakes")
private IrisFluidBodies fluidBodies = new IrisFluidBodies();
@Desc("Enable or disable vanilla structure generation from the extracted vanilla datapack. When disabled, no vanilla structures spawn. When enabled, structures come from the vanilla datapack and can be overridden by external datapacks.")
private boolean vanillaStructures = true;
@ArrayType(type = IrisExternalDatapack.class, min = 1)
@Desc("Pack-scoped external datapack sources for structure import and optional vanilla replacement")
private KList<IrisExternalDatapack> externalDatapacks = new KList<>();
@Desc("forceConvertTo320Height")
private Boolean forceConvertTo320Height = false;
@Desc("The world environment")
@@ -190,6 +186,8 @@ public class IrisDimension extends IrisRegistrant {
private boolean upperDimensionCarving = false;
@Desc("When true, objects from the mantle (structures, trees, etc.) can be placed in the upper dimension terrain zone. When false, the upper terrain is protected from object placement.")
private boolean upperDimensionObjects = false;
@Desc("When true, upper-dimension objects force-place regardless of placement restrictions (slope, underwater, clamp, collisions, carving). Normal dimension objects always place first; upper objects place second and may clip or occlude lower-dimension placements when this is enabled.")
private boolean upperObjectsForcePlace = false;
@RegistryListResource(IrisBiome.class)
@Desc("Keep this either undefined or empty. Setting any biome name into this will force iris to only generate the specified biome. Great for testing.")
private String focus = "";
@@ -241,12 +239,6 @@ public class IrisDimension extends IrisRegistrant {
@ArrayType(min = 1, type = IrisShapedGeneratorStyle.class)
@Desc("Overlay additional noise on top of the interoplated terrain.")
private KList<IrisShapedGeneratorStyle> overlayNoise = new KList<>();
@Desc("If true, the spawner system has infinite energy. This is NOT recommended because it would allow for mobs to keep spawning over and over without a rate limit")
private boolean infiniteEnergy = false;
@MinNumber(0)
@MaxNumber(10000)
@Desc("This is the maximum energy you can have in a dimension")
private double maximumEnergy = 1000;
@MinNumber(0.0001)
@MaxNumber(512)
@Desc("The rock zoom mostly for zooming in on a wispy palette")
@@ -468,7 +460,6 @@ public class IrisDimension extends IrisRegistrant {
}
public void installBiomes(IDataFixer fixer, DataProvider data, KList<File> folders, KSet<String> biomes) {
KMap<String, String> customBiomeToVanillaBiome = new KMap<>();
String namespace = getLoadKey().toLowerCase(Locale.ROOT);
for (IrisBiome irisBiome : getAllBiomes(data)) {
@@ -476,13 +467,8 @@ public class IrisDimension extends IrisRegistrant {
continue;
}
Biome vanillaDerivative = irisBiome.getVanillaDerivative();
NamespacedKey vanillaDerivativeKey = vanillaDerivative == null ? null : vanillaDerivative.getKey();
String vanillaBiomeKey = vanillaDerivativeKey == null ? null : vanillaDerivativeKey.toString();
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
String customBiomeId = customBiome.getId();
String customBiomeKey = namespace + ":" + customBiomeId.toLowerCase(Locale.ROOT);
String json = customBiome.generateJson(fixer);
synchronized (biomes) {
@@ -492,10 +478,6 @@ public class IrisDimension extends IrisRegistrant {
}
}
if (vanillaBiomeKey != null) {
customBiomeToVanillaBiome.put(customBiomeKey, vanillaBiomeKey);
}
for (File datapacks : folders) {
File output = new File(datapacks, "iris/data/" + namespace + "/worldgen/biome/" + customBiomeId + ".json");
@@ -510,126 +492,6 @@ public class IrisDimension extends IrisRegistrant {
}
}
}
installStructureBiomeTags(folders, customBiomeToVanillaBiome);
}
private void installStructureBiomeTags(KList<File> folders, KMap<String, String> customBiomeToVanillaBiome) {
if (customBiomeToVanillaBiome.isEmpty()) {
return;
}
KMap<String, KList<String>> vanillaTags = INMS.get().getVanillaStructureBiomeTags();
if (vanillaTags == null || vanillaTags.isEmpty()) {
return;
}
KMap<String, KSet<String>> customTagValues = new KMap<>();
for (Map.Entry<String, String> customBiomeEntry : customBiomeToVanillaBiome.entrySet()) {
String customBiomeKey = customBiomeEntry.getKey();
String vanillaBiomeKey = customBiomeEntry.getValue();
if (vanillaBiomeKey == null) {
continue;
}
for (Map.Entry<String, KList<String>> tagEntry : vanillaTags.entrySet()) {
KList<String> values = tagEntry.getValue();
if (values == null || !values.contains(vanillaBiomeKey)) {
continue;
}
customTagValues.computeIfAbsent(tagEntry.getKey(), key -> new KSet<>()).add(customBiomeKey);
}
}
if (customTagValues.isEmpty()) {
return;
}
for (File datapacks : folders) {
for (Map.Entry<String, KSet<String>> tagEntry : customTagValues.entrySet()) {
String tagPath = tagEntry.getKey();
KSet<String> customValues = tagEntry.getValue();
if (customValues == null || customValues.isEmpty()) {
continue;
}
File output = new File(datapacks, "iris/data/minecraft/tags/worldgen/biome/" + tagPath + ".json");
try {
writeMergedStructureBiomeTag(output, customValues);
} catch (IOException e) {
Iris.reportError(e);
e.printStackTrace();
}
}
}
}
private void writeMergedStructureBiomeTag(File output, KSet<String> customValues) throws IOException {
synchronized (IrisDimension.class) {
KSet<String> mergedValues = readExistingStructureBiomeTagValues(output);
mergedValues.addAll(customValues);
JSONArray values = new JSONArray();
KList<String> sortedValues = new KList<>(mergedValues).sort();
for (String value : sortedValues) {
values.put(value);
}
JSONObject json = new JSONObject();
json.put("replace", false);
json.put("values", values);
writeAtomicFile(output, json.toString(4));
}
}
private KSet<String> readExistingStructureBiomeTagValues(File output) {
KSet<String> values = new KSet<>();
if (output == null || !output.exists()) {
return values;
}
try {
JSONObject json = new JSONObject(IO.readAll(output));
if (!json.has("values")) {
return values;
}
JSONArray existingValues = json.getJSONArray("values");
for (int index = 0; index < existingValues.length(); index++) {
Object rawValue = existingValues.get(index);
if (rawValue == null) {
continue;
}
String value = String.valueOf(rawValue).trim();
if (!value.isEmpty()) {
values.add(value);
}
}
} catch (Throwable e) {
Iris.warn("Skipping malformed existing structure biome tag file: " + output.getPath());
}
return values;
}
private void writeAtomicFile(File output, String contents) throws IOException {
File parent = output.getParentFile();
if (parent != null && !parent.exists()) {
parent.mkdirs();
}
File temp = new File(parent, output.getName() + ".tmp-" + System.nanoTime());
IO.writeAll(temp, contents);
Path tempPath = temp.toPath();
Path outputPath = output.toPath();
try {
Files.move(tempPath, outputPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException e) {
Files.move(tempPath, outputPath, StandardCopyOption.REPLACE_EXISTING);
}
}
public Dimension getBaseDimension() {
@@ -24,16 +24,13 @@ import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.annotations.*;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.scheduling.ChronoLatch;
import art.arcane.iris.util.common.reflect.KeyedType;
import art.arcane.iris.util.common.scheduling.J;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.bukkit.Location;
import org.bukkit.NamespacedKey;
import org.bukkit.Particle;
import org.bukkit.Registry;
import org.bukkit.Sound;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
@@ -41,8 +38,6 @@ import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import org.bukkit.util.Vector;
import java.util.Locale;
@Snippet("effect")
@Accessors(chain = true)
@NoArgsConstructor
@@ -52,6 +47,7 @@ import java.util.Locale;
public class IrisEffect {
private final transient AtomicCache<PotionEffectType> pt = new AtomicCache<>();
private final transient AtomicCache<ChronoLatch> latch = new AtomicCache<>();
@RegistryListPotionEffect
@Desc("The potion effect to apply in this area")
private String potionEffect = "";
@Desc("The particle effect to apply in the area")
@@ -162,29 +158,24 @@ public class IrisEffect {
public PotionEffectType getRealType() {
return pt.aquire(() ->
{
PotionEffectType t = PotionEffectType.LUCK;
if (getPotionEffect().isEmpty()) {
return t;
if (getPotionEffect() == null || getPotionEffect().isEmpty()) {
return PotionEffectType.LUCK;
}
try {
for (PotionEffectType i : Registry.EFFECT) {
NamespacedKey key = KeyedType.getKey(i);
if (key != null && key.getKey().toUpperCase(Locale.ROOT).replaceAll("\\Q \\E", "_").equals(getPotionEffect())) {
t = i;
return t;
}
PotionEffectType resolved = PotionEffectTypes.resolve(getPotionEffect());
if (resolved != null) {
return resolved;
}
} catch (Throwable e) {
Iris.reportError(e);
}
Iris.warn("Unknown Potion Effect Type: " + getPotionEffect());
if (PotionEffectTypes.shouldWarn(getPotionEffect())) {
Iris.warn("Unknown Potion Effect Type: \"" + getPotionEffect() + "\". Valid types: " + PotionEffectTypes.knownTypesList());
}
return t;
return PotionEffectType.LUCK;
});
}
@@ -52,8 +52,6 @@ public class IrisEntitySpawn implements IRare {
@Required
@Desc("The entity")
private String entity = "";
@Desc("The energy multiplier when calculating spawn energy usage")
private double energyMultiplier = 1;
@MinNumber(1)
@Desc("The 1 in RARITY chance for this entity to spawn")
private int rarity = 1;
@@ -1,29 +0,0 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.object.annotations.Desc;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@Desc("Defines an external datapack source. When replace is true, minecraft namespace entries override the vanilla datapack.")
public class IrisExternalDatapack {
@Desc("Stable id for this external datapack entry")
private String id = "";
@Desc("Datapack source URL. Modrinth version page URLs are supported.")
private String url = "";
@Desc("Enable or disable this external datapack entry")
private boolean enabled = true;
@Desc("If true, Iris hard-fails startup when this external datapack cannot be synced/imported/installed")
private boolean required = false;
@Desc("If true, this datapack replaces vanilla worldgen entries. The datapack itself determines what it overrides.")
private boolean replace = false;
}
@@ -1,29 +0,0 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.object.annotations.Desc;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@Desc("Scoped binding to a dimension external datapack id")
public class IrisExternalDatapackBinding {
@Desc("Target external datapack id defined on the dimension")
private String id = "";
@Desc("Enable or disable this scoped binding")
private boolean enabled = true;
@Desc("Override replace behavior for this scoped binding (null keeps dimension default)")
private Boolean replaceOverride = null;
@Desc("Include child biomes recursively when collecting scoped biome boundaries")
private boolean includeChildren = true;
@Desc("Override required behavior for this scoped binding (null keeps dimension default)")
private Boolean requiredOverride = null;
}
@@ -0,0 +1,240 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine.object;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.object.annotations.ArrayType;
import art.arcane.iris.engine.object.annotations.Desc;
import art.arcane.iris.engine.object.annotations.MaxNumber;
import art.arcane.iris.engine.object.annotations.MinNumber;
import art.arcane.iris.engine.object.annotations.RegistryListResource;
import art.arcane.iris.engine.object.annotations.Snippet;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Snippet("floating-child-biome")
@Accessors(chain = true)
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
@Desc("Declares a floating biome layer above this biome's terrain. A 2D footprint noise decides which columns are part of the island (threshold + style control blankets, swirls, scattered blobs). The top profile is driven by the target biome's own terrain generators (so a mountains biome produces real peaks, desert produces real dunes). The bottom tail hanging below is a separately configurable noise (pick VASCULAR for drippy roots, FRACTAL_RM_SIMPLEX for crystalline spikes, PERLIN for smooth rounded bowls).")
@Data
public class IrisFloatingChildBiomes implements IRare {
private final transient AtomicCache<IrisBiome> resolvedBiome = new AtomicCache<>();
private final transient AtomicCache<CNG> footprintCache = new AtomicCache<>();
private final transient AtomicCache<CNG> pickerCache = new AtomicCache<>();
private final transient AtomicCache<CNG> altitudeCache = new AtomicCache<>();
private final transient AtomicCache<CNG> topShapeCache = new AtomicCache<>();
private final transient AtomicCache<CNG> bottomCache = new AtomicCache<>();
private final transient AtomicCache<CNG> wallWarpCache = new AtomicCache<>();
private final transient AtomicCache<CNG> carveCache = new AtomicCache<>();
private final transient AtomicCache<IrisObjectScale> shrinkScaleCache = new AtomicCache<>();
public CNG getFootprintCng(long baseSeed, IrisData data) {
return footprintCache.aquire(() -> getFootprintStyle().create(new RNG(baseSeed ^ 0xF007B17DL), data));
}
public CNG getPickerCng(long baseSeed, IrisData data) {
return pickerCache.aquire(() -> getPickerStyle().create(new RNG(baseSeed ^ 0x91C4E72DL), data));
}
public CNG getAltitudeCng(long baseSeed, IrisData data) {
return altitudeCache.aquire(() -> getAltitudeStyle().create(new RNG(baseSeed ^ 0xA17DEBBL), data));
}
public CNG getTopShapeCng(long baseSeed, IrisData data) {
return topShapeCache.aquire(() -> getTopShapeStyle().create(new RNG(baseSeed ^ 0x70970601DEFL), data));
}
public CNG getBottomCng(long baseSeed, IrisData data) {
return bottomCache.aquire(() -> getBottomStyle().create(new RNG(baseSeed ^ 0xB0770075CAFEL), data));
}
public CNG getWallWarpCng(long baseSeed, IrisData data) {
IrisGeneratorStyle style = getWallWarpStyle();
if (style == null) {
return null;
}
return wallWarpCache.aquire(() -> style.create(new RNG(baseSeed ^ 0xA117BA17E0FL), data));
}
public CNG getCarveCng(long baseSeed, IrisData data) {
IrisGeneratorStyle style = getCarveStyle();
if (style == null) {
return null;
}
return carveCache.aquire(() -> style.create(new RNG(baseSeed ^ 0xCA5EC1EE5EL), data));
}
@RegistryListResource(IrisBiome.class)
@Desc("The target biome whose visual design (layers, palette, decorators, surface objects, derivative, and — when topShapeMode=BIOME — generator profile) drives the floating island. Leave empty to reuse the parent biome (self).")
private String biome = "";
@MinNumber(1)
@MaxNumber(512)
@Desc("Selection rarity when multiple floating child entries are defined on one parent biome. Lower is more common.")
private int rarity = 1;
@Desc("2D noise that decides which columns are part of this island. Set feature size via the style's own zoom field (e.g. {\"style\":\"CELLULAR\",\"zoom\":0.3} for ~30-block shards, {\"style\":\"SIMPLEX\",\"zoom\":1.0} for ~100-block blobs). Pick SIMPLEX for smooth blobs, CELLULAR for angular shards, VASCULAR for vein/branch strips, FRACTAL_FBM_SIMPLEX for large irregular blanket regions. Fracture (domain warp) this to get swirly silhouettes.")
private IrisGeneratorStyle footprintStyle = NoiseStyle.SIMPLEX.style();
@MinNumber(0)
@MaxNumber(1)
@Desc("Coverage threshold (0..1). Roughly the fraction of the world that is NOT island: 0.0 = every column becomes island, 0.5 ≈ 50% of columns, 0.8 ≈ sparse scattered ~20% coverage, 1.0 = no islands at all. Values near 0.5 feel most natural.")
private double footprintThreshold = 0.5;
@Desc("Picker noise used when multiple floating child entries exist. Samples once per column to deterministically choose which entry's footprint is tested there. Use a large style zoom (e.g. zoom: 4 for ~400-block regions) so each entry owns broad coherent areas.")
private IrisGeneratorStyle pickerStyle = NoiseStyle.SIMPLEX.style();
@Desc("Altitude noise — varies the base platform Y across one island so it isn't a flat plane. Use a large style zoom (e.g. zoom: 2 for ~200-block altitude patches) so an island sits at roughly one altitude.")
private IrisGeneratorStyle altitudeStyle = NoiseStyle.SIMPLEX.style();
@MinNumber(0)
@MaxNumber(2032)
@Desc("Minimum blocks above the parent biome surface where the island base can sit.")
private int minHeightAboveSurface = 60;
@MinNumber(0)
@MaxNumber(2032)
@Desc("Maximum blocks above the parent biome surface where the island base can sit.")
private int maxHeightAboveSurface = 110;
@Desc("Optional absolute minimum world Y for the island base. When set, baseY is clamped upward so the tail bottom stays above this value.")
private Integer minAbsoluteY = null;
@Desc("Optional absolute maximum world Y for the island top. When set, the top is clamped downward.")
private Integer maxAbsoluteY = null;
@Desc("How the top profile of the island is shaped. BIOME = evaluate target biome's own generators (mountains biome -> real mountains). NOISE = use topShapeStyle as a user-controlled heightmap. FLAT = constant maxTopHeight slab.")
private TopShapeMode topShapeMode = TopShapeMode.BIOME;
@MinNumber(0)
@MaxNumber(512)
@Desc("Maximum top profile height in blocks above the island base. Caps how tall the biome terrain can grow on top.")
private int maxTopHeight = 40;
@Desc("Used only when topShapeMode=NOISE. 2D noise driving the top heightmap. Set feature scale via the style's zoom field (small zoom = rugged peaks, large zoom = broad rolling shapes).")
private IrisGeneratorStyle topShapeStyle = NoiseStyle.SIMPLEX.style();
@MinNumber(0)
@MaxNumber(1)
@Desc("Amplitude multiplier applied to the NOISE top profile. 0 = no top (flat at base), 1 = full maxTopHeight range.")
private double topShapeAmp = 1.0;
@Desc("2D noise driving the bottom tail hanging below the island base. VASCULAR = drippy organic roots. FRACTAL_RM_SIMPLEX = crystalline spikes. CELLULAR = jagged chunks. PERLIN = smooth rounded bowl. SIMPLEX = gentle lobes. Set feature scale via the style's zoom field.")
private IrisGeneratorStyle bottomStyle = NoiseStyle.SIMPLEX.style();
@MinNumber(0)
@MaxNumber(512)
@Desc("Minimum blocks below the base where the tail extends.")
private int bottomDepthMin = 4;
@MinNumber(0)
@MaxNumber(512)
@Desc("Maximum blocks below the base where the tail extends.")
private int bottomDepthMax = 20;
@MinNumber(0.1)
@MaxNumber(8)
@Desc("Power curve applied to the bottom noise before mapping to depth. >1 = most columns shallow with occasional deeper spikes (sparse roots). <1 = most columns deep with occasional shallow spots (dense curtains). 1.0 = linear.")
private double bottomExponent = 1.0;
@MinNumber(1)
@MaxNumber(512)
@Desc("Hard cap on the total Y-extent (top minus bottom) of a single island column. Safety limit.")
private int maxThickness = 96;
@Desc("Optional 3D noise that shifts the footprint's XZ sample position per Y layer — naturalizes the walls so they stop looking like a straight extrusion of the 2D footprint. Leave null to disable and keep straight vertical walls. Good defaults: {\"style\":\"SIMPLEX\",\"zoom\":0.25} for gentle undulation, {\"style\":\"FRACTAL_FBM_SIMPLEX\",\"zoom\":0.4} for craggier walls.")
private IrisGeneratorStyle wallWarpStyle = null;
@MinNumber(0)
@MaxNumber(64)
@Desc("Amplitude in blocks of the per-layer XZ shift applied when wallWarpStyle is set. 0 = no warp (straight walls). 4..8 = gentle naturalization. 16+ = heavily meandering walls. Ignored when wallWarpStyle is null.")
private double wallWarpAmplitude = 6.0;
@Desc("Optional 3D noise that swiss-cheeses the island interior by marking individual blocks as air when the noise exceeds carveThreshold. Leave null to keep the island solid. Good defaults: {\"style\":\"CELLULAR\",\"zoom\":0.3} for bubble pockets, {\"style\":\"VASCULAR\",\"zoom\":0.25} for wormy tunnels.")
private IrisGeneratorStyle carveStyle = null;
@MinNumber(0)
@MaxNumber(1)
@Desc("Threshold (0..1) above which carveStyle noise carves air pockets. 1.0 = no carving. 0.75 = sparse pockets. 0.55 = heavy swiss-cheese. 0.4 = shredded lattice. Ignored when carveStyle is null.")
private double carveThreshold = 1.0;
@Desc("Optional water surface height above the island base, in blocks. null = no internal water. Positive = water fills any dip in the top profile up to baseY + localFluidHeight (forms lakes/ponds in concavities of the biome-top heightmap).")
private Integer localFluidHeight = null;
@Desc("Block used for the internal water pool when localFluidHeight is positive.")
private String fluidBlock = "minecraft:water";
@Desc("When true, the target biome's decorators apply to the island's top surface.")
private boolean inheritDecorators = true;
@Desc("When true, the target biome's surface objects are placed on the island's top surface instead of the parent terrain.")
private boolean inheritObjects = true;
@MinNumber(0.01)
@MaxNumber(1)
@Desc("Uniform shrink factor applied to every object placed on this floating island (inherited, extra, and free-floating). 1.0 = native size, 0.5 = half size, 0.25 = quarter. Useful for making small floating biomes feel believable.")
private double objectShrinkFactor = 1.0;
@ArrayType(min = 1, type = IrisObjectPlacement.class)
@Desc("Additional object placements anchored to the island top.")
private KList<IrisObjectPlacement> extraObjects = new KList<>();
@ArrayType(min = 1, type = IrisObjectPlacement.class)
@Desc("Additional object placements that float freely in air, independent of the island. Forced to ObjectPlaceMode.FLOATING.")
private KList<IrisObjectPlacement> floatingObjects = new KList<>();
@Desc("Visualization color for this floating child in Iris Studio.")
private String color = null;
public boolean hasObjectShrink() {
return objectShrinkFactor > 0 && objectShrinkFactor < 1.0;
}
public IrisObjectScale getShrinkScale() {
return shrinkScaleCache.aquire(() -> {
IrisObjectScale s = new IrisObjectScale();
s.setSize(Math.max(0.01, Math.min(1.0, objectShrinkFactor)));
return s;
});
}
public IrisBiome getRealBiome(IrisBiome parent, IrisData data) {
return resolvedBiome.aquire(() -> {
if (biome == null || biome.isBlank() || biome.equals(parent.getLoadKey())) {
return parent;
}
IrisBiome loaded = data.getBiomeLoader().load(biome);
if (loaded == null) {
return parent;
}
return loaded;
});
}
}
@@ -80,6 +80,8 @@ public class IrisObject extends IrisRegistrant {
protected static final BlockData VAIR = B.get("VOID_AIR");
protected static final BlockData VAIR_DEBUG = B.get("COBWEB");
protected static final BlockData[] SNOW_LAYERS = new BlockData[]{B.get("minecraft:snow[layers=1]"), B.get("minecraft:snow[layers=2]"), B.get("minecraft:snow[layers=3]"), B.get("minecraft:snow[layers=4]"), B.get("minecraft:snow[layers=5]"), B.get("minecraft:snow[layers=6]"), B.get("minecraft:snow[layers=7]"), B.get("minecraft:snow[layers=8]")};
private static final long IMPLAUSIBLE_BEDROCK_WARN_THROTTLE_MS = 5000L;
private static final java.util.concurrent.ConcurrentHashMap<String, Long> IMPLAUSIBLE_BEDROCK_WARNS = new java.util.concurrent.ConcurrentHashMap<>();
protected transient final Lock readLock;
protected transient final Lock writeLock;
@Getter
@@ -297,7 +299,7 @@ public class IrisObject extends IrisRegistrant {
public synchronized IrisObject copy() {
IrisObject o = new IrisObject(w, h, d);
o.setLoadKey(o.getLoadKey());
o.setLoadKey(getLoadKey());
o.setLoader(getLoader());
o.setLoadFile(getLoadFile());
o.setCenter(getCenter().clone());
@@ -679,6 +681,7 @@ public class IrisObject extends IrisRegistrant {
}
boolean warped = !config.getWarp().isFlat();
boolean rawStructurePiece = config.getMode() == ObjectPlaceMode.STRUCTURE_PIECE;
boolean stilting = (config.getMode().equals(ObjectPlaceMode.STILT) || config.getMode().equals(ObjectPlaceMode.FAST_STILT) ||
config.getMode() == ObjectPlaceMode.MIN_STILT || config.getMode() == ObjectPlaceMode.FAST_MIN_STILT ||
config.getMode() == ObjectPlaceMode.CENTER_STILT || config.getMode() == ObjectPlaceMode.ERODE_STILT);
@@ -819,17 +822,19 @@ public class IrisObject extends IrisRegistrant {
bail = true;
}
}
} else if (config.getMode().equals(ObjectPlaceMode.FLOATING)) {
y = rty;
}
} else {
y = yv;
if (!config.isForcePlace()) {
if (!config.isForcePlace() && !rawStructurePiece) {
if (shouldBailForCarvingAnchor(placer, config, x, y, z)) {
bail = true;
}
}
}
if (yv >= 0 && config.isBottom()) {
if (yv >= 0 && config.isBottom() && !rawStructurePiece) {
y += Math.floorDiv(h, 2);
CarvingMode carvingMode = config.getCarvingSupport();
if (!config.isForcePlace() && !carvingMode.equals(CarvingMode.CARVING_ONLY)) {
@@ -839,29 +844,42 @@ public class IrisObject extends IrisRegistrant {
}
}
if (yv < 0
&& !config.isForcePlace()
&& !config.isFromBottom()
&& config.getMode() != ObjectPlaceMode.FLOATING
&& !rawStructurePiece
&& config.getCarvingSupport().supportsSurface()
&& placer.getEngine() != null
&& placer.getEngine().getDimension().isBedrock()
&& y <= 1) {
warnImplausibleBedrockPlacement(placer, config, x, y, z);
return -1;
}
if (bail && !config.isForcePlace()) {
return -1;
}
if (yv < 0) {
if (yv < 0 && !config.getMode().equals(ObjectPlaceMode.FLOATING) && !rawStructurePiece) {
if (!config.isForcePlace() && !config.isUnderwater() && !config.isOnwater() && placer.isUnderwater(x, z)) {
return -1;
}
}
if (!config.isForcePlace() && c != null && Math.max(0, h + yrand + ty) + 1 >= c.getHeight()) {
if (!config.isForcePlace() && !rawStructurePiece && c != null && Math.max(0, h + yrand + ty) + 1 >= c.getHeight()) {
return -1;
}
if (!config.isForcePlace() && config.isUnderwater() && y + rty + ty >= placer.getFluidHeight()) {
if (!config.isForcePlace() && !rawStructurePiece && config.isUnderwater() && y + rty + ty >= placer.getFluidHeight()) {
return -1;
}
if (!config.isForcePlace() && !config.getClamp().canPlace(y + rty + ty, y - rty + ty)) {
if (!config.isForcePlace() && !rawStructurePiece && !config.getClamp().canPlace(y + rty + ty, y - rty + ty)) {
return -1;
}
if (!config.isForcePlace() && (!config.getAllowedCollisions().isEmpty() || !config.getForbiddenCollisions().isEmpty())) {
if (!config.isForcePlace() && !rawStructurePiece && (!config.getAllowedCollisions().isEmpty() || !config.getForbiddenCollisions().isEmpty())) {
Engine engine = rdata.getEngine();
BlockVector offset = new BlockVector(config.getTranslate().getX(), config.getTranslate().getY(), config.getTranslate().getZ());
for (int i = x - Math.floorDiv(w, 2) + (int) offset.getX(); i <= x + Math.floorDiv(w, 2) - (w % 2 == 0 ? 1 : 0) + (int) offset.getX(); i++) {
@@ -1019,7 +1037,7 @@ public class IrisObject extends IrisRegistrant {
}
}
if (config.isMeld() && !placer.isSolid(xx, yy, zz)) {
if (config.isMeld() && !rawStructurePiece && !placer.isSolid(xx, yy, zz)) {
continue;
}
@@ -1241,6 +1259,24 @@ public class IrisObject extends IrisRegistrant {
return y;
}
private void warnImplausibleBedrockPlacement(IObjectPlacer placer, IrisObjectPlacement config, int x, int y, int z) {
String key = getLoadKey();
String fingerprint = (key == null ? "<null>" : key) + "|" + config.getMode();
long now = System.currentTimeMillis();
Long last = IMPLAUSIBLE_BEDROCK_WARNS.get(fingerprint);
if (last != null && now - last < IMPLAUSIBLE_BEDROCK_WARN_THROTTLE_MS) {
return;
}
IMPLAUSIBLE_BEDROCK_WARNS.put(fingerprint, now);
Iris.warn("Implausible object placement rejected: "
+ (key == null ? "<no loadKey>" : key)
+ " resolved anchorY=" + y + " at (" + x + "," + z + ") mode=" + config.getMode()
+ " carving=" + config.getCarvingSupport()
+ ". Surface-anchored placement should never land on the bedrock row. "
+ "Height sampling returned a bogus value — not configured for floor placement "
+ "(forcePlace=false, fromBottom=false, mode!=FLOATING). Skipping to protect bedrock.");
}
private boolean shouldBailForCarvingAnchor(IObjectPlacer placer, IrisObjectPlacement placement, int x, int y, int z) {
boolean carved = isCarvedAnchor(placer, x, y, z);
CarvingMode carvingMode = placement.getCarvingSupport();
@@ -1339,6 +1375,9 @@ public class IrisObject extends IrisRegistrant {
}
public IrisObject scaled(double scale, IrisObjectPlacementScaleInterpolator interpolation) {
if (interpolation == null) {
interpolation = IrisObjectPlacementScaleInterpolator.NONE;
}
Vector sm1 = new Vector(scale - 1, scale - 1, scale - 1);
scale = Math.max(0.001, Math.min(50, scale));
if (scale < 1) {
@@ -1361,6 +1400,9 @@ public class IrisObject extends IrisRegistrant {
}
IrisObject oo = new IrisObject((int) Math.ceil((w * scale) + (scale * 2)), (int) Math.ceil((h * scale) + (scale * 2)), (int) Math.ceil((d * scale) + (scale * 2)));
oo.setLoadKey(getLoadKey());
oo.setLoader(getLoader());
oo.setLoadFile(getLoadFile());
readLock.lock();
for (var entry : blocks) {
@@ -89,6 +89,9 @@ public class IrisObjectScale {
}
public IrisObject get(RNG rng, IrisObject origin) {
if (origin == null) {
return null;
}
if (!shouldScale()) {
return origin;
}
@@ -21,19 +21,14 @@ package art.arcane.iris.engine.object;
import art.arcane.iris.Iris;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.object.annotations.*;
import art.arcane.iris.util.common.reflect.KeyedType;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.bukkit.NamespacedKey;
import org.bukkit.entity.LivingEntity;
import org.bukkit.Registry;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import java.util.Locale;
@Accessors(chain = true)
@NoArgsConstructor
@AllArgsConstructor
@@ -44,6 +39,7 @@ import java.util.Locale;
public class IrisPotionEffect {
private final transient AtomicCache<PotionEffectType> pt = new AtomicCache<>();
@Required
@RegistryListPotionEffect
@Desc("The potion effect to apply in this area")
private String potionEffect = "";
@Required
@@ -63,29 +59,24 @@ public class IrisPotionEffect {
public PotionEffectType getRealType() {
return pt.aquire(() ->
{
PotionEffectType t = PotionEffectType.LUCK;
if (getPotionEffect().isEmpty()) {
return t;
if (getPotionEffect() == null || getPotionEffect().isEmpty()) {
return PotionEffectType.LUCK;
}
try {
for (PotionEffectType i : Registry.EFFECT) {
NamespacedKey key = KeyedType.getKey(i);
if (key != null && key.getKey().toUpperCase(Locale.ROOT).replaceAll("\\Q \\E", "_").equals(getPotionEffect())) {
t = i;
return t;
}
PotionEffectType resolved = PotionEffectTypes.resolve(getPotionEffect());
if (resolved != null) {
return resolved;
}
} catch (Throwable e) {
Iris.reportError(e);
}
Iris.warn("Unknown Potion Effect Type: " + getPotionEffect());
if (PotionEffectTypes.shouldWarn(getPotionEffect())) {
Iris.warn("Unknown Potion Effect Type: \"" + getPotionEffect() + "\". Valid types: " + PotionEffectTypes.knownTypesList());
}
return t;
return PotionEffectType.LUCK;
});
}
@@ -24,6 +24,7 @@ import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.IrisRegistrant;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.object.annotations.*;
import com.google.gson.annotations.SerializedName;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.collection.KSet;
@@ -118,9 +119,6 @@ public class IrisRegion extends IrisRegistrant implements IRare {
private IrisCaveProfile caveProfile = new IrisCaveProfile();
@Desc("Configuration of fluid bodies such as rivers & lakes")
private IrisFluidBodies fluidBodies = new IrisFluidBodies();
@ArrayType(type = IrisExternalDatapackBinding.class, min = 1)
@Desc("Scoped external datapack bindings for this region")
private KList<IrisExternalDatapackBinding> externalDatapacks = new KList<>();
@RegistryListResource(IrisBiome.class)
@Required
@ArrayType(min = 1, type = String.class)
@@ -50,9 +50,6 @@ public class IrisSpawner extends IrisRegistrant {
@Desc("The entity spawns to add initially. EXECUTES PER CHUNK!")
private KList<IrisEntitySpawn> initialSpawns = new KList<>();
@Desc("The energy multiplier when calculating spawn energy usage")
private double energyMultiplier = 1;
@Desc("This spawner will not spawn in a given chunk if that chunk has more than the defined amount of living entities.")
private int maxEntitiesPerChunk = 1;
@@ -68,5 +68,13 @@ public enum ObjectPlaceMode {
@Desc("Samples the height of the terrain at every x,z position of your object and pushes it down to the surface. It's pretty much like a melt function over the terrain.")
PAINT
PAINT,
@Desc("Places the object in pure air at an absolute Y driven entirely by translate.y (plus optional translate.yRandom). Terrain height, underwater, and carving anchor checks are skipped. Use this for floating islands, sky structures, clouds, or blimps where the object must not be translated to the ground.")
FLOATING,
@Desc("Raw stamp at the caller-supplied (x, y, z). No terrain sampling, no stilting, no Y recomputation, no underwater or carving anchor guards. Used internally to route native Minecraft structure pieces (villages etc.) through the Iris object placer.")
STRUCTURE_PIECE
}
@@ -0,0 +1,98 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine.object;
import art.arcane.iris.util.common.reflect.KeyedType;
import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
import org.bukkit.potion.PotionEffectType;
import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
public final class PotionEffectTypes {
private static final Map<String, String> LEGACY_ALIASES;
private static final Set<String> MISSING_WARNED = ConcurrentHashMap.newKeySet();
static {
Map<String, String> m = new HashMap<>();
m.put("SLOW", "SLOWNESS");
m.put("FAST_DIGGING", "HASTE");
m.put("SLOW_DIGGING", "MINING_FATIGUE");
m.put("INCREASE_DAMAGE", "STRENGTH");
m.put("HEAL", "INSTANT_HEALTH");
m.put("HARM", "INSTANT_DAMAGE");
m.put("JUMP", "JUMP_BOOST");
m.put("CONFUSION", "NAUSEA");
m.put("DAMAGE_RESISTANCE", "RESISTANCE");
LEGACY_ALIASES = Collections.unmodifiableMap(m);
}
private PotionEffectTypes() {
}
public static String normalize(String input) {
if (input == null) {
return "";
}
String upper = input.trim().toUpperCase(Locale.ROOT).replace(' ', '_');
if (upper.contains(":")) {
upper = upper.substring(upper.indexOf(':') + 1);
}
return LEGACY_ALIASES.getOrDefault(upper, upper);
}
public static PotionEffectType resolve(String rawName) {
if (rawName == null || rawName.trim().isEmpty()) {
return null;
}
String wanted = normalize(rawName);
for (PotionEffectType i : Registry.EFFECT) {
NamespacedKey key = KeyedType.getKey(i);
if (key == null) {
continue;
}
String candidate = key.getKey().toUpperCase(Locale.ROOT).replace(' ', '_');
if (candidate.equals(wanted)) {
return i;
}
}
return null;
}
public static String knownTypesList() {
TreeSet<String> names = new TreeSet<>();
for (PotionEffectType i : Registry.EFFECT) {
NamespacedKey key = KeyedType.getKey(i);
if (key != null) {
names.add(key.getKey().toUpperCase(Locale.ROOT).replace(' ', '_'));
}
}
return String.join(", ", names);
}
public static boolean shouldWarn(String rawName) {
return MISSING_WARNED.add(normalize(rawName));
}
}
@@ -0,0 +1,33 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.object.annotations.Desc;
@Desc("How the top profile of a floating-child island is shaped.")
public enum TopShapeMode {
@Desc("Evaluate the target biome's own terrain generators to build the island top. Mountains biome produces real peaks, desert produces dunes, plains is flat. Recommended default.")
BIOME,
@Desc("Drive the top profile from topShapeStyle noise, independent of the target biome's generators. Amplitude controlled by topShapeAmp.")
NOISE,
@Desc("Flat slab on top, topHeight blocks above the base. Ignores noise and biome generators.")
FLAT
}
@@ -0,0 +1,12 @@
package art.arcane.iris.engine.object.annotations;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Retention(RUNTIME)
@Target({PARAMETER, TYPE, FIELD})
public @interface RegistryListPotionEffect {
}
@@ -1,23 +0,0 @@
package art.arcane.iris.engine.object.annotations.functions;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.engine.framework.ListFunction;
import art.arcane.volmlib.util.collection.KList;
public class StructureKeyFunction implements ListFunction<KList<String>> {
@Override
public String key() {
return "structure-key";
}
@Override
public String fancyName() {
return "Structure Key";
}
@Override
public KList<String> apply(IrisData irisData) {
return INMS.get().getStructureKeys().removeWhere(t -> t.startsWith("#"));
}
}
@@ -1,23 +0,0 @@
package art.arcane.iris.engine.object.annotations.functions;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.engine.framework.ListFunction;
import art.arcane.volmlib.util.collection.KList;
public class StructureKeyOrTagFunction implements ListFunction<KList<String>> {
@Override
public String key() {
return "structure-key-or-tag";
}
@Override
public String fancyName() {
return "Structure Key or Tag";
}
@Override
public KList<String> apply(IrisData irisData) {
return INMS.get().getStructureKeys();
}
}
@@ -376,8 +376,6 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
effectiveListener.onOverlay(x, z, overlayMetrics.appliedBlocks(), overlayMetrics.objectKeys(), System.currentTimeMillis());
}
setChunkReplacementPhase(phaseRef, effectiveListener, "structures", x, z);
INMS.get().placeStructures(c);
setChunkReplacementPhase(phaseRef, effectiveListener, "chunk-load-callback", x, z);
engine.getWorldManager().onChunkLoad(c, true);
world.refreshChunk(c.getX(), c.getZ());
@@ -458,10 +456,6 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
effectiveListener.onOverlay(x, z, overlayMetrics.appliedBlocks(), overlayMetrics.objectKeys(), System.currentTimeMillis());
}, syncExecutor).get();
}
CompletableFuture.runAsync(() -> {
setChunkReplacementPhase(phaseRef, effectiveListener, "structures", x, z);
INMS.get().placeStructures(c);
}, syncExecutor).get();
CompletableFuture.runAsync(() -> {
setChunkReplacementPhase(phaseRef, effectiveListener, "chunk-load-callback", x, z);
engine.getWorldManager().onChunkLoad(c, true);
@@ -811,10 +805,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
@Override
public boolean shouldGenerateStructures() {
if (isStudio() && art.arcane.iris.core.runtime.ObjectStudioActivation.isActive(getEngine().getDimension().getLoadKey())) {
return false;
}
return IrisSettings.get().getGeneral().isAutoGenerateIntrinsicStructures();
return false;
}
@Override
@@ -0,0 +1,118 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.util.common.director;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public final class DirectorHelp {
private DirectorHelp() {
}
public static void print(VolmitSender sender, Class<?> commandRoot) {
Director rootAnnotation = commandRoot.getAnnotation(Director.class);
String rootName = rootAnnotation == null || rootAnnotation.name().isEmpty()
? lowercaseDefault(commandRoot.getSimpleName())
: rootAnnotation.name();
String rootDesc = rootAnnotation == null ? "" : rootAnnotation.description();
sender.sendMessage(C.IRIS + "/" + rootName + C.GRAY + "" + rootDesc);
List<Method> methods = new ArrayList<>();
for (Method m : commandRoot.getDeclaredMethods()) {
if (m.isAnnotationPresent(Director.class)) {
methods.add(m);
}
}
methods.sort(Comparator.comparing(m -> methodName(m)));
for (Method m : methods) {
Director d = m.getAnnotation(Director.class);
String name = methodName(m);
String aliases = formatAliases(d.aliases());
sender.sendMessage(C.WHITE + " " + name + aliases + C.GRAY + "" + d.description());
for (Parameter p : m.getParameters()) {
Param pa = p.getAnnotation(Param.class);
if (pa == null) continue;
String key = pa.name().isEmpty() ? p.getName() : pa.name();
String type = simpleTypeName(p.getType());
String def = pa.defaultValue().isEmpty() ? "" : C.GRAY + " (default: " + pa.defaultValue() + ")";
String pAliases = formatAliases(pa.aliases());
sender.sendMessage(C.GRAY + " " + C.AQUA + key + "=" + C.YELLOW + "<" + type + ">"
+ C.GRAY + pAliases + C.GRAY + "" + pa.description() + def);
}
}
List<Field> subGroups = new ArrayList<>();
for (Field f : commandRoot.getDeclaredFields()) {
if (f.getType().isAnnotationPresent(Director.class)) {
subGroups.add(f);
}
}
if (!subGroups.isEmpty()) {
sender.sendMessage(C.IRIS + " Subcommand groups:");
for (Field f : subGroups) {
Director sub = f.getType().getAnnotation(Director.class);
String subName = sub.name().isEmpty() ? lowercaseDefault(f.getType().getSimpleName()) : sub.name();
sender.sendMessage(C.WHITE + " /" + rootName + " " + subName
+ C.GRAY + "" + sub.description() + C.GRAY + " (try: /" + rootName + " " + subName + " help)");
}
}
}
private static String methodName(Method m) {
Director d = m.getAnnotation(Director.class);
if (d != null && !d.name().isEmpty()) return d.name();
return m.getName();
}
private static String formatAliases(String[] aliases) {
if (aliases == null || aliases.length == 0) return "";
List<String> valid = new ArrayList<>();
for (String a : aliases) {
if (a != null && !a.isEmpty()) valid.add(a);
}
if (valid.isEmpty()) return "";
return C.GRAY + " [" + String.join(", ", valid) + "]";
}
private static String simpleTypeName(Class<?> type) {
if (type.isEnum()) {
Object[] constants = type.getEnumConstants();
List<String> names = new ArrayList<>();
for (Object c : constants) names.add(((Enum<?>) c).name());
return String.join("|", names);
}
return type.getSimpleName();
}
private static String lowercaseDefault(String simpleName) {
String s = simpleName.startsWith("Command") ? simpleName.substring("Command".length()) : simpleName;
return s.isEmpty() ? simpleName.toLowerCase() : s.toLowerCase();
}
}
@@ -1,112 +0,0 @@
package art.arcane.iris.util.common.director.specialhandlers;
import art.arcane.iris.core.ExternalDataPackPipeline;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.util.common.director.DirectorParameterHandler;
import art.arcane.volmlib.util.director.exceptions.DirectorParsingException;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
public class ExternalDatapackLocateHandler implements DirectorParameterHandler<String> {
@Override
public KList<String> getPossibilities() {
LinkedHashSet<String> tokens = new LinkedHashSet<>();
Map<String, Set<String>> locateById = ExternalDataPackPipeline.snapshotLocateStructuresById();
for (Map.Entry<String, Set<String>> entry : locateById.entrySet()) {
if (entry == null) {
continue;
}
String id = entry.getKey();
if (id != null && !id.isBlank()) {
tokens.add(id);
}
Set<String> structures = entry.getValue();
if (structures == null || structures.isEmpty()) {
continue;
}
for (String structure : structures) {
if (structure != null && !structure.isBlank()) {
tokens.add(structure);
}
}
}
KList<String> possibilities = new KList<>();
possibilities.add(tokens);
return possibilities;
}
@Override
public KList<String> getPossibilities(String input) {
String rawInput = input == null ? "" : input;
String[] split = rawInput.split(",", -1);
String partial = split.length == 0 ? "" : split[split.length - 1].trim().toLowerCase(Locale.ROOT);
StringBuilder prefixBuilder = new StringBuilder();
if (split.length > 1) {
for (int index = 0; index < split.length - 1; index++) {
String value = split[index] == null ? "" : split[index].trim();
if (value.isBlank()) {
continue;
}
if (!prefixBuilder.isEmpty()) {
prefixBuilder.append(',');
}
prefixBuilder.append(value);
}
}
String prefix = prefixBuilder.toString();
LinkedHashSet<String> completions = new LinkedHashSet<>();
for (String possibility : getPossibilities()) {
if (possibility == null || possibility.isBlank()) {
continue;
}
String normalized = possibility.toLowerCase(Locale.ROOT);
if (!partial.isBlank() && !normalized.startsWith(partial)) {
continue;
}
if (prefix.isBlank()) {
completions.add(possibility);
} else {
completions.add(prefix + "," + possibility);
}
}
KList<String> results = new KList<>();
results.add(completions);
return results;
}
@Override
public String toString(String value) {
return value == null ? "" : value;
}
@Override
public String parse(String in, boolean force) throws DirectorParsingException {
if (in == null || in.trim().isBlank()) {
throw new DirectorParsingException("You must provide at least one external datapack id or structure id.");
}
return in.trim();
}
@Override
public boolean supports(Class<?> type) {
return type.equals(String.class);
}
@Override
public String getRandomDefault() {
KList<String> possibilities = getPossibilities();
String random = possibilities.getRandom();
return random == null ? "external-datapack-id" : random;
}
}
@@ -0,0 +1,92 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.util.common.director.specialhandlers;
import art.arcane.iris.Iris;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.util.common.director.DirectorParameterHandler;
import art.arcane.volmlib.util.director.exceptions.DirectorParsingException;
import java.io.File;
import java.util.HashSet;
import java.util.Set;
public class ObjectTargetHandler implements DirectorParameterHandler<String> {
@Override
public KList<String> getPossibilities() {
KList<String> out = new KList<>();
Set<String> prefixes = new HashSet<>();
IrisData data = data();
if (data != null) {
for (String k : data.getObjectLoader().getPossibleKeys()) {
out.add(k);
collectPrefixes(k, prefixes);
}
} else {
File packsFolder = Iris.instance.getDataFolder("packs");
File[] packs = packsFolder.listFiles();
if (packs != null) {
for (File pack : packs) {
if (!pack.isDirectory()) continue;
IrisData d = IrisData.get(pack);
for (String k : d.getObjectLoader().getPossibleKeys()) {
out.add(k);
collectPrefixes(k, prefixes);
}
}
}
}
for (String p : prefixes) {
out.add(p);
}
return out;
}
private static void collectPrefixes(String key, Set<String> prefixes) {
int idx = 0;
while ((idx = key.indexOf('/', idx)) >= 0) {
prefixes.add(key.substring(0, idx + 1));
idx++;
}
}
@Override
public String toString(String irisObject) {
return irisObject;
}
@Override
public String parse(String in, boolean force) throws DirectorParsingException {
return in;
}
@Override
public boolean supports(Class<?> type) {
return type.equals(String.class);
}
@Override
public String getRandomDefault() {
String f = getPossibilities().getRandom();
return f == null ? "trees/" : f;
}
}
@@ -1,93 +0,0 @@
package art.arcane.iris.core;
import art.arcane.volmlib.util.nbt.io.NBTDeserializer;
import art.arcane.volmlib.util.nbt.io.NBTSerializer;
import art.arcane.volmlib.util.nbt.io.NamedTag;
import art.arcane.volmlib.util.nbt.tag.CompoundTag;
import art.arcane.volmlib.util.nbt.tag.IntTag;
import art.arcane.volmlib.util.nbt.tag.ListTag;
import art.arcane.volmlib.util.nbt.tag.Tag;
import org.junit.Test;
import java.io.ByteArrayInputStream;
import java.util.HashMap;
import java.util.Map;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
public class ExternalDataPackPipelineNbtRewriteTest {
@Test
public void rewritesOnlyJigsawPoolReferencesForCompressedAndUncompressedNbt() throws Exception {
for (boolean compressed : new boolean[]{false, true}) {
byte[] source = encodeStructureNbt(compressed, true);
Map<String, String> remapped = new HashMap<>();
remapped.put("minecraft:witch_hut/foundation", "iris_external_1:witch_hut/foundation");
byte[] rewritten = invokeRewrite(source, remapped);
CompoundTag root = decodeRoot(rewritten, compressed);
ListTag<?> blocks = root.getListTag("blocks");
CompoundTag jigsawBlock = (CompoundTag) blocks.get(0);
CompoundTag nonJigsawBlock = (CompoundTag) blocks.get(1);
assertEquals("iris_external_1:witch_hut/foundation", jigsawBlock.getCompoundTag("nbt").getString("pool"));
assertEquals("minecraft:witch_hut/foundation", nonJigsawBlock.getCompoundTag("nbt").getString("pool"));
}
}
@Test
public void nonJigsawPayloadIsLeftUnchanged() throws Exception {
byte[] source = encodeStructureNbt(false, false);
Map<String, String> remapped = new HashMap<>();
remapped.put("minecraft:witch_hut/foundation", "iris_external_1:witch_hut/foundation");
byte[] rewritten = invokeRewrite(source, remapped);
assertArrayEquals(source, rewritten);
}
private byte[] invokeRewrite(byte[] input, Map<String, String> remappedKeys) {
return StructureNbtJigsawPoolRewriter.rewrite(input, remappedKeys);
}
private byte[] encodeStructureNbt(boolean compressed, boolean includeJigsaw) throws Exception {
CompoundTag root = new CompoundTag();
ListTag<CompoundTag> palette = new ListTag<>(CompoundTag.class);
CompoundTag firstPalette = new CompoundTag();
firstPalette.putString("Name", includeJigsaw ? "minecraft:jigsaw" : "minecraft:stone");
palette.add(firstPalette);
CompoundTag secondPalette = new CompoundTag();
secondPalette.putString("Name", "minecraft:stone");
palette.add(secondPalette);
root.put("palette", palette);
ListTag<CompoundTag> blocks = new ListTag<>(CompoundTag.class);
blocks.add(blockTag(0, "minecraft:witch_hut/foundation"));
blocks.add(blockTag(1, "minecraft:witch_hut/foundation"));
root.put("blocks", blocks);
NamedTag named = new NamedTag("test", root);
return new NBTSerializer(compressed).toBytes(named);
}
private CompoundTag blockTag(int state, String pool) {
CompoundTag block = new CompoundTag();
block.putInt("state", state);
CompoundTag nbt = new CompoundTag();
nbt.putString("pool", pool);
block.put("nbt", nbt);
ListTag<IntTag> pos = new ListTag<>(IntTag.class);
pos.add(new IntTag(0));
pos.add(new IntTag(0));
pos.add(new IntTag(0));
block.put("pos", pos);
return block;
}
private CompoundTag decodeRoot(byte[] bytes, boolean compressed) throws Exception {
NamedTag namedTag = new NBTDeserializer(compressed).fromStream(new ByteArrayInputStream(bytes));
Tag<?> rootTag = namedTag.getTag();
return (CompoundTag) rootTag;
}
}
@@ -1,44 +0,0 @@
package art.arcane.iris.core;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import org.junit.Test;
import java.io.File;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class ServerConfiguratorDatapackFolderTest {
@Test
public void resolvesDimensionWorldFolderBackToRootDatapacks() {
File folder = new File("/tmp/server/world/dimensions/minecraft/overworld");
File datapacks = ServerConfigurator.resolveDatapacksFolder(folder);
assertEquals(new File("/tmp/server/world/datapacks").getAbsolutePath(), datapacks.getAbsolutePath());
}
@Test
public void keepsStandaloneWorldFolderDatapacksUnchanged() {
File folder = new File("/tmp/server/custom_world");
File datapacks = ServerConfigurator.resolveDatapacksFolder(folder);
assertEquals(new File("/tmp/server/custom_world/datapacks").getAbsolutePath(), datapacks.getAbsolutePath());
}
@Test
public void installFoldersIncludeExtraStudioWorldDatapackTargets() {
File baseFolder = new File("/tmp/server/world/datapacks");
File extraFolder = new File("/tmp/server/iris-studio/datapacks");
KList<File> baseFolders = new KList<>();
baseFolders.add(baseFolder);
KList<File> extraFolders = new KList<>();
extraFolders.add(extraFolder);
KMap<String, KList<File>> extrasByPack = new KMap<>();
extrasByPack.put("overworld", extraFolders);
KList<File> folders = ServerConfigurator.collectInstallDatapackFolders(baseFolders, extrasByPack);
assertEquals(2, folders.size());
assertTrue(folders.contains(baseFolder));
assertTrue(folders.contains(extraFolder));
}
}
@@ -1,45 +0,0 @@
package art.arcane.iris.core.runtime;
import org.junit.Test;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class DatapackReadinessResultTest {
@Test
public void verificationUsesDimensionTypeKeyPath() throws Exception {
Path root = Files.createTempDirectory("iris-datapack-readiness");
Path datapackRoot = root.resolve("iris");
Files.createDirectories(datapackRoot.resolve("data/iris/dimension_type"));
Files.writeString(datapackRoot.resolve("pack.mcmeta"), "{}");
Files.writeString(datapackRoot.resolve("data/iris/dimension_type/runtime-key.json"), "{}");
ArrayList<String> verifiedPaths = new ArrayList<>();
ArrayList<String> missingPaths = new ArrayList<>();
DatapackReadinessResult.collectVerificationPaths(root.toFile(), "runtime-key", verifiedPaths, missingPaths);
assertTrue(missingPaths.isEmpty());
assertEquals(2, verifiedPaths.size());
}
@Test
public void verificationMarksMissingDimensionTypePath() throws Exception {
Path root = Files.createTempDirectory("iris-datapack-readiness-missing");
Path datapackRoot = root.resolve("iris");
Files.createDirectories(datapackRoot);
Files.writeString(datapackRoot.resolve("pack.mcmeta"), "{}");
ArrayList<String> verifiedPaths = new ArrayList<>();
ArrayList<String> missingPaths = new ArrayList<>();
DatapackReadinessResult.collectVerificationPaths(root.toFile(), "runtime-key", verifiedPaths, missingPaths);
assertEquals(1, verifiedPaths.size());
assertEquals(1, missingPaths.size());
assertTrue(missingPaths.get(0).endsWith(File.separator + "iris" + File.separator + "data" + File.separator + "iris" + File.separator + "dimension_type" + File.separator + "runtime-key.json"));
}
}
@@ -2,19 +2,11 @@ package art.arcane.iris.core.nms.v1_21_R7;
import com.mojang.datafixers.util.Pair;
import com.mojang.serialization.MapCodec;
import art.arcane.iris.Iris;
import art.arcane.iris.core.ExternalDataPackPipeline;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.util.common.reflect.WrappedField;
import art.arcane.iris.util.common.reflect.WrappedReturningMethod;
import net.minecraft.CrashReport;
import net.minecraft.CrashReportCategory;
import net.minecraft.ReportedException;
import net.minecraft.core.*;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.Identifier;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.WorldGenRegion;
import net.minecraft.util.random.WeightedList;
@@ -29,12 +21,9 @@ import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.levelgen.*;
import net.minecraft.world.level.levelgen.blending.Blender;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.StructureSet;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.craftbukkit.CraftWorld;
import org.bukkit.craftbukkit.generator.CustomChunkGenerator;
@@ -45,18 +34,12 @@ import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
public class IrisChunkGenerator extends CustomChunkGenerator {
private static final WrappedField<ChunkGenerator, BiomeSource> BIOME_SOURCE;
private static final WrappedReturningMethod<Heightmap, Object> SET_HEIGHT;
private static final int EXTERNAL_FOUNDATION_MAX_DEPTH = 96;
private static final Set<String> loggedExternalStructureFingerprintKeys = ConcurrentHashMap.newKeySet();
private final ChunkGenerator delegate;
private final Engine engine;
private volatile Registry<Structure> cachedStructureRegistry;
private volatile Map<Structure, Integer> cachedStructureOrder;
public IrisChunkGenerator(ChunkGenerator delegate, long seed, Engine engine, World world) {
super(((CraftWorld) world).getHandle(), edit(delegate, new CustomBiomeSource(seed, engine, world)), null);
@@ -66,10 +49,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
@Override
public @Nullable Pair<BlockPos, Holder<Structure>> findNearestMapStructure(ServerLevel level, HolderSet<Structure> holders, BlockPos pos, int radius, boolean findUnexplored) {
if (holders.size() == 0) return null;
if (engine.getDimension().isDisableExplorerMaps())
return null;
return delegate.findNearestMapStructure(level, holders, pos, radius, findUnexplored);
return null;
}
@Override
@@ -96,12 +76,6 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
@Override
public void createStructures(RegistryAccess registryAccess, ChunkGeneratorStructureState structureState, StructureManager structureManager, ChunkAccess access, StructureTemplateManager templateManager, ResourceKey<Level> levelKey) {
if (!structureManager.shouldGenerateStructures())
return;
if (!IrisSettings.get().getGeneral().isAutoGenerateIntrinsicStructures()) {
return;
}
delegate.createStructures(registryAccess, structureState, structureManager, access, templateManager, levelKey);
}
@Override
@@ -157,18 +131,8 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
@Override
public void addVanillaDecorations(WorldGenLevel level, ChunkAccess chunkAccess, StructureManager structureManager) {
if (!structureManager.shouldGenerateStructures())
return;
if (!IrisSettings.get().getGeneral().isAutoGenerateIntrinsicStructures()) {
return;
}
SectionPos sectionPos = SectionPos.of(chunkAccess.getPos(), level.getMinSectionY());
BlockPos blockPos = sectionPos.origin();
WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed()));
long i = random.setDecorationSeed(level.getSeed(), blockPos.getX(), blockPos.getZ());
Registry<Structure> structureRegistry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
Map<Structure, Integer> structureOrder = getStructureOrder(structureRegistry);
Heightmap surface = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
Heightmap ocean = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
@@ -180,403 +144,18 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
int wX = x + blockPos.getX();
int wZ = z + blockPos.getZ();
int noAir = engine.getHeight(wX, wZ, false) + engine.getMinHeight() + 1;
int noFluid = engine.getHeight(wX, wZ, true) + engine.getMinHeight() + 1;
int oceanHeight = ocean.getFirstAvailable(x, z);
int surfaceHeight = surface.getFirstAvailable(x, z);
int motionHeight = motion.getFirstAvailable(x, z);
int motionNoLeavesHeight = motionNoLeaves.getFirstAvailable(x, z);
if (noFluid > oceanHeight) {
SET_HEIGHT.invoke(ocean, x, z, noFluid);
}
if (noAir > surfaceHeight) {
SET_HEIGHT.invoke(surface, x, z, noAir);
}
if (noAir > motionHeight) {
SET_HEIGHT.invoke(motion, x, z, noAir);
}
if (noAir > motionNoLeavesHeight) {
SET_HEIGHT.invoke(motionNoLeaves, x, z, noAir);
}
}
}
List<StructureStart> starts = new ArrayList<>(structureManager.startsForStructure(chunkAccess.getPos(), structure -> true));
starts.sort(Comparator.comparingInt(start -> structureOrder.getOrDefault(start.getStructure(), Integer.MAX_VALUE)));
Set<String> externalSmartBoreStructures = ExternalDataPackPipeline.snapshotSmartBoreStructureKeys();
IrisSettings.IrisSettingsGeneral general = IrisSettings.get().getGeneral();
boolean intrinsicFoundationsEnabled = general.isIntrinsicStructureFoundations();
int intrinsicFoundationDepth = Math.max(0, general.getIntrinsicFoundationMaxDepth());
List<String> intrinsicAllowlist = general.getIntrinsicStructureAllowlist();
int seededStructureIndex = Integer.MIN_VALUE;
for (int j = 0; j < starts.size(); j++) {
StructureStart start = starts.get(j);
Structure structure = start.getStructure();
int structureIndex = structureOrder.getOrDefault(structure, j);
if (structureIndex != seededStructureIndex) {
random.setFeatureSeed(i, structureIndex, structure.step().ordinal());
seededStructureIndex = structureIndex;
}
Supplier<String> supplier = () -> structureRegistry.getResourceKey(structure).map(Object::toString).orElseGet(structure::toString);
String structureKey = resolveStructureKey(structureRegistry, structure);
boolean isExternalSmartBoreStructure = externalSmartBoreStructures.contains(structureKey);
boolean isIntrinsicFoundationStructure = !isExternalSmartBoreStructure
&& intrinsicFoundationsEnabled
&& intrinsicFoundationDepth > 0
&& matchesIntrinsicAllowlist(structureKey, intrinsicAllowlist);
int foundationDepth = isExternalSmartBoreStructure
? EXTERNAL_FOUNDATION_MAX_DEPTH
: (isIntrinsicFoundationStructure ? intrinsicFoundationDepth : 0);
BitSet[] beforeSolidColumns = null;
if (foundationDepth > 0) {
beforeSolidColumns = snapshotChunkSolidColumns(level, chunkAccess);
}
try {
level.setCurrentlyGenerating(supplier);
start.placeInChunk(level, structureManager, this, random, getWritableArea(chunkAccess), chunkAccess.getPos());
if (beforeSolidColumns != null) {
applyStructureFoundations(level, chunkAccess, beforeSolidColumns, foundationDepth);
}
if (shouldLogExternalStructureFingerprint(structureKey)) {
logExternalStructureFingerprint(structureKey, start);
}
} catch (Exception exception) {
CrashReport crashReport = CrashReport.forThrowable(exception, "Feature placement");
CrashReportCategory category = crashReport.addCategory("Feature");
category.setDetail("Description", supplier::get);
throw new ReportedException(crashReport);
int terrainTop = engine.getHeight(wX, wZ, false) + engine.getMinHeight() + 1;
int terrainNoFluid = engine.getHeight(wX, wZ, true) + engine.getMinHeight() + 1;
SET_HEIGHT.invoke(ocean, x, z, terrainNoFluid);
SET_HEIGHT.invoke(surface, x, z, terrainTop);
SET_HEIGHT.invoke(motion, x, z, terrainTop);
SET_HEIGHT.invoke(motionNoLeaves, x, z, terrainTop);
}
}
Heightmap.primeHeightmaps(chunkAccess, ChunkStatus.FINAL_HEIGHTMAPS);
}
private static String resolveStructureKey(Registry<Structure> structureRegistry, Structure structure) {
Identifier directKey = structureRegistry.getKey(structure);
if (directKey != null) {
return directKey.toString().toLowerCase(Locale.ROOT);
}
String fallback = String.valueOf(structure);
int slash = fallback.lastIndexOf('/');
int end = fallback.lastIndexOf(']');
if (slash >= 0 && end > slash) {
return fallback.substring(slash + 1, end).toLowerCase(Locale.ROOT);
}
return fallback.toLowerCase(Locale.ROOT);
}
private static BoundingBox getWritableArea(ChunkAccess ichunkaccess) {
ChunkPos chunkPos = ichunkaccess.getPos();
int minX = chunkPos.getMinBlockX();
int minZ = chunkPos.getMinBlockZ();
LevelHeightAccessor heightAccessor = ichunkaccess.getHeightAccessorForGeneration();
int minY = heightAccessor.getMinY() + 1;
int maxY = heightAccessor.getMaxY();
return new BoundingBox(minX, minY, minZ, minX + 15, maxY, minZ + 15);
}
private static BitSet[] snapshotChunkSolidColumns(WorldGenLevel level, ChunkAccess chunkAccess) {
int minY = level.getMinY();
int maxY = level.getMaxY();
int ySpan = maxY - minY;
if (ySpan <= 0) {
return new BitSet[0];
}
ChunkPos chunkPos = chunkAccess.getPos();
int minX = chunkPos.getMinBlockX();
int minZ = chunkPos.getMinBlockZ();
BitSet[] columns = new BitSet[16 * 16];
BlockPos.MutableBlockPos mutablePos = new BlockPos.MutableBlockPos();
for (int localX = 0; localX < 16; localX++) {
for (int localZ = 0; localZ < 16; localZ++) {
int index = (localX << 4) | localZ;
BitSet solids = new BitSet(ySpan);
int worldX = minX + localX;
int worldZ = minZ + localZ;
for (int y = minY; y < maxY; y++) {
mutablePos.set(worldX, y, worldZ);
if (isFoundationSolid(level.getBlockState(mutablePos))) {
solids.set(y - minY);
}
}
columns[index] = solids;
}
}
return columns;
}
private static boolean matchesIntrinsicAllowlist(String structureKey, List<String> allowlist) {
if (structureKey == null || structureKey.isBlank() || allowlist == null || allowlist.isEmpty()) {
return false;
}
String key = structureKey.toLowerCase(Locale.ROOT);
for (String raw : allowlist) {
if (raw == null) {
continue;
}
String pattern = raw.trim().toLowerCase(Locale.ROOT);
if (pattern.isEmpty()) {
continue;
}
if (pattern.endsWith("*")) {
if (key.startsWith(pattern.substring(0, pattern.length() - 1))) {
return true;
}
} else if (key.equals(pattern)) {
return true;
}
}
return false;
}
private static void applyStructureFoundations(
WorldGenLevel level,
ChunkAccess chunkAccess,
BitSet[] beforeSolidColumns,
int maxDepth
) {
if (beforeSolidColumns == null || beforeSolidColumns.length == 0 || maxDepth <= 0) {
return;
}
int minY = level.getMinY();
int maxY = level.getMaxY();
int ySpan = maxY - minY;
if (ySpan <= 0) {
return;
}
ChunkPos chunkPos = chunkAccess.getPos();
int minX = chunkPos.getMinBlockX();
int minZ = chunkPos.getMinBlockZ();
BlockPos.MutableBlockPos mutablePos = new BlockPos.MutableBlockPos();
for (int localX = 0; localX < 16; localX++) {
for (int localZ = 0; localZ < 16; localZ++) {
int index = (localX << 4) | localZ;
BitSet before = beforeSolidColumns[index];
if (before == null) {
continue;
}
int worldX = minX + localX;
int worldZ = minZ + localZ;
int lowestNewSolidY = Integer.MIN_VALUE;
for (int y = minY; y < maxY; y++) {
mutablePos.set(worldX, y, worldZ);
BlockState state = level.getBlockState(mutablePos);
if (!isFoundationSolid(state)) {
continue;
}
if (before.get(y - minY)) {
continue;
}
lowestNewSolidY = y;
break;
}
if (lowestNewSolidY == Integer.MIN_VALUE) {
continue;
}
mutablePos.set(worldX, lowestNewSolidY, worldZ);
BlockState foundationState = level.getBlockState(mutablePos);
if (!isFoundationSolid(foundationState)) {
continue;
}
int depth = 0;
for (int y = lowestNewSolidY - 1; y >= minY && depth < maxDepth; y--) {
mutablePos.set(worldX, y, worldZ);
BlockState state = level.getBlockState(mutablePos);
if (isFoundationSolid(state)) {
break;
}
level.setBlock(mutablePos, foundationState, 2);
depth++;
}
}
}
}
private static boolean isFoundationSolid(BlockState state) {
if (state == null || state.isAir()) {
return false;
}
if (!state.getFluidState().isEmpty()) {
return false;
}
return Heightmap.Types.MOTION_BLOCKING_NO_LEAVES.isOpaque().test(state);
}
private static boolean shouldLogExternalStructureFingerprint(String structureKey) {
if (!IrisSettings.get().getGeneral().isDebug()) {
return false;
}
if (structureKey == null || structureKey.isBlank()) {
return false;
}
String normalized = structureKey.toLowerCase(Locale.ROOT);
if (!"minecraft:ancient_city".equals(normalized)
&& !"minecraft:mineshaft".equals(normalized)
&& !"minecraft:mineshaft_mesa".equals(normalized)) {
return false;
}
return loggedExternalStructureFingerprintKeys.add(normalized);
}
private static void logExternalStructureFingerprint(String structureKey, StructureStart start) {
if (start == null) {
return;
}
List<?> pieces = extractPieces(start);
int pieceCount = pieces.size();
String firstPieceType = "none";
String firstPieceFingerprint = "none";
if (!pieces.isEmpty()) {
Object firstPiece = pieces.get(0);
if (firstPiece != null) {
firstPieceType = firstPiece.getClass().getName();
firstPieceFingerprint = resolvePieceFingerprint(firstPiece);
}
}
Iris.debug("External structure fingerprint: key=" + structureKey
+ ", pieces=" + pieceCount
+ ", firstPiece=" + firstPieceType
+ ", fingerprint=" + firstPieceFingerprint);
}
private static List<?> extractPieces(StructureStart start) {
try {
Method getPiecesMethod = start.getClass().getMethod("getPieces");
Object result = getPiecesMethod.invoke(start);
if (result instanceof List<?> list) {
return list;
}
if (result != null) {
Method piecesMethod = result.getClass().getMethod("pieces");
Object piecesResult = piecesMethod.invoke(result);
if (piecesResult instanceof List<?> list) {
return list;
}
}
} catch (Throwable ignored) {
}
try {
Method piecesMethod = start.getClass().getMethod("pieces");
Object result = piecesMethod.invoke(start);
if (result instanceof List<?> list) {
return list;
}
} catch (Throwable ignored) {
}
return List.of();
}
private static String resolvePieceFingerprint(Object piece) {
if (piece == null) {
return "unknown";
}
try {
Method templateNameMethod = piece.getClass().getMethod("templateName");
Object value = templateNameMethod.invoke(piece);
if (value != null) {
String normalized = String.valueOf(value);
if (!normalized.isBlank()) {
return normalized;
}
}
} catch (Throwable ignored) {
}
try {
Method templateMethod = piece.getClass().getMethod("template");
Object value = templateMethod.invoke(piece);
if (value != null) {
return value.getClass().getName();
}
} catch (Throwable ignored) {
}
Class<?> current = piece.getClass();
while (current != null && current != Object.class) {
Field[] fields = current.getDeclaredFields();
for (Field field : fields) {
try {
field.setAccessible(true);
Object value = field.get(piece);
if (value == null) {
continue;
}
if (value instanceof Identifier identifier) {
String normalized = identifier.toString();
if (!normalized.isBlank()) {
return normalized;
}
}
if (value instanceof String text) {
String fieldName = field.getName() == null ? "" : field.getName().toLowerCase(Locale.ROOT);
if (fieldName.contains("template") || fieldName.contains("name") || fieldName.contains("id")) {
if (!text.isBlank()) {
return text;
}
}
}
} catch (Throwable ignored) {
}
}
current = current.getSuperclass();
}
return piece.getClass().getSimpleName();
}
private Map<Structure, Integer> getStructureOrder(Registry<Structure> structureRegistry) {
Map<Structure, Integer> localOrder = cachedStructureOrder;
Registry<Structure> localRegistry = cachedStructureRegistry;
if (localRegistry == structureRegistry && localOrder != null) {
return localOrder;
}
synchronized (this) {
Map<Structure, Integer> synchronizedOrder = cachedStructureOrder;
Registry<Structure> synchronizedRegistry = cachedStructureRegistry;
if (synchronizedRegistry == structureRegistry && synchronizedOrder != null) {
return synchronizedOrder;
}
List<Structure> sortedStructures = structureRegistry.stream()
.sorted(Comparator.comparingInt(structure -> structure.step().ordinal()))
.toList();
Map<Structure, Integer> builtOrder = new IdentityHashMap<>(sortedStructures.size());
for (int index = 0; index < sortedStructures.size(); index++) {
Structure structure = sortedStructures.get(index);
builtOrder.put(structure, index);
}
cachedStructureRegistry = structureRegistry;
cachedStructureOrder = builtOrder;
return builtOrder;
}
}
@Override
public void spawnOriginalMobs(WorldGenRegion regionlimitedworldaccess) {
delegate.spawnOriginalMobs(regionlimitedworldaccess);
@@ -5,7 +5,6 @@ import art.arcane.iris.Iris;
import art.arcane.iris.core.nms.INMSBinding;
import art.arcane.iris.core.nms.container.BiomeColor;
import art.arcane.iris.core.nms.container.Pair;
import art.arcane.iris.core.nms.container.StructurePlacement;
import art.arcane.iris.core.nms.container.BlockProperty;
import art.arcane.iris.core.nms.datapack.DataVersion;
import art.arcane.iris.engine.data.cache.AtomicCache;
@@ -731,65 +730,6 @@ public class NMSBinding implements INMSBinding {
return 0;
}
@Override
public KList<String> getStructureKeys() {
KList<String> keys = new KList<>();
var registry = registry().lookup(Registries.STRUCTURE).orElse(null);
if (registry == null) return keys;
registry.keySet().stream().map(Identifier::toString).forEach(keys::add);
registry.getTags()
.map(HolderSet.Named::key)
.map(TagKey::location)
.map(Identifier::toString)
.map(s -> "#" + s)
.forEach(keys::add);
return keys;
}
@Override
public KMap<String, KList<String>> getVanillaStructureBiomeTags() {
KMap<String, KList<String>> tags = new KMap<>();
Registry<net.minecraft.world.level.biome.Biome> registry = registry().lookup(Registries.BIOME).orElse(null);
if (registry == null) {
return tags;
}
registry.getTags().forEach(named -> {
TagKey<net.minecraft.world.level.biome.Biome> tagKey = named.key();
Identifier location = tagKey.location();
if (!"minecraft".equals(location.getNamespace())) {
return;
}
String path = location.getPath();
if (!path.startsWith("has_structure/")) {
return;
}
KList<String> values = new KList<>();
named.stream().forEach(holder -> {
net.minecraft.world.level.biome.Biome biome = holder.value();
Identifier biomeLocation = registry.getKey(biome);
if (biomeLocation == null) {
return;
}
if ("minecraft".equals(biomeLocation.getNamespace())) {
values.add(biomeLocation.toString());
}
});
KList<String> uniqueValues = values.removeDuplicates();
if (!uniqueValues.isEmpty()) {
tags.put(path, uniqueValues);
}
});
return tags;
}
@Override
public boolean missingDimensionTypes(String... keys) {
var type = registry().lookupOrThrow(Registries.DIMENSION_TYPE);
@@ -848,76 +788,8 @@ public class NMSBinding implements INMSBinding {
return new BlockProperty(property.getName(), property.getValueClass(), state.getValue(property), property.getPossibleValues(), property::getName);
}
@Override
public void placeStructures(Chunk chunk) {
var craft = ((CraftChunk) chunk);
var level = craft.getCraftWorld().getHandle();
var access = craft.getHandle(ChunkStatus.FEATURES);
if (access instanceof LevelChunk) {
return;
}
level.getChunkSource().getGenerator().applyBiomeDecoration(level, access, level.structureManager());
}
@Override
public KMap<art.arcane.iris.core.link.Identifier, StructurePlacement> collectStructures() {
var structureSets = registry().lookupOrThrow(Registries.STRUCTURE_SET);
var structurePlacements = registry().lookupOrThrow(Registries.STRUCTURE_PLACEMENT);
return structureSets.keySet()
.stream()
.map(structureSets::get)
.filter(Optional::isPresent)
.map(Optional::get)
.map(holder -> {
var set = holder.value();
var placement = set.placement();
var key = holder.key().identifier();
StructurePlacement.StructurePlacementBuilder<?, ?> builder;
if (placement instanceof RandomSpreadStructurePlacement random) {
builder = StructurePlacement.RandomSpread.builder()
.separation(random.separation())
.spacing(random.spacing())
.spreadType(switch (random.spreadType()) {
case LINEAR -> StructurePlacement.SpreadType.LINEAR;
case TRIANGULAR -> StructurePlacement.SpreadType.TRIANGULAR;
});
} else if (placement instanceof ConcentricRingsStructurePlacement rings) {
builder = StructurePlacement.ConcentricRings.builder()
.distance(rings.distance())
.spread(rings.spread())
.count(rings.count());
} else {
Iris.warn("Unsupported structure placement for set " + key + " with type " + structurePlacements.getKey(placement.type()));
return null;
}
return new Pair<>(new art.arcane.iris.core.link.Identifier(key.getNamespace(), key.getPath()), builder
.salt(placement.salt)
.frequency(placement.frequency)
.structures(set.structures()
.stream()
.map(entry -> new StructurePlacement.Structure(
entry.weight(),
entry.structure()
.unwrapKey()
.map(ResourceKey::identifier)
.map(Identifier::toString)
.orElse(null),
entry.structure().tags()
.map(TagKey::location)
.map(Identifier::toString)
.toList()
))
.filter(StructurePlacement.Structure::isValid)
.toList())
.build());
})
.filter(Objects::nonNull)
.collect(Collectors.toMap(Pair::getA, Pair::getB, (a, b) -> a, KMap::new));
}
private static final Pattern VANILLA_DATAPACK_ENTRY = Pattern.compile(
"^data/minecraft/(?:worldgen/(?:structure|structure_set|template_pool|processor_list)/.+\\.json"
"^data/minecraft/(?:worldgen/(?:structure|structure_set|template_pool|processor_list|configured_feature|placed_feature)/.+\\.json"
+ "|structures?/.+\\.nbt"
+ "|tags/worldgen/biome/has_structure/.+\\.json)$"
);