mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-27 04:37:47 +00:00
d
d
This commit is contained in:
+2
-13
@@ -1,5 +1,6 @@
|
|||||||
package art.arcane.iris.core.nms.v26_2_R1;
|
package art.arcane.iris.core.nms.v26_2_R1;
|
||||||
|
|
||||||
|
import art.arcane.iris.nativegen.NativeStructureGenerationKeys;
|
||||||
import net.minecraft.core.Holder;
|
import net.minecraft.core.Holder;
|
||||||
import net.minecraft.core.Registry;
|
import net.minecraft.core.Registry;
|
||||||
import net.minecraft.core.RegistryAccess;
|
import net.minecraft.core.RegistryAccess;
|
||||||
@@ -12,7 +13,6 @@ import net.minecraft.world.level.biome.BiomeSource;
|
|||||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||||
|
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
@@ -74,18 +74,7 @@ final class VanillaStructureBiomes {
|
|||||||
if (level == null) {
|
if (level == null) {
|
||||||
throw new IllegalStateException("Minecraft server level is unavailable");
|
throw new IllegalStateException("Minecraft server level is unavailable");
|
||||||
}
|
}
|
||||||
Set<String> reachable = new LinkedHashSet<>();
|
|
||||||
Set<String> possible = possibleBiomeKeys(source);
|
Set<String> possible = possibleBiomeKeys(source);
|
||||||
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
return NativeStructureGenerationKeys.reachable(level, possible);
|
||||||
for (Map.Entry<ResourceKey<Structure>, Structure> entry : registry.entrySet()) {
|
|
||||||
for (Holder<Biome> holder : entry.getValue().biomes()) {
|
|
||||||
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
|
|
||||||
if (key.isPresent() && possible.contains(key.get().identifier().toString())) {
|
|
||||||
reachable.add(entry.getKey().identifier().toString());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return reachable;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
package art.arcane.iris.nativegen;
|
||||||
|
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.core.Vec3i;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadType;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class NativeStructureGenerationKeysContractTest {
|
||||||
|
@BeforeClass
|
||||||
|
public static void bootstrapMinecraft() {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void reachabilityUsesEnabledActiveStructureSetsWithoutLazyPlacementGeneration() throws IOException {
|
||||||
|
String source = Files.readString(Path.of(
|
||||||
|
System.getProperty("iris.nativeStructureGenerationKeysSource")));
|
||||||
|
|
||||||
|
assertTrue(source.contains("getWorldGenSettings().options().generateStructures()"));
|
||||||
|
assertTrue(source.contains("getGeneratorState().possibleStructureSets()"));
|
||||||
|
assertTrue(source.contains("isEnabledPlacement(structureSet.placement())"));
|
||||||
|
assertTrue(source.contains("StructurePlacement.class.getDeclaredMethods()"));
|
||||||
|
assertTrue(source.contains("entry.weight() <= 0"));
|
||||||
|
assertTrue(source.contains("hasPossibleBiome(entry.structure().value(), possibleBiomes)"));
|
||||||
|
assertFalse(source.contains("getPlacementsForStructure("));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void frequencyGateUsesMappedAccessorAndRejectsNonPositivePlacements() {
|
||||||
|
assertFalse(NativeStructureGenerationKeys.isEnabledPlacement(placement(0.0F)));
|
||||||
|
assertFalse(NativeStructureGenerationKeys.isEnabledPlacement(placement(-0.1F)));
|
||||||
|
assertFalse(NativeStructureGenerationKeys.isEnabledPlacement(placement(Float.NaN)));
|
||||||
|
assertTrue(NativeStructureGenerationKeys.isEnabledPlacement(placement(0.25F)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RandomSpreadStructurePlacement placement(float frequency) {
|
||||||
|
return new RandomSpreadStructurePlacement(
|
||||||
|
Vec3i.ZERO,
|
||||||
|
StructurePlacement.FrequencyReductionMethod.DEFAULT,
|
||||||
|
frequency,
|
||||||
|
1,
|
||||||
|
Optional.empty(),
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
RandomSpreadType.LINEAR);
|
||||||
|
}
|
||||||
|
}
|
||||||
+467
-46
@@ -19,17 +19,23 @@
|
|||||||
package art.arcane.iris.core.commands;
|
package art.arcane.iris.core.commands;
|
||||||
|
|
||||||
import art.arcane.iris.Iris;
|
import art.arcane.iris.Iris;
|
||||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
import art.arcane.iris.core.localization.BukkitCommandMessages;
|
||||||
|
import art.arcane.iris.core.localization.BukkitCommandMessagesExtended;
|
||||||
|
import art.arcane.iris.core.localization.IrisLanguage;
|
||||||
|
import art.arcane.iris.core.datapack.DatapackIngestService;
|
||||||
import art.arcane.iris.core.service.ObjectStudioSaveService;
|
import art.arcane.iris.core.service.ObjectStudioSaveService;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||||
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
||||||
import art.arcane.iris.engine.platform.EngineBukkitOps;
|
|
||||||
import art.arcane.iris.engine.framework.StructureReachability;
|
import art.arcane.iris.engine.framework.StructureReachability;
|
||||||
import art.arcane.iris.engine.object.IrisBiome;
|
import art.arcane.iris.engine.object.IrisBiome;
|
||||||
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
||||||
import art.arcane.iris.engine.object.IrisRegion;
|
import art.arcane.iris.engine.object.IrisRegion;
|
||||||
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
|
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
|
||||||
|
import art.arcane.iris.engine.platform.EngineBukkitOps;
|
||||||
|
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||||
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
|
import art.arcane.iris.spi.PlatformStructureHooks;
|
||||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||||
import art.arcane.iris.util.common.director.specialhandlers.ObjectHandler;
|
import art.arcane.iris.util.common.director.specialhandlers.ObjectHandler;
|
||||||
import art.arcane.iris.util.common.director.specialhandlers.StructureHandler;
|
import art.arcane.iris.util.common.director.specialhandlers.StructureHandler;
|
||||||
@@ -40,6 +46,7 @@ import art.arcane.volmlib.util.collection.KList;
|
|||||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||||
import art.arcane.volmlib.util.director.annotations.Director;
|
import art.arcane.volmlib.util.director.annotations.Director;
|
||||||
import art.arcane.volmlib.util.director.annotations.Param;
|
import art.arcane.volmlib.util.director.annotations.Param;
|
||||||
|
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.NamespacedKey;
|
import org.bukkit.NamespacedKey;
|
||||||
@@ -49,10 +56,14 @@ import org.bukkit.entity.Player;
|
|||||||
import org.bukkit.generator.structure.Structure;
|
import org.bukkit.generator.structure.Structure;
|
||||||
import org.bukkit.util.StructureSearchResult;
|
import org.bukkit.util.StructureSearchResult;
|
||||||
|
|
||||||
import art.arcane.iris.core.localization.IrisLanguage;
|
import java.util.ArrayList;
|
||||||
import art.arcane.iris.core.localization.BukkitCommandMessages;
|
import java.util.LinkedHashMap;
|
||||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
import java.util.LinkedHashSet;
|
||||||
import art.arcane.iris.core.localization.BukkitCommandMessagesExtended;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
@Director(name = "find", origin = DirectorOrigin.PLAYER, description = "Iris Find commands", descriptionKey = "iris.director.commandfind.director.iris_find_commands", aliases = "goto")
|
@Director(name = "find", origin = DirectorOrigin.PLAYER, description = "Iris Find commands", descriptionKey = "iris.director.commandfind.director.iris_find_commands", aliases = "goto")
|
||||||
public class CommandFind implements DirectorExecutor {
|
public class CommandFind implements DirectorExecutor {
|
||||||
@Director(description = "Find a biome", descriptionKey = "iris.director.commandfind.director.find_biome")
|
@Director(description = "Find a biome", descriptionKey = "iris.director.commandfind.director.find_biome")
|
||||||
@@ -123,57 +134,66 @@ public class CommandFind implements DirectorExecutor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String structureKey = structure == null ? "" : structure.trim();
|
|
||||||
Structure nativeStructure = resolveNativeStructure(structureKey);
|
|
||||||
boolean irisReplacement = false;
|
|
||||||
if (nativeStructure != null) {
|
|
||||||
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(
|
|
||||||
e, structureKey, false);
|
|
||||||
if (!decision.generate()
|
|
||||||
&& decision.status() != NativeStructureGenerationStatus.REPLACED_BY_IRIS) {
|
|
||||||
commandSender.sendMessage(C.RED + NativeStructureGenerationPolicy.generationStatusMessage(
|
|
||||||
structureKey, decision.status()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
irisReplacement = decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS;
|
|
||||||
if (irisReplacement && !IrisStructureLocator.hasNativePlacement(e, structureKey)) {
|
|
||||||
locateIrisStructure(e, structureKey, commandSender);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nativeStructure == null && IrisStructureLocator.isPlaced(e, structureKey)) {
|
|
||||||
locateIrisStructure(e, structureKey, commandSender);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nativeStructure == null) {
|
|
||||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_FIND_UNKNOWN_STRUCTURE, MessageArgument.untrusted("structureKey", structureKey)));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final boolean replacementLocate = irisReplacement;
|
|
||||||
final boolean explicitNativePlacement = IrisStructureLocator.hasNativePlacement(
|
|
||||||
e, structureKey);
|
|
||||||
|
|
||||||
Player target = player();
|
Player target = player();
|
||||||
if (target == null) {
|
if (target == null) {
|
||||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_FIND_RUN_THIS_GAME_TELEPORT_STRUCTURE));
|
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_FIND_RUN_THIS_GAME_TELEPORT_STRUCTURE));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String structureKey = structure == null ? "" : structure.trim();
|
||||||
|
Structure nativeStructure = resolveNativeStructure(structureKey);
|
||||||
|
boolean registered = nativeStructure != null;
|
||||||
|
IrisNativeStructureDecision decision = registered
|
||||||
|
? NativeStructureGenerationPolicy.resolve(e, structureKey, false)
|
||||||
|
: null;
|
||||||
|
boolean nativePlacement = IrisStructureLocator.hasNativePlacement(e, structureKey);
|
||||||
|
boolean locatableNativePlacement = IrisStructureLocator.hasLocatableNativePlacement(e, structureKey);
|
||||||
|
boolean locatableEditablePlacement = IrisStructureLocator.hasLocatableEditablePlacement(e, structureKey);
|
||||||
World targetWorld = target.getWorld();
|
World targetWorld = target.getWorld();
|
||||||
|
boolean nativeGenerationEnabled = targetWorld.canGenerateStructures();
|
||||||
|
boolean requiresReachability = registered && decision.generate()
|
||||||
|
&& decision.status() != NativeStructureGenerationStatus.REPLACED_BY_IRIS
|
||||||
|
&& nativeGenerationEnabled;
|
||||||
|
boolean reachable = !requiresReachability || StructureReachability.isReachable(e, structureKey);
|
||||||
|
StructureLookupRoute route = selectStructureLookupRoute(
|
||||||
|
registered, decision, nativePlacement, locatableNativePlacement,
|
||||||
|
locatableEditablePlacement, nativeGenerationEnabled, reachable);
|
||||||
|
|
||||||
|
if (route == StructureLookupRoute.UNKNOWN) {
|
||||||
|
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_FIND_UNKNOWN_STRUCTURE, MessageArgument.untrusted("structureKey", structureKey)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (route == StructureLookupRoute.POLICY_DISABLED) {
|
||||||
|
commandSender.sendMessage(C.RED + NativeStructureGenerationPolicy.generationStatusMessage(
|
||||||
|
structureKey, decision.status()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (route == StructureLookupRoute.NO_ACTIVE_PLACEMENT) {
|
||||||
|
commandSender.sendMessage(C.YELLOW + structureKey
|
||||||
|
+ " has no active placement in this world.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (route == StructureLookupRoute.WORLD_DISABLED) {
|
||||||
|
commandSender.sendMessage(C.YELLOW + structureKey
|
||||||
|
+ " cannot generate because native structure generation is disabled for this world.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (route == StructureLookupRoute.UNREACHABLE) {
|
||||||
|
KList<String> miss = StructureReachability.missingBiomeKeys(e, structureKey);
|
||||||
|
commandSender.sendMessage(C.YELLOW + structureKey
|
||||||
|
+ " cannot generate in this world (its required biomes are not produced by this pack"
|
||||||
|
+ (miss.isEmpty() ? "" : ": needs " + String.join("/", miss)) + ").");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (route == StructureLookupRoute.IRIS) {
|
||||||
|
locateIrisStructure(e, structureKey, commandSender);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Location origin = target.getLocation();
|
Location origin = target.getLocation();
|
||||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_FIND_LOCATING, MessageArgument.untrusted("structureKey", structureKey)));
|
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_FIND_LOCATING, MessageArgument.untrusted("structureKey", structureKey)));
|
||||||
J.s(() -> {
|
J.s(() -> {
|
||||||
try {
|
try {
|
||||||
if (!replacementLocate && !explicitNativePlacement
|
|
||||||
&& !StructureReachability.isReachable(e, structureKey)) {
|
|
||||||
KList<String> miss = StructureReachability.missingBiomeKeys(e, structureKey);
|
|
||||||
sendStructureMessage(target, commandSender,
|
|
||||||
C.YELLOW + structureKey + " cannot generate in this world (its required biomes are not produced by this pack"
|
|
||||||
+ (miss.isEmpty() ? "" : ": needs " + String.join("/", miss)) + ").");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
StructureSearchResult result = targetWorld.locateNearestStructure(
|
StructureSearchResult result = targetWorld.locateNearestStructure(
|
||||||
origin, nativeStructure, 100, false);
|
origin, nativeStructure, 100, false);
|
||||||
if (result == null || result.getLocation() == null) {
|
if (result == null || result.getLocation() == null) {
|
||||||
@@ -191,6 +211,365 @@ public class CommandFind implements DirectorExecutor {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Director(description = "Print every structure excluded from /iris goto and its rejection reason to the server console", sync = true)
|
||||||
|
public void unregistered() {
|
||||||
|
VolmitSender commandSender = sender();
|
||||||
|
if (commandSender == null) {
|
||||||
|
Iris.reportError("Structure exclusion report started without a command sender context.",
|
||||||
|
new IllegalStateException("Missing command sender context"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Engine activeEngine = engine();
|
||||||
|
if (activeEngine == null) {
|
||||||
|
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_FIND_NOT_IRIS_WORLD));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Player target = player();
|
||||||
|
if (target == null) {
|
||||||
|
commandSender.sendMessage(C.RED + "Run this command from the Iris world to inspect its structures.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
StructureExclusionReport snapshot;
|
||||||
|
try {
|
||||||
|
snapshot = collectStructureExclusions(
|
||||||
|
activeEngine, target.getWorld().canGenerateStructures());
|
||||||
|
} catch (Throwable error) {
|
||||||
|
commandSender.sendMessage(C.RED + "Could not build the structure exclusion report; see the server console.");
|
||||||
|
Iris.reportError("Could not snapshot /iris goto unregistered report for world '"
|
||||||
|
+ target.getWorld().getName() + "'.", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String worldName = target.getWorld().getName();
|
||||||
|
J.a(() -> {
|
||||||
|
try {
|
||||||
|
StructureExclusionReport report = includeManagedDatapackStructures(
|
||||||
|
snapshot, DatapackIngestService.installed());
|
||||||
|
printStructureExclusionReport(worldName, report);
|
||||||
|
sendStructureMessage(target, commandSender, C.GREEN + "Printed " + report.entries().size()
|
||||||
|
+ " non-generating structure candidate(s) and their reasons to the server console. "
|
||||||
|
+ "This was an eligibility check; no chunks were searched.");
|
||||||
|
} catch (Throwable error) {
|
||||||
|
sendStructureMessage(target, commandSender,
|
||||||
|
C.RED + "Could not finish the structure exclusion report; see the server console.");
|
||||||
|
Iris.reportError("Could not finish /iris goto unregistered report for world '"
|
||||||
|
+ worldName + "'.", error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StructureExclusionReport collectStructureExclusions(
|
||||||
|
Engine engine, boolean nativeGenerationEnabled) {
|
||||||
|
PlatformStructureHooks structureHooks = IrisPlatforms.get().structureHooks();
|
||||||
|
Map<String, String> registeredKeys = distinctStructureKeys(structureHooks.structureKeys());
|
||||||
|
Set<String> reachableKeys = StructureReachability.reachableKeys(engine);
|
||||||
|
Set<String> possibleBiomeKeys = normalizedStructureKeys(
|
||||||
|
structureHooks.possibleBiomeKeys(engine.getWorld().platformWorld()));
|
||||||
|
List<StructureExclusion> exclusions = new ArrayList<>();
|
||||||
|
int registeredExcluded = 0;
|
||||||
|
|
||||||
|
for (Map.Entry<String, String> entry : registeredKeys.entrySet()) {
|
||||||
|
String normalizedKey = entry.getKey();
|
||||||
|
String key = entry.getValue();
|
||||||
|
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine, key, false);
|
||||||
|
boolean nativePlacement = IrisStructureLocator.hasNativePlacement(engine, key);
|
||||||
|
boolean locatableNativePlacement = IrisStructureLocator.hasLocatableNativePlacement(engine, key);
|
||||||
|
boolean locatableEditablePlacement = IrisStructureLocator.hasLocatableEditablePlacement(engine, key);
|
||||||
|
StructureLookupRoute route = selectStructureLookupRoute(
|
||||||
|
true, decision, nativePlacement, locatableNativePlacement,
|
||||||
|
locatableEditablePlacement, nativeGenerationEnabled,
|
||||||
|
reachableKeys.contains(normalizedKey));
|
||||||
|
if (route == StructureLookupRoute.IRIS || route == StructureLookupRoute.NATIVE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<String> requiredBiomes = needsNativeReachabilityReason(route, decision)
|
||||||
|
? structureHooks.structureBiomeKeys(key)
|
||||||
|
: List.of();
|
||||||
|
exclusions.add(new StructureExclusion(
|
||||||
|
StructureExclusionKind.EXCLUDED, key, null,
|
||||||
|
describeStructureExclusion(
|
||||||
|
route, key, decision.status(), nativePlacement,
|
||||||
|
requiredBiomes, possibleBiomeKeys)));
|
||||||
|
registeredExcluded++;
|
||||||
|
}
|
||||||
|
|
||||||
|
int configuredUnregistered = 0;
|
||||||
|
Set<String> configuredUnregisteredKeys = new LinkedHashSet<>();
|
||||||
|
for (String configuredKey : IrisStructureLocator.placedKeys(engine)) {
|
||||||
|
String normalizedKey = normalizeStructureKey(configuredKey);
|
||||||
|
if (normalizedKey.isEmpty()
|
||||||
|
|| registeredKeys.containsKey(normalizedKey)
|
||||||
|
|| !IrisStructureLocator.hasNativePlacement(engine, configuredKey)
|
||||||
|
|| !configuredUnregisteredKeys.add(normalizedKey)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
exclusions.add(new StructureExclusion(
|
||||||
|
StructureExclusionKind.UNREGISTERED, configuredKey, null,
|
||||||
|
describeConfiguredUnregisteredNative(
|
||||||
|
IrisStructureLocator.hasLocatableNativePlacement(engine, configuredKey))));
|
||||||
|
configuredUnregistered++;
|
||||||
|
}
|
||||||
|
|
||||||
|
int editableUnplaced = 0;
|
||||||
|
Set<String> unplacedEditableKeys = new LinkedHashSet<>();
|
||||||
|
Set<String> locatableEditableKeys = normalizedStructureKeys(
|
||||||
|
IrisStructureLocator.locatableEditableKeys(engine));
|
||||||
|
for (String editableKey : engine.getData().getStructureLoader().getPossibleKeys()) {
|
||||||
|
String normalizedKey = normalizeStructureKey(editableKey);
|
||||||
|
if (!isUnplacedEditableCandidate(
|
||||||
|
normalizedKey, registeredKeys.keySet(), locatableEditableKeys,
|
||||||
|
IrisStructureLocator.hasNativePlacement(engine, editableKey))
|
||||||
|
|| !unplacedEditableKeys.add(normalizedKey)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
boolean configuredPlacement = IrisStructureLocator.hasEditablePlacement(engine, editableKey);
|
||||||
|
String reason = configuredPlacement
|
||||||
|
? "editable Iris structure has a configured placement, but no matching placement is active "
|
||||||
|
+ "because its density is not positive or its Y band does not intersect this world"
|
||||||
|
: "editable Iris structure exists in this pack, but no structure placement references it";
|
||||||
|
exclusions.add(new StructureExclusion(
|
||||||
|
StructureExclusionKind.UNPLACED, editableKey, null, reason));
|
||||||
|
editableUnplaced++;
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> configuredImportUrls = normalizedImportUrls(
|
||||||
|
engine.getDimension().getDatapackImports());
|
||||||
|
return new StructureExclusionReport(
|
||||||
|
List.copyOf(exclusions), Set.copyOf(registeredKeys.keySet()),
|
||||||
|
configuredImportUrls, registeredExcluded, configuredUnregistered,
|
||||||
|
editableUnplaced, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean isUnplacedEditableCandidate(
|
||||||
|
String normalizedKey, Set<String> registeredKeys,
|
||||||
|
Set<String> locatableEditableKeys, boolean nativePlacement) {
|
||||||
|
return normalizedKey != null
|
||||||
|
&& !normalizedKey.isEmpty()
|
||||||
|
&& !registeredKeys.contains(normalizedKey)
|
||||||
|
&& !locatableEditableKeys.contains(normalizedKey)
|
||||||
|
&& !nativePlacement;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StructureExclusionReport includeManagedDatapackStructures(
|
||||||
|
StructureExclusionReport snapshot, List<DatapackIngestService.Entry> installedEntries) {
|
||||||
|
Map<String, ManagedDatapackStructure> unregisteredStructures = new LinkedHashMap<>();
|
||||||
|
for (DatapackIngestService.Entry entry : installedEntries) {
|
||||||
|
String importUrl = normalizeImportUrl(entry.url);
|
||||||
|
if (importUrl.isEmpty() || !snapshot.configuredImportUrls().contains(importUrl)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String sourceId = entry.id == null || entry.id.isBlank() ? "unknown" : entry.id.trim();
|
||||||
|
List<String> structureKeys = entry.structureKeys == null ? List.of() : entry.structureKeys;
|
||||||
|
for (String key : structureKeys) {
|
||||||
|
String normalizedKey = normalizeStructureKey(key);
|
||||||
|
if (normalizedKey.isEmpty() || snapshot.registeredKeys().contains(normalizedKey)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ManagedDatapackStructure managed = unregisteredStructures.computeIfAbsent(
|
||||||
|
normalizedKey, ignored -> new ManagedDatapackStructure(key.trim()));
|
||||||
|
managed.sourceIds().add(sourceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<StructureExclusion> entries = new ArrayList<>();
|
||||||
|
int configuredUnregistered = snapshot.configuredUnregistered();
|
||||||
|
for (StructureExclusion exclusion : snapshot.entries()) {
|
||||||
|
boolean replacedByManagedSource = exclusion.kind() == StructureExclusionKind.UNREGISTERED
|
||||||
|
&& unregisteredStructures.containsKey(normalizeStructureKey(exclusion.key()));
|
||||||
|
if (replacedByManagedSource) {
|
||||||
|
configuredUnregistered--;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
entries.add(exclusion);
|
||||||
|
}
|
||||||
|
for (ManagedDatapackStructure managed : unregisteredStructures.values()) {
|
||||||
|
entries.add(new StructureExclusion(
|
||||||
|
StructureExclusionKind.UNREGISTERED, managed.key(),
|
||||||
|
String.join(",", managed.sourceIds()),
|
||||||
|
"declared by an Iris-managed datapack but absent from the live registry; "
|
||||||
|
+ "restart, enablement, or datapack validation may be required"));
|
||||||
|
}
|
||||||
|
sortStructureExclusions(entries);
|
||||||
|
return new StructureExclusionReport(
|
||||||
|
List.copyOf(entries), snapshot.registeredKeys(), snapshot.configuredImportUrls(),
|
||||||
|
snapshot.registeredExcluded(), Math.max(0, configuredUnregistered),
|
||||||
|
snapshot.editableUnplaced(), unregisteredStructures.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void printStructureExclusionReport(
|
||||||
|
String worldName, StructureExclusionReport report) {
|
||||||
|
Iris.info("Iris goto unregistered report for world '%s': %d non-generating structure candidate(s).",
|
||||||
|
worldName, report.entries().size());
|
||||||
|
for (StructureExclusion exclusion : report.entries()) {
|
||||||
|
String source = exclusion.source() == null
|
||||||
|
? ""
|
||||||
|
: " (source " + exclusion.source() + ")";
|
||||||
|
Iris.info("[%s] %s%s: %s",
|
||||||
|
exclusion.kind().label(), exclusion.key(), source, exclusion.reason());
|
||||||
|
}
|
||||||
|
Iris.info("Iris goto unregistered summary: %d registered key(s) excluded, "
|
||||||
|
+ "%d managed datapack key(s) unregistered, %d configured native key(s) unregistered, "
|
||||||
|
+ "%d editable Iris structure(s) unplaced.",
|
||||||
|
report.registeredExcluded(), report.managedUnregistered(),
|
||||||
|
report.configuredUnregistered(), report.editableUnplaced());
|
||||||
|
Iris.info("Eligibility report only; no chunks were searched and existing generated starts were not scanned.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean needsNativeReachabilityReason(
|
||||||
|
StructureLookupRoute route, IrisNativeStructureDecision decision) {
|
||||||
|
return route == StructureLookupRoute.UNREACHABLE
|
||||||
|
|| route == StructureLookupRoute.NO_ACTIVE_PLACEMENT && decision.generate();
|
||||||
|
}
|
||||||
|
|
||||||
|
static String describeStructureExclusion(
|
||||||
|
StructureLookupRoute route, String key, NativeStructureGenerationStatus status,
|
||||||
|
boolean nativePlacement, List<String> requiredBiomes, Set<String> possibleBiomeKeys) {
|
||||||
|
return switch (route) {
|
||||||
|
case UNKNOWN -> "the key is not registered by the active server/datapack";
|
||||||
|
case POLICY_DISABLED -> NativeStructureGenerationPolicy.generationStatusMessage(key, status);
|
||||||
|
case WORLD_DISABLED -> "native structure generation is disabled for this world";
|
||||||
|
case UNREACHABLE -> nativeReachabilityReason(requiredBiomes, possibleBiomeKeys);
|
||||||
|
case NO_ACTIVE_PLACEMENT -> {
|
||||||
|
String placementType = nativePlacement
|
||||||
|
? "configured nativeStructures placement"
|
||||||
|
: "configured Iris replacement";
|
||||||
|
String reason = placementType + " is inactive: no matching placement has positive density "
|
||||||
|
+ "when density-based and a Y band intersecting this world";
|
||||||
|
if (status == NativeStructureGenerationStatus.GENERATE_NATIVE) {
|
||||||
|
reason += "; the native route is also inactive because "
|
||||||
|
+ nativeReachabilityReason(requiredBiomes, possibleBiomeKeys);
|
||||||
|
}
|
||||||
|
yield reason;
|
||||||
|
}
|
||||||
|
case IRIS, NATIVE -> throw new IllegalArgumentException(
|
||||||
|
"Active structure route cannot be described as excluded: " + route);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static String nativeReachabilityReason(
|
||||||
|
List<String> requiredBiomes, Set<String> possibleBiomeKeys) {
|
||||||
|
if (requiredBiomes == null || requiredBiomes.isEmpty()) {
|
||||||
|
return "its resolved biome filter is empty";
|
||||||
|
}
|
||||||
|
Set<String> possible = possibleBiomeKeys == null ? Set.of() : possibleBiomeKeys;
|
||||||
|
for (String requiredBiome : requiredBiomes) {
|
||||||
|
if (possible.contains(normalizeStructureKey(requiredBiome))) {
|
||||||
|
return "no active positive-weight, positive-frequency structure-set entry includes it in this world";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "this pack does not produce any of its required biome(s): "
|
||||||
|
+ String.join("/", requiredBiomes);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String describeConfiguredUnregisteredNative(boolean locatablePlacement) {
|
||||||
|
String reason = "configured in nativeStructures, but the key is not registered by the active "
|
||||||
|
+ "server/datapack, so Minecraft cannot create its native structure start";
|
||||||
|
return locatablePlacement
|
||||||
|
? reason
|
||||||
|
: reason + "; its Iris placement is also inactive because no matching placement has positive "
|
||||||
|
+ "density when density-based and a Y band intersecting this world";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, String> distinctStructureKeys(List<String> keys) {
|
||||||
|
Map<String, String> distinctKeys = new LinkedHashMap<>();
|
||||||
|
for (String key : keys) {
|
||||||
|
String normalizedKey = normalizeStructureKey(key);
|
||||||
|
if (!normalizedKey.isEmpty()) {
|
||||||
|
distinctKeys.putIfAbsent(normalizedKey, key.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return distinctKeys;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Set<String> normalizedStructureKeys(Iterable<String> keys) {
|
||||||
|
Set<String> normalizedKeys = new LinkedHashSet<>();
|
||||||
|
if (keys == null) {
|
||||||
|
return normalizedKeys;
|
||||||
|
}
|
||||||
|
for (String key : keys) {
|
||||||
|
String normalizedKey = normalizeStructureKey(key);
|
||||||
|
if (!normalizedKey.isEmpty()) {
|
||||||
|
normalizedKeys.add(normalizedKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return normalizedKeys;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Set<String> normalizedImportUrls(Iterable<String> urls) {
|
||||||
|
Set<String> normalizedUrls = new LinkedHashSet<>();
|
||||||
|
if (urls == null) {
|
||||||
|
return normalizedUrls;
|
||||||
|
}
|
||||||
|
for (String url : urls) {
|
||||||
|
String normalizedUrl = normalizeImportUrl(url);
|
||||||
|
if (!normalizedUrl.isEmpty()) {
|
||||||
|
normalizedUrls.add(normalizedUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Set.copyOf(normalizedUrls);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalizeImportUrl(String url) {
|
||||||
|
return url == null ? "" : url.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void sortStructureExclusions(List<StructureExclusion> exclusions) {
|
||||||
|
exclusions.sort((left, right) -> {
|
||||||
|
int kindComparison = left.kind().compareTo(right.kind());
|
||||||
|
return kindComparison == 0
|
||||||
|
? left.key().compareToIgnoreCase(right.key())
|
||||||
|
: kindComparison;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalizeStructureKey(String key) {
|
||||||
|
return key == null ? "" : key.trim().toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
static StructureLookupRoute selectStructureLookupRoute(
|
||||||
|
boolean registered, IrisNativeStructureDecision decision, boolean nativePlacement,
|
||||||
|
boolean locatableNativePlacement, boolean locatableEditablePlacement,
|
||||||
|
boolean nativeGenerationEnabled, boolean reachable) {
|
||||||
|
if (!registered) {
|
||||||
|
return !nativePlacement && locatableEditablePlacement
|
||||||
|
? StructureLookupRoute.IRIS
|
||||||
|
: StructureLookupRoute.UNKNOWN;
|
||||||
|
}
|
||||||
|
if (decision == null) {
|
||||||
|
throw new IllegalArgumentException("Registered structure lookup requires a generation decision");
|
||||||
|
}
|
||||||
|
if (!decision.generate()
|
||||||
|
&& decision.status() != NativeStructureGenerationStatus.REPLACED_BY_IRIS) {
|
||||||
|
return StructureLookupRoute.POLICY_DISABLED;
|
||||||
|
}
|
||||||
|
if (decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS) {
|
||||||
|
if (!nativePlacement) {
|
||||||
|
return locatableEditablePlacement
|
||||||
|
? StructureLookupRoute.IRIS
|
||||||
|
: StructureLookupRoute.NO_ACTIVE_PLACEMENT;
|
||||||
|
}
|
||||||
|
if (!nativeGenerationEnabled) {
|
||||||
|
return StructureLookupRoute.WORLD_DISABLED;
|
||||||
|
}
|
||||||
|
return locatableNativePlacement
|
||||||
|
? StructureLookupRoute.NATIVE
|
||||||
|
: StructureLookupRoute.NO_ACTIVE_PLACEMENT;
|
||||||
|
}
|
||||||
|
if (!nativeGenerationEnabled) {
|
||||||
|
return StructureLookupRoute.WORLD_DISABLED;
|
||||||
|
}
|
||||||
|
if (nativePlacement && locatableNativePlacement) {
|
||||||
|
return StructureLookupRoute.NATIVE;
|
||||||
|
}
|
||||||
|
if (reachable) {
|
||||||
|
return StructureLookupRoute.NATIVE;
|
||||||
|
}
|
||||||
|
return nativePlacement
|
||||||
|
? StructureLookupRoute.NO_ACTIVE_PLACEMENT
|
||||||
|
: StructureLookupRoute.UNREACHABLE;
|
||||||
|
}
|
||||||
|
|
||||||
private static Structure resolveNativeStructure(String structureKey) {
|
private static Structure resolveNativeStructure(String structureKey) {
|
||||||
Registry<Structure> structureRegistry = Bukkit.getRegistry(Structure.class);
|
Registry<Structure> structureRegistry = Bukkit.getRegistry(Structure.class);
|
||||||
if (structureRegistry == null) {
|
if (structureRegistry == null) {
|
||||||
@@ -314,4 +693,46 @@ public class CommandFind implements DirectorExecutor {
|
|||||||
private void sendStructureMessage(Player target, VolmitSender commandSender, String message) {
|
private void sendStructureMessage(Player target, VolmitSender commandSender, String message) {
|
||||||
J.runEntity(target, () -> commandSender.sendMessage(message));
|
J.runEntity(target, () -> commandSender.sendMessage(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum StructureLookupRoute {
|
||||||
|
UNKNOWN,
|
||||||
|
POLICY_DISABLED,
|
||||||
|
NO_ACTIVE_PLACEMENT,
|
||||||
|
WORLD_DISABLED,
|
||||||
|
UNREACHABLE,
|
||||||
|
IRIS,
|
||||||
|
NATIVE
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum StructureExclusionKind {
|
||||||
|
UNREGISTERED("unregistered"),
|
||||||
|
EXCLUDED("excluded"),
|
||||||
|
UNPLACED("unplaced");
|
||||||
|
|
||||||
|
private final String label;
|
||||||
|
|
||||||
|
StructureExclusionKind(String label) {
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String label() {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record StructureExclusion(
|
||||||
|
StructureExclusionKind kind, String key, String source, String reason) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record StructureExclusionReport(
|
||||||
|
List<StructureExclusion> entries, Set<String> registeredKeys,
|
||||||
|
Set<String> configuredImportUrls, int registeredExcluded,
|
||||||
|
int configuredUnregistered, int editableUnplaced, int managedUnregistered) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ManagedDatapackStructure(String key, Set<String> sourceIds) {
|
||||||
|
private ManagedDatapackStructure(String key) {
|
||||||
|
this(key, new LinkedHashSet<>());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+181
-16
@@ -1,12 +1,15 @@
|
|||||||
package art.arcane.iris.core.commands;
|
package art.arcane.iris.core.commands;
|
||||||
|
|
||||||
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
||||||
|
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
||||||
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
|
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
@@ -25,32 +28,189 @@ public class IrisStructureLocateCommandContractTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void findRoutesRegisteredReplacementThroughPersistedNativeLocate() throws IOException {
|
public void findRoutesOnlyActiveWorldStructures() throws IOException {
|
||||||
String source = Files.readString(Path.of(System.getProperty("iris.commandFindSource")));
|
String source = Files.readString(Path.of(System.getProperty("iris.commandFindSource")));
|
||||||
int methodStart = source.indexOf("public void structure(");
|
int methodStart = source.indexOf("public void structure(");
|
||||||
int methodEnd = source.indexOf("private static Structure resolveNativeStructure", methodStart);
|
int methodEnd = source.indexOf("static StructureLookupRoute selectStructureLookupRoute", methodStart);
|
||||||
String method = source.substring(methodStart, methodEnd);
|
String method = source.substring(methodStart, methodEnd);
|
||||||
int nativeResolution = method.indexOf("resolveNativeStructure(structureKey)");
|
int nativeResolution = method.indexOf("resolveNativeStructure(structureKey)");
|
||||||
int policyResolution = method.indexOf("NativeStructureGenerationPolicy.resolve(", nativeResolution);
|
int policyResolution = method.indexOf("NativeStructureGenerationPolicy.resolve(", nativeResolution);
|
||||||
int replacementCheck = method.indexOf(
|
int nativeLocatableCheck = method.indexOf(
|
||||||
"decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS", policyResolution);
|
"IrisStructureLocator.hasLocatableNativePlacement(e, structureKey)", policyResolution);
|
||||||
int genericIrisLookup = method.indexOf(
|
int editableLocatableCheck = method.indexOf(
|
||||||
"nativeStructure == null && IrisStructureLocator.isPlaced(e, structureKey)", replacementCheck);
|
"IrisStructureLocator.hasLocatableEditablePlacement(e, structureKey)", nativeLocatableCheck);
|
||||||
int replacementLocate = method.indexOf("final boolean replacementLocate = irisReplacement", genericIrisLookup);
|
int worldGenerationCheck = method.indexOf("targetWorld.canGenerateStructures()", editableLocatableCheck);
|
||||||
int nativeLocate = method.indexOf("targetWorld.locateNearestStructure(", genericIrisLookup);
|
int routeResolution = method.indexOf("selectStructureLookupRoute(", editableLocatableCheck);
|
||||||
|
int nativeLocate = method.indexOf("targetWorld.locateNearestStructure(", routeResolution);
|
||||||
|
|
||||||
assertTrue(nativeResolution >= 0);
|
assertTrue(nativeResolution >= 0);
|
||||||
assertTrue(policyResolution > nativeResolution);
|
assertTrue(policyResolution > nativeResolution);
|
||||||
assertTrue(replacementCheck > policyResolution);
|
assertTrue(nativeLocatableCheck > policyResolution);
|
||||||
assertTrue(genericIrisLookup > replacementCheck);
|
assertTrue(editableLocatableCheck > nativeLocatableCheck);
|
||||||
assertTrue(replacementLocate > genericIrisLookup);
|
assertTrue(worldGenerationCheck > editableLocatableCheck);
|
||||||
assertTrue(nativeLocate > policyResolution);
|
assertTrue(routeResolution > editableLocatableCheck);
|
||||||
assertTrue(method.contains("irisReplacement && !IrisStructureLocator.hasNativePlacement"));
|
assertTrue(nativeLocate > routeResolution);
|
||||||
assertTrue(method.contains("!replacementLocate && !explicitNativePlacement"));
|
assertFalse(method.contains("IrisStructureLocator.isPlaced(e, structureKey)"));
|
||||||
assertTrue(method.contains("&& !StructureReachability.isReachable"));
|
|
||||||
assertTrue(method.contains("decision.status() != NativeStructureGenerationStatus.REPLACED_BY_IRIS"));
|
|
||||||
assertFalse(method.contains("NativeStructureLocateCapability"));
|
assertFalse(method.contains("NativeStructureLocateCapability"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void findRejectsUnregisteredNativeAndDormantPlacements() {
|
||||||
|
assertEquals(CommandFind.StructureLookupRoute.UNKNOWN,
|
||||||
|
CommandFind.selectStructureLookupRoute(
|
||||||
|
false, null, true, true, false, true, true));
|
||||||
|
assertEquals(CommandFind.StructureLookupRoute.UNKNOWN,
|
||||||
|
CommandFind.selectStructureLookupRoute(
|
||||||
|
false, null, false, false, false, true, true));
|
||||||
|
assertEquals(CommandFind.StructureLookupRoute.NO_ACTIVE_PLACEMENT,
|
||||||
|
CommandFind.selectStructureLookupRoute(
|
||||||
|
true, decision(NativeStructureGenerationStatus.REPLACED_BY_IRIS),
|
||||||
|
false, false, false, true, true));
|
||||||
|
assertEquals(CommandFind.StructureLookupRoute.NO_ACTIVE_PLACEMENT,
|
||||||
|
CommandFind.selectStructureLookupRoute(
|
||||||
|
true, decision(NativeStructureGenerationStatus.GENERATE_NATIVE),
|
||||||
|
true, false, false, true, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void findAllowsLocatableEditablePlacementsWithoutNativeGeneration() {
|
||||||
|
assertEquals(CommandFind.StructureLookupRoute.IRIS,
|
||||||
|
CommandFind.selectStructureLookupRoute(
|
||||||
|
false, null, false, false, true, false, false));
|
||||||
|
assertEquals(CommandFind.StructureLookupRoute.IRIS,
|
||||||
|
CommandFind.selectStructureLookupRoute(
|
||||||
|
true, decision(NativeStructureGenerationStatus.REPLACED_BY_IRIS),
|
||||||
|
false, false, true, false, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void findRequiresWorldGenerationAndReachabilityForNativeRoutes() {
|
||||||
|
assertEquals(CommandFind.StructureLookupRoute.WORLD_DISABLED,
|
||||||
|
CommandFind.selectStructureLookupRoute(
|
||||||
|
true, decision(NativeStructureGenerationStatus.REPLACED_BY_IRIS),
|
||||||
|
true, true, false, false, true));
|
||||||
|
assertEquals(CommandFind.StructureLookupRoute.WORLD_DISABLED,
|
||||||
|
CommandFind.selectStructureLookupRoute(
|
||||||
|
true, decision(NativeStructureGenerationStatus.GENERATE_NATIVE),
|
||||||
|
false, false, false, false, true));
|
||||||
|
assertEquals(CommandFind.StructureLookupRoute.UNREACHABLE,
|
||||||
|
CommandFind.selectStructureLookupRoute(
|
||||||
|
true, decision(NativeStructureGenerationStatus.GENERATE_NATIVE),
|
||||||
|
false, false, false, true, false));
|
||||||
|
assertEquals(CommandFind.StructureLookupRoute.NATIVE,
|
||||||
|
CommandFind.selectStructureLookupRoute(
|
||||||
|
true, decision(NativeStructureGenerationStatus.GENERATE_NATIVE),
|
||||||
|
true, true, false, true, false));
|
||||||
|
assertEquals(CommandFind.StructureLookupRoute.NATIVE,
|
||||||
|
CommandFind.selectStructureLookupRoute(
|
||||||
|
true, decision(NativeStructureGenerationStatus.GENERATE_NATIVE),
|
||||||
|
true, false, false, true, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void findHonorsPackDisableBeforePlacementRouting() {
|
||||||
|
assertEquals(CommandFind.StructureLookupRoute.POLICY_DISABLED,
|
||||||
|
CommandFind.selectStructureLookupRoute(
|
||||||
|
true, decision(NativeStructureGenerationStatus.DISABLED_BY_PACK),
|
||||||
|
false, false, true, true, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unregisteredDiagnosticExplainsEveryRejectedRoute() {
|
||||||
|
assertEquals(
|
||||||
|
"Native structure minecraft:village is disabled by this dimension's importedStructures settings.",
|
||||||
|
CommandFind.describeStructureExclusion(
|
||||||
|
CommandFind.StructureLookupRoute.POLICY_DISABLED, "minecraft:village",
|
||||||
|
NativeStructureGenerationStatus.DISABLED_BY_PACK,
|
||||||
|
false, List.of(), Set.of()));
|
||||||
|
assertEquals(
|
||||||
|
"native structure generation is disabled for this world",
|
||||||
|
CommandFind.describeStructureExclusion(
|
||||||
|
CommandFind.StructureLookupRoute.WORLD_DISABLED, "minecraft:village",
|
||||||
|
NativeStructureGenerationStatus.GENERATE_NATIVE,
|
||||||
|
false, List.of(), Set.of()));
|
||||||
|
assertEquals(
|
||||||
|
"its resolved biome filter is empty",
|
||||||
|
CommandFind.describeStructureExclusion(
|
||||||
|
CommandFind.StructureLookupRoute.UNREACHABLE, "towns_and_towers:exclusive",
|
||||||
|
NativeStructureGenerationStatus.GENERATE_NATIVE,
|
||||||
|
false, List.of(), Set.of()));
|
||||||
|
assertEquals(
|
||||||
|
"this pack does not produce any of its required biome(s): terralith:alpine_grove/bwg:aspen_boreal",
|
||||||
|
CommandFind.describeStructureExclusion(
|
||||||
|
CommandFind.StructureLookupRoute.UNREACHABLE, "towns_and_towers:exclusive",
|
||||||
|
NativeStructureGenerationStatus.GENERATE_NATIVE,
|
||||||
|
false, List.of("terralith:alpine_grove", "bwg:aspen_boreal"),
|
||||||
|
Set.of("minecraft:plains")));
|
||||||
|
assertEquals(
|
||||||
|
"no active positive-weight, positive-frequency structure-set entry includes it in this world",
|
||||||
|
CommandFind.describeStructureExclusion(
|
||||||
|
CommandFind.StructureLookupRoute.UNREACHABLE, "example:dormant",
|
||||||
|
NativeStructureGenerationStatus.GENERATE_NATIVE,
|
||||||
|
false, List.of("minecraft:plains"), Set.of("minecraft:plains")));
|
||||||
|
assertEquals(
|
||||||
|
"no active positive-weight, positive-frequency structure-set entry includes it in this world",
|
||||||
|
CommandFind.describeStructureExclusion(
|
||||||
|
CommandFind.StructureLookupRoute.UNREACHABLE, "example:partial_overlap",
|
||||||
|
NativeStructureGenerationStatus.GENERATE_NATIVE,
|
||||||
|
false, List.of("minecraft:plains", "mod:absent"), Set.of("minecraft:plains")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unregisteredDiagnosticExplainsInactiveAndMissingNativePlacements() {
|
||||||
|
assertEquals(
|
||||||
|
"configured Iris replacement is inactive: no matching placement has positive density when "
|
||||||
|
+ "density-based and a Y band intersecting this world",
|
||||||
|
CommandFind.describeStructureExclusion(
|
||||||
|
CommandFind.StructureLookupRoute.NO_ACTIVE_PLACEMENT, "example:replacement",
|
||||||
|
NativeStructureGenerationStatus.REPLACED_BY_IRIS,
|
||||||
|
false, List.of(), Set.of()));
|
||||||
|
assertEquals(
|
||||||
|
"configured nativeStructures placement is inactive: no matching placement has positive density "
|
||||||
|
+ "when density-based and a Y band intersecting this world; the native route is also "
|
||||||
|
+ "inactive because its resolved biome filter is empty",
|
||||||
|
CommandFind.describeStructureExclusion(
|
||||||
|
CommandFind.StructureLookupRoute.NO_ACTIVE_PLACEMENT, "example:native",
|
||||||
|
NativeStructureGenerationStatus.GENERATE_NATIVE,
|
||||||
|
true, List.of(), Set.of()));
|
||||||
|
assertTrue(CommandFind.describeConfiguredUnregisteredNative(true)
|
||||||
|
.contains("key is not registered by the active server/datapack"));
|
||||||
|
assertFalse(CommandFind.describeConfiguredUnregisteredNative(true)
|
||||||
|
.contains("Iris placement is also inactive"));
|
||||||
|
assertTrue(CommandFind.describeConfiguredUnregisteredNative(false)
|
||||||
|
.contains("Iris placement is also inactive"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unplacedEditableDiagnosticDefersToRegistryAndNativeCategories() {
|
||||||
|
assertTrue(CommandFind.isUnplacedEditableCandidate(
|
||||||
|
"pack:editable", Set.of(), Set.of(), false));
|
||||||
|
assertFalse(CommandFind.isUnplacedEditableCandidate(
|
||||||
|
"pack:editable", Set.of("pack:editable"), Set.of(), false));
|
||||||
|
assertFalse(CommandFind.isUnplacedEditableCandidate(
|
||||||
|
"pack:editable", Set.of(), Set.of("pack:editable"), false));
|
||||||
|
assertFalse(CommandFind.isUnplacedEditableCandidate(
|
||||||
|
"pack:editable", Set.of(), Set.of(), true));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unregisteredDiagnosticPrintsDetailsOnlyToConsole() throws IOException {
|
||||||
|
String source = Files.readString(Path.of(System.getProperty("iris.commandFindSource")));
|
||||||
|
int methodStart = source.indexOf("public void unregistered()");
|
||||||
|
int methodEnd = source.indexOf("private static StructureExclusionReport", methodStart);
|
||||||
|
String method = source.substring(methodStart, methodEnd);
|
||||||
|
|
||||||
|
assertTrue(methodStart >= 0);
|
||||||
|
assertTrue(source.contains("Iris.info(\"[%s] %s%s: %s\""));
|
||||||
|
assertTrue(method.contains("non-generating structure candidate(s) and their reasons to the server console"));
|
||||||
|
assertTrue(method.contains("no chunks were searched"));
|
||||||
|
assertTrue(method.indexOf("DatapackIngestService.installed()") > method.indexOf("J.a(() ->"));
|
||||||
|
assertTrue(source.contains("engine.getDimension().getDatapackImports()"));
|
||||||
|
assertTrue(source.contains("snapshot.configuredImportUrls().contains(importUrl)"));
|
||||||
|
assertTrue(source.contains("engine.getData().getStructureLoader().getPossibleKeys()"));
|
||||||
|
assertFalse(method.contains("commandSender.sendMessage(exclusion"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void findNativePolicyMessagesMatchModdedDiagnostics() {
|
public void findNativePolicyMessagesMatchModdedDiagnostics() {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
@@ -95,4 +255,9 @@ public class IrisStructureLocateCommandContractTest {
|
|||||||
assertTrue(reachabilityGuard > nativeRequirement);
|
assertTrue(reachabilityGuard > nativeRequirement);
|
||||||
assertTrue(reachabilityLookup > reachabilityGuard);
|
assertTrue(reachabilityLookup > reachabilityGuard);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static IrisNativeStructureDecision decision(NativeStructureGenerationStatus status) {
|
||||||
|
return new IrisNativeStructureDecision(
|
||||||
|
status, 0, null, false, false, null, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+117
@@ -0,0 +1,117 @@
|
|||||||
|
/*
|
||||||
|
* Iris is a World Generator for Minecraft Servers
|
||||||
|
* Copyright (c) 2026 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.nativegen;
|
||||||
|
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.world.level.biome.Biome;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureSet;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.lang.reflect.Modifier;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public final class NativeStructureGenerationKeys {
|
||||||
|
private static final Method FREQUENCY_METHOD = resolveFrequencyMethod();
|
||||||
|
|
||||||
|
private NativeStructureGenerationKeys() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Set<String> reachable(ServerLevel level, Set<String> possibleBiomeKeys) {
|
||||||
|
ServerLevel activeLevel = Objects.requireNonNull(level, "Native structure reachability requires a level");
|
||||||
|
Set<String> possibleBiomes = Objects.requireNonNull(
|
||||||
|
possibleBiomeKeys, "Native structure reachability requires possible biome keys");
|
||||||
|
if (!activeLevel.getServer().getWorldGenSettings().options().generateStructures()) {
|
||||||
|
return Collections.emptySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> keys = new LinkedHashSet<>();
|
||||||
|
List<Holder<StructureSet>> structureSets =
|
||||||
|
activeLevel.getChunkSource().getGeneratorState().possibleStructureSets();
|
||||||
|
for (Holder<StructureSet> structureSetHolder : structureSets) {
|
||||||
|
StructureSet structureSet = structureSetHolder.value();
|
||||||
|
if (!isEnabledPlacement(structureSet.placement())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (StructureSet.StructureSelectionEntry entry : structureSet.structures()) {
|
||||||
|
if (entry.weight() <= 0 || !hasPossibleBiome(entry.structure().value(), possibleBiomes)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Optional<ResourceKey<Structure>> key = entry.structure().unwrapKey();
|
||||||
|
key.ifPresent(resourceKey -> keys.add(resourceKey.identifier().toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Collections.unmodifiableSet(keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean hasPossibleBiome(Structure structure, Set<String> possibleBiomeKeys) {
|
||||||
|
for (Holder<Biome> holder : structure.biomes()) {
|
||||||
|
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
|
||||||
|
if (key.isPresent() && possibleBiomeKeys.contains(key.get().identifier().toString())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean isEnabledPlacement(StructurePlacement placement) {
|
||||||
|
return placementFrequency(placement) > 0.0F;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float placementFrequency(StructurePlacement placement) {
|
||||||
|
StructurePlacement activePlacement = Objects.requireNonNull(
|
||||||
|
placement, "Native structure placement frequency requires a placement");
|
||||||
|
try {
|
||||||
|
return ((Float) FREQUENCY_METHOD.invoke(activePlacement)).floatValue();
|
||||||
|
} catch (ReflectiveOperationException error) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Unable to read native structure placement frequency from "
|
||||||
|
+ activePlacement.getClass().getName(), error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Method resolveFrequencyMethod() {
|
||||||
|
Method frequencyMethod = null;
|
||||||
|
for (Method method : StructurePlacement.class.getDeclaredMethods()) {
|
||||||
|
if (Modifier.isStatic(method.getModifiers())
|
||||||
|
|| method.getParameterCount() != 0
|
||||||
|
|| method.getReturnType() != float.class) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (frequencyMethod != null) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Native structure placement exposes multiple zero-argument float accessors");
|
||||||
|
}
|
||||||
|
frequencyMethod = method;
|
||||||
|
}
|
||||||
|
if (frequencyMethod == null || !frequencyMethod.trySetAccessible()) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Native structure placement frequency accessor is unavailable");
|
||||||
|
}
|
||||||
|
return frequencyMethod;
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-18
@@ -19,6 +19,7 @@
|
|||||||
package art.arcane.iris.modded;
|
package art.arcane.iris.modded;
|
||||||
|
|
||||||
import art.arcane.iris.nativegen.NativeStructureGenerationException;
|
import art.arcane.iris.nativegen.NativeStructureGenerationException;
|
||||||
|
import art.arcane.iris.nativegen.NativeStructureGenerationKeys;
|
||||||
import art.arcane.iris.nativegen.NativeStructureFactory;
|
import art.arcane.iris.nativegen.NativeStructureFactory;
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
import art.arcane.iris.spi.PlatformStructureHooks;
|
import art.arcane.iris.spi.PlatformStructureHooks;
|
||||||
@@ -225,22 +226,15 @@ public final class ModdedStructureHooks implements PlatformStructureHooks {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<String> reachableStructureKeys(PlatformWorld world) {
|
public List<String> reachableStructureKeys(PlatformWorld world) {
|
||||||
List<String> keys = new ArrayList<>();
|
|
||||||
ServerLevel level = requireLevel(world, "resolve reachable structures");
|
ServerLevel level = requireLevel(world, "resolve reachable structures");
|
||||||
try {
|
try {
|
||||||
BiomeSource source = level.getChunkSource().getGenerator().getBiomeSource();
|
BiomeSource source = level.getChunkSource().getGenerator().getBiomeSource();
|
||||||
Set<String> possibleBiomes = possibleBiomeKeys(source);
|
Set<String> possibleBiomes = possibleBiomeKeys(source);
|
||||||
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
return new ArrayList<>(NativeStructureGenerationKeys.reachable(level, possibleBiomes));
|
||||||
for (Map.Entry<ResourceKey<Structure>, Structure> entry : registry.entrySet()) {
|
|
||||||
if (hasPossibleBiome(entry.getValue(), possibleBiomes)) {
|
|
||||||
keys.add(entry.getKey().identifier().toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (RuntimeException error) {
|
} catch (RuntimeException error) {
|
||||||
throw new IllegalStateException("Iris failed to resolve reachable structures for modded level '"
|
throw new IllegalStateException("Iris failed to resolve reachable structures for modded level '"
|
||||||
+ level.dimension().identifier() + "'", error);
|
+ level.dimension().identifier() + "'", error);
|
||||||
}
|
}
|
||||||
return keys;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -401,16 +395,6 @@ public final class ModdedStructureHooks implements PlatformStructureHooks {
|
|||||||
return keys;
|
return keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean hasPossibleBiome(Structure structure, Set<String> possibleBiomeKeys) {
|
|
||||||
for (Holder<Biome> holder : structure.biomes()) {
|
|
||||||
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
|
|
||||||
if (key.isPresent() && possibleBiomeKeys.contains(key.get().identifier().toString())) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ServerLevel level(PlatformWorld world) {
|
private static ServerLevel level(PlatformWorld world) {
|
||||||
return world instanceof ModdedPlatformWorld moddedWorld ? moddedWorld.level() : null;
|
return world instanceof ModdedPlatformWorld moddedWorld ? moddedWorld.level() : null;
|
||||||
}
|
}
|
||||||
|
|||||||
+4
@@ -54,6 +54,9 @@ import static art.arcane.iris.modded.command.ModdedCommandFeedback.USAGE_ICON;
|
|||||||
final class ModdedCommandHelp {
|
final class ModdedCommandHelp {
|
||||||
private static final int PAGE_SIZE = 17;
|
private static final int PAGE_SIZE = 17;
|
||||||
private static final int PAGE_BUTTON_WIDTH = 10;
|
private static final int PAGE_BUTTON_WIDTH = 10;
|
||||||
|
private static final TextKey COMMAND_UNREGISTERED = TextKey.of(
|
||||||
|
"iris.modded.help.entry.command.unregistered",
|
||||||
|
"Print every structure excluded from goto completion and its exact reason to the server console");
|
||||||
private static final Map<String, List<Entry>> SECTIONS = new LinkedHashMap<>();
|
private static final Map<String, List<Entry>> SECTIONS = new LinkedHashMap<>();
|
||||||
|
|
||||||
static {
|
static {
|
||||||
@@ -98,6 +101,7 @@ final class ModdedCommandHelp {
|
|||||||
Entry.command("biome", "<key>", ModdedHelpMessages.COMMAND_BIOME_FIND_AN_IRIS_BIOME),
|
Entry.command("biome", "<key>", ModdedHelpMessages.COMMAND_BIOME_FIND_AN_IRIS_BIOME),
|
||||||
Entry.command("region", "<key>", ModdedHelpMessages.COMMAND_REGION_FIND_AN_IRIS_REGION),
|
Entry.command("region", "<key>", ModdedHelpMessages.COMMAND_REGION_FIND_AN_IRIS_REGION),
|
||||||
Entry.command("object", "<key>", ModdedHelpMessages.COMMAND_OBJECT_FIND_AN_OBJECT_PLACEMENT),
|
Entry.command("object", "<key>", ModdedHelpMessages.COMMAND_OBJECT_FIND_AN_OBJECT_PLACEMENT),
|
||||||
|
Entry.command("unregistered", "", COMMAND_UNREGISTERED),
|
||||||
Entry.command("structure", "<key>", ModdedHelpMessages.COMMAND_STRUCTURE_FIND_AN_IRIS_PLACED_OR_NATIVE_DATAPACK_STRUCTURE),
|
Entry.command("structure", "<key>", ModdedHelpMessages.COMMAND_STRUCTURE_FIND_AN_IRIS_PLACED_OR_NATIVE_DATAPACK_STRUCTURE),
|
||||||
Entry.command("poi", "<type>", ModdedHelpMessages.COMMAND_POI_FIND_A_SUPPORTED_POINT_OF_INTEREST)
|
Entry.command("poi", "<type>", ModdedHelpMessages.COMMAND_POI_FIND_A_SUPPORTED_POINT_OF_INTEREST)
|
||||||
));
|
));
|
||||||
|
|||||||
+90
-13
@@ -21,6 +21,10 @@ package art.arcane.iris.modded.command;
|
|||||||
import art.arcane.iris.core.pack.PackDirectoryResolver;
|
import art.arcane.iris.core.pack.PackDirectoryResolver;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||||
|
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
||||||
|
import art.arcane.iris.engine.framework.StructureReachability;
|
||||||
|
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
||||||
|
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
|
||||||
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||||
import art.arcane.iris.modded.ModdedEngineBootstrap;
|
import art.arcane.iris.modded.ModdedEngineBootstrap;
|
||||||
import art.arcane.iris.modded.ModdedServerLevels;
|
import art.arcane.iris.modded.ModdedServerLevels;
|
||||||
@@ -41,11 +45,16 @@ import org.slf4j.LoggerFactory;
|
|||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.TreeMap;
|
||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
final class ModdedCommandSuggestions {
|
final class ModdedCommandSuggestions {
|
||||||
static final SuggestionProvider<CommandSourceStack> BIOME_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestBiomeKeys(context, builder);
|
static final SuggestionProvider<CommandSourceStack> BIOME_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestBiomeKeys(context, builder);
|
||||||
@@ -103,22 +112,62 @@ final class ModdedCommandSuggestions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static CompletableFuture<Suggestions> suggestStructureKeys(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) {
|
static CompletableFuture<Suggestions> suggestStructureKeys(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) {
|
||||||
ModdedCommandFeedback.tab(context.getSource());
|
CommandSourceStack source = context.getSource();
|
||||||
|
ModdedCommandFeedback.tab(source);
|
||||||
try {
|
try {
|
||||||
Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel());
|
ServerLevel level = source.getLevel();
|
||||||
Collection<String> irisKeys = engine == null ? List.of() : IrisStructureLocator.placedKeys(engine);
|
Engine engine = IrisModdedCommands.engineFor(level);
|
||||||
Registry<Structure> registry = context.getSource().getServer().registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
if (engine == null) {
|
||||||
List<String> nativeKeys = new ArrayList<>(registry.keySet().size());
|
return builder.buildFuture();
|
||||||
for (Identifier identifier : registry.keySet()) {
|
|
||||||
nativeKeys.add(identifier.toString());
|
|
||||||
}
|
}
|
||||||
return SharedSuggestionProvider.suggest(combineStructureKeys(irisKeys, nativeKeys), builder);
|
boolean nativeGenerationEnabled =
|
||||||
|
source.getServer().getWorldGenSettings().options().generateStructures();
|
||||||
|
Collection<String> irisKeys = IrisStructureLocator.locatableEditableKeys(engine);
|
||||||
|
Set<String> reachableNativeKeys = StructureReachability.reachableKeys(engine);
|
||||||
|
Registry<Structure> registry = source.getServer().registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||||
|
List<String> nativeKeys = new ArrayList<>(registry.keySet().size());
|
||||||
|
Set<String> registeredKeys = new HashSet<>(registry.keySet().size());
|
||||||
|
for (Identifier identifier : registry.keySet()) {
|
||||||
|
String key = identifier.toString();
|
||||||
|
registeredKeys.add(normalizeKey(key));
|
||||||
|
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine, key, false);
|
||||||
|
boolean nativePlacement = IrisStructureLocator.hasNativePlacement(engine, key);
|
||||||
|
boolean locatableNativePlacement = nativePlacement
|
||||||
|
&& IrisStructureLocator.hasLocatableNativePlacement(engine, key);
|
||||||
|
boolean locatableEditableReplacement = !nativePlacement
|
||||||
|
&& decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS
|
||||||
|
&& IrisStructureLocator.hasLocatableEditablePlacement(engine, key);
|
||||||
|
if (isEligibleRegisteredStructure(decision, nativePlacement,
|
||||||
|
locatableNativePlacement, locatableEditableReplacement,
|
||||||
|
reachableNativeKeys.contains(normalizeKey(key)), nativeGenerationEnabled)) {
|
||||||
|
nativeKeys.add(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Collection<String> unregisteredIrisKeys = eligibleUnregisteredEditableKeys(
|
||||||
|
irisKeys, registeredKeys,
|
||||||
|
(String candidate) -> IrisStructureLocator.hasNativePlacement(engine, candidate));
|
||||||
|
return SharedSuggestionProvider.suggest(
|
||||||
|
combineStructureKeys(unregisteredIrisKeys, nativeKeys), builder);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
warnTabFailure("structure keys", context.getSource(), e);
|
warnTabFailure("structure keys", source, e);
|
||||||
}
|
}
|
||||||
return builder.buildFuture();
|
return builder.buildFuture();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static boolean isEligibleRegisteredStructure(IrisNativeStructureDecision decision,
|
||||||
|
boolean nativePlacement,
|
||||||
|
boolean locatableNativePlacement,
|
||||||
|
boolean locatableEditableReplacement,
|
||||||
|
boolean reachable,
|
||||||
|
boolean nativeGenerationEnabled) {
|
||||||
|
if (decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS) {
|
||||||
|
return locatableEditableReplacement
|
||||||
|
|| nativeGenerationEnabled && nativePlacement && locatableNativePlacement;
|
||||||
|
}
|
||||||
|
return nativeGenerationEnabled && decision.generate()
|
||||||
|
&& (reachable || nativePlacement && locatableNativePlacement);
|
||||||
|
}
|
||||||
|
|
||||||
static void warnTabFailure(String suggestion, CommandSourceStack source, Throwable error) {
|
static void warnTabFailure(String suggestion, CommandSourceStack source, Throwable error) {
|
||||||
String origin = tabOrigin(source);
|
String origin = tabOrigin(source);
|
||||||
if (!REPORTED_TAB_FAILURES.add(suggestion + '|' + origin + '|' + error.getClass().getName())) {
|
if (!REPORTED_TAB_FAILURES.add(suggestion + '|' + origin + '|' + error.getClass().getName())) {
|
||||||
@@ -142,10 +191,38 @@ final class ModdedCommandSuggestions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static List<String> combineStructureKeys(Collection<String> irisKeys, Collection<String> nativeKeys) {
|
static List<String> combineStructureKeys(Collection<String> irisKeys, Collection<String> nativeKeys) {
|
||||||
Set<String> combined = new TreeSet<>();
|
Map<String, String> combined = new TreeMap<>();
|
||||||
combined.addAll(irisKeys);
|
addStructureKeys(combined, irisKeys);
|
||||||
combined.addAll(nativeKeys);
|
addStructureKeys(combined, nativeKeys);
|
||||||
return List.copyOf(combined);
|
return List.copyOf(combined.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<String> eligibleUnregisteredEditableKeys(
|
||||||
|
Collection<String> irisKeys, Set<String> normalizedRegisteredKeys,
|
||||||
|
Predicate<String> nativePlacement) {
|
||||||
|
List<String> filtered = new ArrayList<>(irisKeys.size());
|
||||||
|
for (String key : irisKeys) {
|
||||||
|
String normalizedKey = normalizeKey(key);
|
||||||
|
if (!normalizedKey.isEmpty()
|
||||||
|
&& !normalizedRegisteredKeys.contains(normalizedKey)
|
||||||
|
&& !nativePlacement.test(key)) {
|
||||||
|
filtered.add(key.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.copyOf(filtered);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addStructureKeys(Map<String, String> combined, Collection<String> keys) {
|
||||||
|
for (String key : keys) {
|
||||||
|
String normalizedKey = normalizeKey(key);
|
||||||
|
if (!normalizedKey.isEmpty()) {
|
||||||
|
combined.putIfAbsent(normalizedKey, key.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static String normalizeKey(String key) {
|
||||||
|
return key == null ? "" : key.trim().toLowerCase(Locale.ROOT);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CompletableFuture<Suggestions> suggestPackNames(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) {
|
private static CompletableFuture<Suggestions> suggestPackNames(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) {
|
||||||
|
|||||||
+3
@@ -242,6 +242,9 @@ final class ModdedCommandTree {
|
|||||||
.then(Commands.literal("object")
|
.then(Commands.literal("object")
|
||||||
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.OBJECT_KEYS)
|
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.OBJECT_KEYS)
|
||||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedLocateCommands.gotoObject(context.getSource(), StringArgumentType.getString(context, "key")))))
|
.executes((CommandContext<CommandSourceStack> context) -> ModdedLocateCommands.gotoObject(context.getSource(), StringArgumentType.getString(context, "key")))))
|
||||||
|
.then(Commands.literal("unregistered")
|
||||||
|
.executes((CommandContext<CommandSourceStack> context) ->
|
||||||
|
ModdedUnregisteredStructures.print(context.getSource())))
|
||||||
.then(Commands.literal("structure")
|
.then(Commands.literal("structure")
|
||||||
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.STRUCTURE_KEYS)
|
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(ModdedCommandSuggestions.STRUCTURE_KEYS)
|
||||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedLocateCommands.gotoStructure(context.getSource(), StringArgumentType.getString(context, "key")))))
|
.executes((CommandContext<CommandSourceStack> context) -> ModdedLocateCommands.gotoStructure(context.getSource(), StringArgumentType.getString(context, "key")))))
|
||||||
|
|||||||
+82
-26
@@ -26,6 +26,7 @@ import art.arcane.iris.engine.framework.GenerationSessionLease;
|
|||||||
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||||
import art.arcane.iris.engine.framework.Locator;
|
import art.arcane.iris.engine.framework.Locator;
|
||||||
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
||||||
|
import art.arcane.iris.engine.framework.StructureReachability;
|
||||||
import art.arcane.iris.engine.framework.WrongEngineBroException;
|
import art.arcane.iris.engine.framework.WrongEngineBroException;
|
||||||
import art.arcane.iris.engine.object.IrisBiome;
|
import art.arcane.iris.engine.object.IrisBiome;
|
||||||
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
||||||
@@ -157,7 +158,8 @@ final class ModdedLocateCommands {
|
|||||||
}
|
}
|
||||||
Optional<NativeStructureTarget> resolved = resolveNativeStructure(source, level, engine, key);
|
Optional<NativeStructureTarget> resolved = resolveNativeStructure(source, level, engine, key);
|
||||||
if (resolved.isEmpty()) {
|
if (resolved.isEmpty()) {
|
||||||
if (IrisStructureLocator.isPlaced(engine, key)) {
|
if (!IrisStructureLocator.hasNativePlacement(engine, key)
|
||||||
|
&& IrisStructureLocator.hasLocatableEditablePlacement(engine, key)) {
|
||||||
locateIrisStructure(source, level, engine, player, key);
|
locateIrisStructure(source, level, engine, player, key);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -166,31 +168,74 @@ final class ModdedLocateCommands {
|
|||||||
}
|
}
|
||||||
NativeStructureTarget target = resolved.get();
|
NativeStructureTarget target = resolved.get();
|
||||||
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine, target.key(), false);
|
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine, target.key(), false);
|
||||||
if (!decision.generate()
|
boolean nativeGenerationEnabled =
|
||||||
&& decision.status() != NativeStructureGenerationStatus.REPLACED_BY_IRIS) {
|
source.getServer().getWorldGenSettings().options().generateStructures();
|
||||||
IrisModdedCommands.fail(source, NativeStructureGenerationPolicy.generationStatusMessage(
|
boolean nativePlacement = IrisStructureLocator.hasNativePlacement(engine, target.key());
|
||||||
target.key(), decision.status()));
|
boolean locatableNativePlacement = nativePlacement
|
||||||
|
&& IrisStructureLocator.hasLocatableNativePlacement(engine, target.key());
|
||||||
|
boolean locatableEditableReplacement = !nativePlacement
|
||||||
|
&& decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS
|
||||||
|
&& IrisStructureLocator.hasLocatableEditablePlacement(engine, target.key());
|
||||||
|
boolean reachable = StructureReachability.isReachable(engine, target.key());
|
||||||
|
if (!ModdedCommandSuggestions.isEligibleRegisteredStructure(
|
||||||
|
decision, nativePlacement, locatableNativePlacement, locatableEditableReplacement,
|
||||||
|
reachable, nativeGenerationEnabled)) {
|
||||||
|
IrisModdedCommands.fail(source, registeredStructureUnavailableMessage(
|
||||||
|
target.key(), target.availability(), decision,
|
||||||
|
nativePlacement, locatableNativePlacement,
|
||||||
|
nativeGenerationEnabled, reachable));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS) {
|
if (locatableEditableReplacement) {
|
||||||
if (!IrisStructureLocator.hasNativePlacement(engine, target.key())) {
|
locateIrisStructure(source, level, engine, player, target.key());
|
||||||
locateIrisStructure(source, level, engine, player, target.key());
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_SEARCHING_NATIVE_STRUCTURE_WITHIN_CHUNKS, MessageArgument.untrusted("value", target.key()), MessageArgument.untrusted("NATIVESTRUCTURELOCATERADIUS", NATIVE_STRUCTURE_LOCATE_RADIUS)));
|
|
||||||
runNativeStructureLocate(source, level, player, target);
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
if (!IrisStructureLocator.hasNativePlacement(engine, target.key())
|
|
||||||
&& target.availability() != NativeStructureAvailability.AVAILABLE) {
|
|
||||||
IrisModdedCommands.fail(source, nativeUnavailableMessage(target.key(), target.availability()));
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_SEARCHING_NATIVE_STRUCTURE_WITHIN_CHUNKS, MessageArgument.untrusted("value", target.key()), MessageArgument.untrusted("NATIVESTRUCTURELOCATERADIUS", NATIVE_STRUCTURE_LOCATE_RADIUS)));
|
IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_SEARCHING_NATIVE_STRUCTURE_WITHIN_CHUNKS, MessageArgument.untrusted("value", target.key()), MessageArgument.untrusted("NATIVESTRUCTURELOCATERADIUS", NATIVE_STRUCTURE_LOCATE_RADIUS)));
|
||||||
runNativeStructureLocate(source, level, player, target);
|
runNativeStructureLocate(source, level, player, target);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static String registeredStructureUnavailableMessage(
|
||||||
|
String key, NativeStructureAvailability nativeAvailability,
|
||||||
|
IrisNativeStructureDecision decision,
|
||||||
|
boolean nativePlacement, boolean locatableNativePlacement,
|
||||||
|
boolean nativeGenerationEnabled, boolean reachable) {
|
||||||
|
if (!decision.generate()
|
||||||
|
&& decision.status() != NativeStructureGenerationStatus.REPLACED_BY_IRIS) {
|
||||||
|
return NativeStructureGenerationPolicy.generationStatusMessage(key, decision.status());
|
||||||
|
}
|
||||||
|
if (decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS) {
|
||||||
|
if (nativePlacement && !nativeGenerationEnabled) {
|
||||||
|
return withInactiveNativePlacement(
|
||||||
|
nativeUnavailableMessage(key, NativeStructureAvailability.WORLD_DISABLED),
|
||||||
|
nativePlacement, locatableNativePlacement);
|
||||||
|
}
|
||||||
|
return "Iris replacement " + key
|
||||||
|
+ " is configured, but every matching placement has non-positive density or a Y band "
|
||||||
|
+ "outside this world's height range.";
|
||||||
|
}
|
||||||
|
if (!nativeGenerationEnabled) {
|
||||||
|
return withInactiveNativePlacement(
|
||||||
|
nativeUnavailableMessage(key, NativeStructureAvailability.WORLD_DISABLED),
|
||||||
|
nativePlacement, locatableNativePlacement);
|
||||||
|
}
|
||||||
|
NativeStructureAvailability availability = nativeAvailability;
|
||||||
|
if (availability == NativeStructureAvailability.AVAILABLE && !reachable) {
|
||||||
|
availability = NativeStructureAvailability.NO_PLACEMENT;
|
||||||
|
}
|
||||||
|
return withInactiveNativePlacement(
|
||||||
|
nativeUnavailableMessage(key, availability), nativePlacement, locatableNativePlacement);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String withInactiveNativePlacement(
|
||||||
|
String reason, boolean nativePlacement, boolean locatableNativePlacement) {
|
||||||
|
if (!nativePlacement || locatableNativePlacement) {
|
||||||
|
return reason;
|
||||||
|
}
|
||||||
|
return reason + " A matching Iris nativeStructures placement is also configured, but has non-positive "
|
||||||
|
+ "density or a Y band outside this world's height range.";
|
||||||
|
}
|
||||||
|
|
||||||
private static void locateIrisStructure(CommandSourceStack source, ServerLevel level, Engine engine,
|
private static void locateIrisStructure(CommandSourceStack source, ServerLevel level, Engine engine,
|
||||||
ServerPlayer player, String key) {
|
ServerPlayer player, String key) {
|
||||||
MinecraftServer server = source.getServer();
|
MinecraftServer server = source.getServer();
|
||||||
@@ -311,7 +356,7 @@ final class ModdedLocateCommands {
|
|||||||
case AVAILABLE -> available++;
|
case AVAILABLE -> available++;
|
||||||
case WORLD_DISABLED, FILTERED -> disabled++;
|
case WORLD_DISABLED, FILTERED -> disabled++;
|
||||||
case IRIS_SUPPRESSED -> suppressed++;
|
case IRIS_SUPPRESSED -> suppressed++;
|
||||||
case BIOME_UNREACHABLE -> unreachableBiomes++;
|
case EMPTY_BIOME_FILTER, BIOME_UNREACHABLE -> unreachableBiomes++;
|
||||||
case NO_PLACEMENT -> unsupported++;
|
case NO_PLACEMENT -> unsupported++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -361,25 +406,28 @@ final class ModdedLocateCommands {
|
|||||||
return Optional.of(new NativeStructureTarget(key, holder.get(), availability));
|
return Optional.of(new NativeStructureTarget(key, holder.get(), availability));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static NativeStructureAvailability nativeAvailability(CommandSourceStack source, ServerLevel level,
|
static NativeStructureAvailability nativeAvailability(CommandSourceStack source, ServerLevel level,
|
||||||
Engine engine, String key,
|
Engine engine, String key,
|
||||||
Holder.Reference<Structure> holder) {
|
Holder.Reference<Structure> holder) {
|
||||||
boolean worldEnabled = source.getServer().getWorldGenSettings().options().generateStructures();
|
boolean worldEnabled = source.getServer().getWorldGenSettings().options().generateStructures();
|
||||||
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine, key, false);
|
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine, key, false);
|
||||||
boolean selected = decision.status() != NativeStructureGenerationStatus.DISABLED_BY_PACK;
|
boolean selected = decision.status() != NativeStructureGenerationStatus.DISABLED_BY_PACK;
|
||||||
boolean suppressed = decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS;
|
boolean suppressed = decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS;
|
||||||
|
boolean biomeFilterEmpty = holder.value().biomes().stream().findAny().isEmpty();
|
||||||
ChunkGenerator chunkGenerator = level.getChunkSource().getGenerator();
|
ChunkGenerator chunkGenerator = level.getChunkSource().getGenerator();
|
||||||
boolean biomeReachable = chunkGenerator instanceof IrisModdedChunkGenerator irisGenerator
|
boolean biomeReachable = chunkGenerator instanceof IrisModdedChunkGenerator irisGenerator
|
||||||
&& irisGenerator.isNativeStructureReachable(holder);
|
&& irisGenerator.isNativeStructureReachable(holder);
|
||||||
boolean hasPlacement = false;
|
boolean hasPlacement = false;
|
||||||
if (worldEnabled && selected && !suppressed && biomeReachable) {
|
if (worldEnabled && selected && !suppressed && !biomeFilterEmpty && biomeReachable) {
|
||||||
hasPlacement = !level.getChunkSource().getGeneratorState().getPlacementsForStructure(holder).isEmpty();
|
hasPlacement = !level.getChunkSource().getGeneratorState().getPlacementsForStructure(holder).isEmpty();
|
||||||
}
|
}
|
||||||
return classifyNativeAvailability(worldEnabled, selected, suppressed, biomeReachable, hasPlacement);
|
return classifyNativeAvailability(
|
||||||
|
worldEnabled, selected, suppressed, biomeFilterEmpty, biomeReachable, hasPlacement);
|
||||||
}
|
}
|
||||||
|
|
||||||
static NativeStructureAvailability classifyNativeAvailability(boolean worldEnabled, boolean selected,
|
static NativeStructureAvailability classifyNativeAvailability(boolean worldEnabled, boolean selected,
|
||||||
boolean suppressed, boolean biomeReachable,
|
boolean suppressed, boolean biomeFilterEmpty,
|
||||||
|
boolean biomeReachable,
|
||||||
boolean hasPlacement) {
|
boolean hasPlacement) {
|
||||||
if (!worldEnabled) {
|
if (!worldEnabled) {
|
||||||
return NativeStructureAvailability.WORLD_DISABLED;
|
return NativeStructureAvailability.WORLD_DISABLED;
|
||||||
@@ -390,6 +438,9 @@ final class ModdedLocateCommands {
|
|||||||
if (suppressed) {
|
if (suppressed) {
|
||||||
return NativeStructureAvailability.IRIS_SUPPRESSED;
|
return NativeStructureAvailability.IRIS_SUPPRESSED;
|
||||||
}
|
}
|
||||||
|
if (biomeFilterEmpty) {
|
||||||
|
return NativeStructureAvailability.EMPTY_BIOME_FILTER;
|
||||||
|
}
|
||||||
if (!biomeReachable) {
|
if (!biomeReachable) {
|
||||||
return NativeStructureAvailability.BIOME_UNREACHABLE;
|
return NativeStructureAvailability.BIOME_UNREACHABLE;
|
||||||
}
|
}
|
||||||
@@ -399,15 +450,19 @@ final class ModdedLocateCommands {
|
|||||||
return NativeStructureAvailability.AVAILABLE;
|
return NativeStructureAvailability.AVAILABLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String nativeUnavailableMessage(String key, NativeStructureAvailability availability) {
|
static String nativeUnavailableMessage(String key, NativeStructureAvailability availability) {
|
||||||
return switch (availability) {
|
return switch (availability) {
|
||||||
case WORLD_DISABLED -> "Native structure generation is disabled for this world, so " + key + " cannot generate or be located.";
|
case WORLD_DISABLED -> "Native structure generation is disabled for this world, so " + key + " cannot generate or be located.";
|
||||||
case FILTERED -> NativeStructureGenerationPolicy.generationStatusMessage(
|
case FILTERED -> NativeStructureGenerationPolicy.generationStatusMessage(
|
||||||
key, NativeStructureGenerationStatus.DISABLED_BY_PACK);
|
key, NativeStructureGenerationStatus.DISABLED_BY_PACK);
|
||||||
case IRIS_SUPPRESSED -> NativeStructureGenerationPolicy.generationStatusMessage(
|
case IRIS_SUPPRESSED -> NativeStructureGenerationPolicy.generationStatusMessage(
|
||||||
key, NativeStructureGenerationStatus.REPLACED_BY_IRIS);
|
key, NativeStructureGenerationStatus.REPLACED_BY_IRIS);
|
||||||
|
case EMPTY_BIOME_FILTER -> "Native structure " + key
|
||||||
|
+ " has a biome tag or filter that resolves to zero registered biomes.";
|
||||||
case BIOME_UNREACHABLE -> "Native structure " + key + " cannot generate because none of its required biomes are produced by this Iris pack.";
|
case BIOME_UNREACHABLE -> "Native structure " + key + " cannot generate because none of its required biomes are produced by this Iris pack.";
|
||||||
case NO_PLACEMENT -> "Native structure " + key + " is registered, but its structure set has no placement supported by this dimension's generator state.";
|
case NO_PLACEMENT -> "Native structure " + key
|
||||||
|
+ " is registered and biome-compatible, but has no active positive-weight, "
|
||||||
|
+ "positive-frequency structure-set placement in this dimension's generator state.";
|
||||||
case AVAILABLE -> "Native structure " + key + " is available.";
|
case AVAILABLE -> "Native structure " + key + " is available.";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -531,6 +586,7 @@ final class ModdedLocateCommands {
|
|||||||
WORLD_DISABLED,
|
WORLD_DISABLED,
|
||||||
FILTERED,
|
FILTERED,
|
||||||
IRIS_SUPPRESSED,
|
IRIS_SUPPRESSED,
|
||||||
|
EMPTY_BIOME_FILTER,
|
||||||
BIOME_UNREACHABLE,
|
BIOME_UNREACHABLE,
|
||||||
NO_PLACEMENT
|
NO_PLACEMENT
|
||||||
}
|
}
|
||||||
|
|||||||
+204
@@ -0,0 +1,204 @@
|
|||||||
|
/*
|
||||||
|
* Iris is a World Generator for Minecraft Servers
|
||||||
|
* Copyright (c) 2026 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.modded.command;
|
||||||
|
|
||||||
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
|
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||||
|
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
||||||
|
import art.arcane.iris.engine.framework.StructureReachability;
|
||||||
|
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
||||||
|
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
|
||||||
|
import net.minecraft.commands.CommandSourceStack;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.core.Registry;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
final class ModdedUnregisteredStructures {
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||||
|
|
||||||
|
private ModdedUnregisteredStructures() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static int print(CommandSourceStack source) {
|
||||||
|
ServerLevel level = source.getLevel();
|
||||||
|
Engine engine = IrisModdedCommands.engineFor(level);
|
||||||
|
if (engine == null) {
|
||||||
|
IrisModdedCommands.fail(source,
|
||||||
|
"This dimension is not generated by Iris, so it has no Iris structure eligibility report.");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
List<ExcludedStructure> excluded = collect(source, level, engine);
|
||||||
|
String dimension = level.dimension().identifier().toString();
|
||||||
|
long unregistered = excluded.stream()
|
||||||
|
.filter((ExcludedStructure entry) -> entry.status() == ReportStatus.UNREGISTERED)
|
||||||
|
.count();
|
||||||
|
long unplaced = excluded.stream()
|
||||||
|
.filter((ExcludedStructure entry) -> entry.status() == ReportStatus.UNPLACED)
|
||||||
|
.count();
|
||||||
|
long hidden = excluded.size() - unregistered - unplaced;
|
||||||
|
LOGGER.info("[Iris goto unregistered] {} structure candidate(s) excluded from /iris goto structure in {}",
|
||||||
|
excluded.size(), dimension);
|
||||||
|
for (ExcludedStructure entry : excluded) {
|
||||||
|
LOGGER.info("[Iris goto unregistered] [{}] {} - {}",
|
||||||
|
entry.status().label(), entry.key(), entry.reason());
|
||||||
|
}
|
||||||
|
LOGGER.info("[Iris goto unregistered] Inventory scope is the live registry, this pack's "
|
||||||
|
+ "nativeStructures placements, and structureLoader editable resources. This is deterministic "
|
||||||
|
+ "eligibility analysis and performs no chunk search; absent unmanaged datapack resources "
|
||||||
|
+ "cannot be inferred after registry loading.");
|
||||||
|
IrisModdedCommands.ok(source, "Iris wrote " + excluded.size() + " structure candidate(s) for "
|
||||||
|
+ dimension + " to the server console (" + hidden + " excluded, "
|
||||||
|
+ unregistered + " unregistered, " + unplaced + " unplaced).");
|
||||||
|
return 1;
|
||||||
|
} catch (Throwable error) {
|
||||||
|
LOGGER.error("Iris failed to build the excluded structure report for {}",
|
||||||
|
level.dimension().identifier(), error);
|
||||||
|
IrisModdedCommands.fail(source,
|
||||||
|
"Iris could not build the excluded structure report; see the server console.");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<ExcludedStructure> collect(CommandSourceStack source, ServerLevel level, Engine engine) {
|
||||||
|
boolean nativeGenerationEnabled =
|
||||||
|
source.getServer().getWorldGenSettings().options().generateStructures();
|
||||||
|
Set<String> reachableNativeKeys = StructureReachability.reachableKeys(engine);
|
||||||
|
Registry<Structure> registry = source.getServer().registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||||
|
Map<String, ExcludedStructure> excluded = new TreeMap<>();
|
||||||
|
Set<String> registeredKeys = new HashSet<>(registry.keySet().size());
|
||||||
|
Set<String> eligibleRegisteredKeys = new HashSet<>(registry.keySet().size());
|
||||||
|
for (Identifier identifier : registry.keySet()) {
|
||||||
|
String key = identifier.toString();
|
||||||
|
String normalizedKey = ModdedCommandSuggestions.normalizeKey(key);
|
||||||
|
registeredKeys.add(normalizedKey);
|
||||||
|
Optional<Holder.Reference<Structure>> holder = registry.get(identifier);
|
||||||
|
if (holder.isEmpty()) {
|
||||||
|
excluded.put(normalizedKey, new ExcludedStructure(ReportStatus.EXCLUDED,
|
||||||
|
key, "The live structure registry contains this key but has no bound structure holder."));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine, key, false);
|
||||||
|
boolean nativePlacement = IrisStructureLocator.hasNativePlacement(engine, key);
|
||||||
|
boolean locatableNativePlacement = nativePlacement
|
||||||
|
&& IrisStructureLocator.hasLocatableNativePlacement(engine, key);
|
||||||
|
boolean locatableEditableReplacement = !nativePlacement
|
||||||
|
&& decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS
|
||||||
|
&& IrisStructureLocator.hasLocatableEditablePlacement(engine, key);
|
||||||
|
boolean reachable = reachableNativeKeys.contains(normalizedKey);
|
||||||
|
if (ModdedCommandSuggestions.isEligibleRegisteredStructure(
|
||||||
|
decision, nativePlacement, locatableNativePlacement,
|
||||||
|
locatableEditableReplacement, reachable, nativeGenerationEnabled)) {
|
||||||
|
eligibleRegisteredKeys.add(normalizedKey);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ModdedLocateCommands.NativeStructureAvailability availability =
|
||||||
|
ModdedLocateCommands.nativeAvailability(source, level, engine, key, holder.get());
|
||||||
|
String reason = ModdedLocateCommands.registeredStructureUnavailableMessage(
|
||||||
|
key, availability, decision, nativePlacement, locatableNativePlacement,
|
||||||
|
nativeGenerationEnabled, reachable);
|
||||||
|
excluded.put(normalizedKey, new ExcludedStructure(ReportStatus.EXCLUDED, key, reason));
|
||||||
|
}
|
||||||
|
List<String> absentNativeKeys = missingConfiguredNativeKeys(
|
||||||
|
IrisStructureLocator.placedKeys(engine), registeredKeys,
|
||||||
|
(String key) -> IrisStructureLocator.hasNativePlacement(engine, key));
|
||||||
|
for (String key : absentNativeKeys) {
|
||||||
|
String normalizedKey = ModdedCommandSuggestions.normalizeKey(key);
|
||||||
|
excluded.putIfAbsent(normalizedKey, new ExcludedStructure(ReportStatus.UNREGISTERED, key,
|
||||||
|
"Configured by an Iris nativeStructures placement, but absent from the live structure registry; "
|
||||||
|
+ "install or enable the datapack or mod that provides this key."));
|
||||||
|
}
|
||||||
|
if (engine.getData().getStructureLoader() != null) {
|
||||||
|
for (String key : engine.getData().getStructureLoader().getPossibleKeys()) {
|
||||||
|
String normalizedKey = ModdedCommandSuggestions.normalizeKey(key);
|
||||||
|
if (normalizedKey.isEmpty() || eligibleRegisteredKeys.contains(normalizedKey)
|
||||||
|
|| excluded.containsKey(normalizedKey)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
boolean registered = registeredKeys.contains(normalizedKey);
|
||||||
|
boolean nativePlacement = IrisStructureLocator.hasNativePlacement(engine, key);
|
||||||
|
boolean locatableEditable = IrisStructureLocator.hasLocatableEditablePlacement(engine, key);
|
||||||
|
if (!registered && !nativePlacement && locatableEditable) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
boolean configured = IrisStructureLocator.hasEditablePlacement(engine, key);
|
||||||
|
excluded.putIfAbsent(normalizedKey, new ExcludedStructure(
|
||||||
|
ReportStatus.UNPLACED, key.trim(), editableExclusionReason(configured)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.copyOf(excluded.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<String> missingConfiguredNativeKeys(
|
||||||
|
Collection<String> placedKeys, Set<String> normalizedRegisteredKeys,
|
||||||
|
Predicate<String> nativePlacement) {
|
||||||
|
Map<String, String> missing = new TreeMap<>();
|
||||||
|
for (String key : placedKeys) {
|
||||||
|
String normalizedKey = ModdedCommandSuggestions.normalizeKey(key);
|
||||||
|
if (!normalizedKey.isEmpty()
|
||||||
|
&& !normalizedRegisteredKeys.contains(normalizedKey)
|
||||||
|
&& nativePlacement.test(key)) {
|
||||||
|
missing.putIfAbsent(normalizedKey, key.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.copyOf(missing.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
static String editableExclusionReason(boolean configured) {
|
||||||
|
if (!configured) {
|
||||||
|
return "Editable Iris structure resource is loaded, but no biome, region, or dimension structure "
|
||||||
|
+ "placement references it.";
|
||||||
|
}
|
||||||
|
return "Editable Iris structure resource is referenced by a placement, but every matching placement "
|
||||||
|
+ "has non-positive density or a Y band outside this world's height range.";
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ReportStatus {
|
||||||
|
EXCLUDED("excluded"),
|
||||||
|
UNREGISTERED("unregistered"),
|
||||||
|
UNPLACED("unplaced");
|
||||||
|
|
||||||
|
private final String label;
|
||||||
|
|
||||||
|
ReportStatus(String label) {
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
String label() {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record ExcludedStructure(ReportStatus status, String key, String reason) {
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
@@ -39,6 +39,7 @@ public class IrisModdedCommandParityTest {
|
|||||||
child(iris, "height");
|
child(iris, "height");
|
||||||
child(iris, "worlds");
|
child(iris, "worlds");
|
||||||
child(iris, "accesslist");
|
child(iris, "accesslist");
|
||||||
|
child(child(iris, "goto"), "unregistered");
|
||||||
|
|
||||||
CommandNode<CommandSourceStack> edit = child(iris, "edit");
|
CommandNode<CommandSourceStack> edit = child(iris, "edit");
|
||||||
child(edit, "b");
|
child(edit, "b");
|
||||||
@@ -75,6 +76,7 @@ public class IrisModdedCommandParityTest {
|
|||||||
assertTrue(ModdedCommandHelp.documents("studio", "pkg"));
|
assertTrue(ModdedCommandHelp.documents("studio", "pkg"));
|
||||||
assertTrue(ModdedCommandHelp.documents("object", "we"));
|
assertTrue(ModdedCommandHelp.documents("object", "we"));
|
||||||
assertTrue(ModdedCommandHelp.documents("world", "mainworld"));
|
assertTrue(ModdedCommandHelp.documents("world", "mainworld"));
|
||||||
|
assertTrue(ModdedCommandHelp.documents("goto", "unregistered"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
+173
-14
@@ -1,5 +1,7 @@
|
|||||||
package art.arcane.iris.modded.command;
|
package art.arcane.iris.modded.command;
|
||||||
|
|
||||||
|
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
||||||
|
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
|
||||||
import art.arcane.iris.nativegen.NativeStructureLocateResults;
|
import art.arcane.iris.nativegen.NativeStructureLocateResults;
|
||||||
import com.mojang.datafixers.util.Pair;
|
import com.mojang.datafixers.util.Pair;
|
||||||
import net.minecraft.core.BlockPos;
|
import net.minecraft.core.BlockPos;
|
||||||
@@ -8,6 +10,8 @@ import org.junit.Test;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
@@ -49,7 +53,7 @@ public class IrisModdedStructureCommandTest {
|
|||||||
String source = source("ModdedLocateCommands.java");
|
String source = source("ModdedLocateCommands.java");
|
||||||
String suggestions = source("ModdedCommandSuggestions.java");
|
String suggestions = source("ModdedCommandSuggestions.java");
|
||||||
|
|
||||||
assertTrue(source.contains("IrisStructureLocator.isPlaced(engine, key)"));
|
assertTrue(source.contains("IrisStructureLocator.hasLocatableEditablePlacement(engine, key)"));
|
||||||
assertTrue(source.contains("registry.get(identifier)"));
|
assertTrue(source.contains("registry.get(identifier)"));
|
||||||
assertTrue(source.contains("getPlacementsForStructure(holder)"));
|
assertTrue(source.contains("getPlacementsForStructure(holder)"));
|
||||||
assertTrue(source.contains("generator.findNearestMapStructure("));
|
assertTrue(source.contains("generator.findNearestMapStructure("));
|
||||||
@@ -57,7 +61,7 @@ public class IrisModdedStructureCommandTest {
|
|||||||
assertTrue(source.contains("HolderSet.direct(target.holder())"));
|
assertTrue(source.contains("HolderSet.direct(target.holder())"));
|
||||||
assertFalse(source.contains("NativeStructureLocateCapability"));
|
assertFalse(source.contains("NativeStructureLocateCapability"));
|
||||||
assertTrue(source.contains("boolean teleported = player.teleportTo("));
|
assertTrue(source.contains("boolean teleported = player.teleportTo("));
|
||||||
assertTrue(suggestions.contains("combineStructureKeys(irisKeys, nativeKeys)"));
|
assertTrue(suggestions.contains("combineStructureKeys(unregisteredIrisKeys, nativeKeys)"));
|
||||||
assertTrue(source.contains("irisGenerator.isNativeStructureReachable(holder)"));
|
assertTrue(source.contains("irisGenerator.isNativeStructureReachable(holder)"));
|
||||||
assertTrue(source.contains("LocateStatus.SEARCH_LIMIT_REACHED"));
|
assertTrue(source.contains("LocateStatus.SEARCH_LIMIT_REACHED"));
|
||||||
assertTrue(source.contains("IRIS_MODDED_COMMANDS_UNABLE_LOCATE_IRIS_PLACED_STRUCTURE_DENSITY_SEARCH_SAFETY_LIMIT_WAS"));
|
assertTrue(source.contains("IRIS_MODDED_COMMANDS_UNABLE_LOCATE_IRIS_PLACED_STRUCTURE_DENSITY_SEARCH_SAFETY_LIMIT_WAS"));
|
||||||
@@ -68,6 +72,148 @@ public class IrisModdedStructureCommandTest {
|
|||||||
assertFalse(source.contains("at[2] + 8"));
|
assertFalse(source.contains("at[2] + 8"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void registeredStructureEligibilityMatchesGotoAndSuggestions() {
|
||||||
|
IrisNativeStructureDecision nativeDecision = decision(NativeStructureGenerationStatus.GENERATE_NATIVE);
|
||||||
|
IrisNativeStructureDecision replacementDecision = decision(
|
||||||
|
NativeStructureGenerationStatus.REPLACED_BY_IRIS);
|
||||||
|
IrisNativeStructureDecision disabledDecision = decision(
|
||||||
|
NativeStructureGenerationStatus.DISABLED_BY_PACK);
|
||||||
|
|
||||||
|
assertTrue(ModdedCommandSuggestions.isEligibleRegisteredStructure(
|
||||||
|
nativeDecision, false, false, false, true, true));
|
||||||
|
assertTrue(ModdedCommandSuggestions.isEligibleRegisteredStructure(
|
||||||
|
nativeDecision, true, true, false, false, true));
|
||||||
|
assertFalse(ModdedCommandSuggestions.isEligibleRegisteredStructure(
|
||||||
|
nativeDecision, true, false, false, false, true));
|
||||||
|
assertFalse(ModdedCommandSuggestions.isEligibleRegisteredStructure(
|
||||||
|
nativeDecision, true, true, false, true, false));
|
||||||
|
assertTrue(ModdedCommandSuggestions.isEligibleRegisteredStructure(
|
||||||
|
replacementDecision, false, false, true, false, false));
|
||||||
|
assertTrue(ModdedCommandSuggestions.isEligibleRegisteredStructure(
|
||||||
|
replacementDecision, true, true, false, false, true));
|
||||||
|
assertFalse(ModdedCommandSuggestions.isEligibleRegisteredStructure(
|
||||||
|
replacementDecision, true, true, false, false, false));
|
||||||
|
assertFalse(ModdedCommandSuggestions.isEligibleRegisteredStructure(
|
||||||
|
replacementDecision, false, false, false, true, true));
|
||||||
|
assertFalse(ModdedCommandSuggestions.isEligibleRegisteredStructure(
|
||||||
|
disabledDecision, true, true, true, true, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void structureSuggestionsDedupeAndSortPlacedAndNativeKeys() {
|
||||||
|
List<String> suggestions = ModdedCommandSuggestions.combineStructureKeys(
|
||||||
|
List.of("towns_and_towers:village_forest", "minecraft:village_plains",
|
||||||
|
"towns_and_towers:village_forest", "MINECRAFT:VILLAGE_PLAINS"),
|
||||||
|
List.of("minecraft:stronghold", "minecraft:village_plains"));
|
||||||
|
|
||||||
|
assertEquals(List.of("minecraft:stronghold", "minecraft:village_plains",
|
||||||
|
"towns_and_towers:village_forest"), suggestions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void structureSuggestionsCannotReintroduceRegistryOrNativePlacementCollisions() {
|
||||||
|
List<String> suggestions = ModdedCommandSuggestions.eligibleUnregisteredEditableKeys(
|
||||||
|
List.of("iris:custom", "minecraft:disabled", "MINECRAFT:UNREACHABLE",
|
||||||
|
"iris:native_collision"),
|
||||||
|
Set.of("minecraft:disabled", "minecraft:unreachable"),
|
||||||
|
(String key) -> key.equalsIgnoreCase("iris:native_collision"));
|
||||||
|
|
||||||
|
assertEquals(List.of("iris:custom"), suggestions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unregisteredReportFindsOnlyConfiguredNativeKeysAbsentFromRegistry() {
|
||||||
|
List<String> missing = ModdedUnregisteredStructures.missingConfiguredNativeKeys(
|
||||||
|
List.of("iris:editable", "missing:native", "MISSING:NATIVE", "registered:native"),
|
||||||
|
Set.of("registered:native"),
|
||||||
|
(String key) -> key.equalsIgnoreCase("missing:native")
|
||||||
|
|| key.equalsIgnoreCase("registered:native"));
|
||||||
|
|
||||||
|
assertEquals(List.of("missing:native"), missing);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unregisteredReportDistinguishesEveryNativeExclusionReason() {
|
||||||
|
assertEquals(ModdedLocateCommands.NativeStructureAvailability.WORLD_DISABLED,
|
||||||
|
ModdedLocateCommands.classifyNativeAvailability(false, true, false, false, true, true));
|
||||||
|
assertEquals(ModdedLocateCommands.NativeStructureAvailability.FILTERED,
|
||||||
|
ModdedLocateCommands.classifyNativeAvailability(true, false, false, false, true, true));
|
||||||
|
assertEquals(ModdedLocateCommands.NativeStructureAvailability.IRIS_SUPPRESSED,
|
||||||
|
ModdedLocateCommands.classifyNativeAvailability(true, true, true, false, true, true));
|
||||||
|
assertEquals(ModdedLocateCommands.NativeStructureAvailability.EMPTY_BIOME_FILTER,
|
||||||
|
ModdedLocateCommands.classifyNativeAvailability(true, true, false, true, false, false));
|
||||||
|
assertEquals(ModdedLocateCommands.NativeStructureAvailability.BIOME_UNREACHABLE,
|
||||||
|
ModdedLocateCommands.classifyNativeAvailability(true, true, false, false, false, false));
|
||||||
|
assertEquals(ModdedLocateCommands.NativeStructureAvailability.NO_PLACEMENT,
|
||||||
|
ModdedLocateCommands.classifyNativeAvailability(true, true, false, false, true, false));
|
||||||
|
assertEquals(ModdedLocateCommands.NativeStructureAvailability.AVAILABLE,
|
||||||
|
ModdedLocateCommands.classifyNativeAvailability(true, true, false, false, true, true));
|
||||||
|
|
||||||
|
assertTrue(ModdedLocateCommands.nativeUnavailableMessage(
|
||||||
|
"towns_and_towers:exclusive",
|
||||||
|
ModdedLocateCommands.NativeStructureAvailability.EMPTY_BIOME_FILTER)
|
||||||
|
.contains("resolves to zero registered biomes"));
|
||||||
|
assertTrue(ModdedLocateCommands.nativeUnavailableMessage(
|
||||||
|
"minecraft:village_plains",
|
||||||
|
ModdedLocateCommands.NativeStructureAvailability.NO_PLACEMENT)
|
||||||
|
.contains("no active positive-weight, positive-frequency structure-set placement"));
|
||||||
|
String combined = ModdedLocateCommands.registeredStructureUnavailableMessage(
|
||||||
|
"towns_and_towers:exclusive",
|
||||||
|
ModdedLocateCommands.NativeStructureAvailability.EMPTY_BIOME_FILTER,
|
||||||
|
decision(NativeStructureGenerationStatus.GENERATE_NATIVE),
|
||||||
|
true, false, true, false);
|
||||||
|
assertTrue(combined.contains("resolves to zero registered biomes"));
|
||||||
|
assertTrue(combined.contains("matching Iris nativeStructures placement is also configured"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unregisteredReportExplainsEditablePlacementState() {
|
||||||
|
assertTrue(ModdedUnregisteredStructures.editableExclusionReason(false)
|
||||||
|
.contains("no biome, region, or dimension structure placement"));
|
||||||
|
assertTrue(ModdedUnregisteredStructures.editableExclusionReason(true)
|
||||||
|
.contains("non-positive density or a Y band outside"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unregisteredCommandUsesGotoEligibilityAndPrintsToConsole() throws IOException {
|
||||||
|
String command = source("ModdedUnregisteredStructures.java");
|
||||||
|
String tree = source("ModdedCommandTree.java");
|
||||||
|
|
||||||
|
assertTrue(tree.contains("Commands.literal(\"unregistered\")"));
|
||||||
|
assertTrue(tree.contains("ModdedUnregisteredStructures.print(context.getSource())"));
|
||||||
|
assertTrue(command.contains("ModdedCommandSuggestions.isEligibleRegisteredStructure("));
|
||||||
|
assertTrue(command.contains("ModdedLocateCommands.registeredStructureUnavailableMessage("));
|
||||||
|
assertTrue(command.contains("engine.getData().getStructureLoader().getPossibleKeys()"));
|
||||||
|
assertTrue(command.contains("IrisStructureLocator.hasLocatableEditablePlacement(engine, key)"));
|
||||||
|
assertTrue(command.contains("LOGGER.info(\"[Iris goto unregistered] [{}] {} - {}\""));
|
||||||
|
assertTrue(command.contains("UNREGISTERED(\"unregistered\")"));
|
||||||
|
assertFalse(command.contains("DatapackIngestService"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void structureSuggestionsUseActiveLevelAvailability() throws IOException {
|
||||||
|
String suggestions = source("ModdedCommandSuggestions.java");
|
||||||
|
int methodStart = suggestions.indexOf("static CompletableFuture<Suggestions> suggestStructureKeys(");
|
||||||
|
int methodEnd = suggestions.indexOf("static boolean isEligibleRegisteredStructure(", methodStart);
|
||||||
|
String method = suggestions.substring(methodStart, methodEnd);
|
||||||
|
|
||||||
|
assertTrue(method.contains("ServerLevel level = source.getLevel()"));
|
||||||
|
assertTrue(method.contains("Engine engine = IrisModdedCommands.engineFor(level)"));
|
||||||
|
assertFalse(method.contains("IrisStructureLocator.locatableKeys(engine)"));
|
||||||
|
assertTrue(method.contains("IrisStructureLocator.locatableEditableKeys(engine)"));
|
||||||
|
assertTrue(method.contains("StructureReachability.reachableKeys(engine)"));
|
||||||
|
assertTrue(method.contains("NativeStructureGenerationPolicy.resolve(engine, key, false)"));
|
||||||
|
assertTrue(method.contains("IrisStructureLocator.hasLocatableNativePlacement(engine, key)"));
|
||||||
|
assertTrue(method.contains("IrisStructureLocator.hasLocatableEditablePlacement(engine, key)"));
|
||||||
|
assertTrue(method.contains("reachableNativeKeys.contains(normalizeKey(key))"));
|
||||||
|
assertTrue(method.contains("nativeGenerationEnabled"));
|
||||||
|
assertTrue(method.contains("eligibleUnregisteredEditableKeys("));
|
||||||
|
assertTrue(method.contains("IrisStructureLocator.hasNativePlacement(engine, candidate)"));
|
||||||
|
assertFalse(method.contains("NativeStructureGenerationKeys.active(level)"));
|
||||||
|
assertFalse(method.contains("getPlacementsForStructure"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void generatorLocateUsesEveryIrisPlacedNativeStructure() throws IOException {
|
public void generatorLocateUsesEveryIrisPlacedNativeStructure() throws IOException {
|
||||||
String source = moddedSource("ModdedNativeStructureStage.java");
|
String source = moddedSource("ModdedNativeStructureStage.java");
|
||||||
@@ -111,20 +257,29 @@ public class IrisModdedStructureCommandTest {
|
|||||||
int methodEnd = source.indexOf("private static void locateIrisStructure(", methodStart);
|
int methodEnd = source.indexOf("private static void locateIrisStructure(", methodStart);
|
||||||
String method = source.substring(methodStart, methodEnd);
|
String method = source.substring(methodStart, methodEnd);
|
||||||
int nativeResolution = method.indexOf("resolveNativeStructure(source, level, engine, key)");
|
int nativeResolution = method.indexOf("resolveNativeStructure(source, level, engine, key)");
|
||||||
int genericIrisLookup = method.indexOf("IrisStructureLocator.isPlaced(engine, key)", nativeResolution);
|
int editableIrisLookup = method.indexOf(
|
||||||
int policyResolution = method.indexOf("NativeStructureGenerationPolicy.resolve(engine, target.key(), false)", genericIrisLookup);
|
"IrisStructureLocator.hasLocatableEditablePlacement(engine, key)", nativeResolution);
|
||||||
int replacementCheck = method.indexOf(
|
int policyResolution = method.indexOf(
|
||||||
"decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS", policyResolution);
|
"NativeStructureGenerationPolicy.resolve(engine, target.key(), false)", editableIrisLookup);
|
||||||
int replacementLocate = method.indexOf("runNativeStructureLocate(source, level, player, target)",
|
int eligibilityCheck = method.indexOf(
|
||||||
replacementCheck);
|
"ModdedCommandSuggestions.isEligibleRegisteredStructure(", policyResolution);
|
||||||
|
int replacementLocate = method.indexOf(
|
||||||
|
"locateIrisStructure(source, level, engine, player, target.key())", eligibilityCheck);
|
||||||
|
int nativeLocate = method.indexOf("runNativeStructureLocate(source, level, player, target)",
|
||||||
|
replacementLocate);
|
||||||
|
|
||||||
assertTrue(nativeResolution >= 0);
|
assertTrue(nativeResolution >= 0);
|
||||||
assertTrue(genericIrisLookup > nativeResolution);
|
assertTrue(editableIrisLookup > nativeResolution);
|
||||||
assertTrue(policyResolution > genericIrisLookup);
|
assertTrue(policyResolution > editableIrisLookup);
|
||||||
assertTrue(replacementCheck > policyResolution);
|
assertTrue(eligibilityCheck > policyResolution);
|
||||||
assertTrue(replacementLocate > replacementCheck);
|
assertTrue(replacementLocate > eligibilityCheck);
|
||||||
assertTrue(method.contains("!IrisStructureLocator.hasNativePlacement(engine, target.key())"));
|
assertTrue(nativeLocate > replacementLocate);
|
||||||
assertTrue(method.contains("&& target.availability() != NativeStructureAvailability.AVAILABLE"));
|
assertTrue(method.contains("IrisStructureLocator.hasLocatableNativePlacement(engine, target.key())"));
|
||||||
|
assertTrue(method.contains("IrisStructureLocator.hasLocatableEditablePlacement(engine, target.key())"));
|
||||||
|
assertTrue(method.contains("!IrisStructureLocator.hasNativePlacement(engine, key)"));
|
||||||
|
assertTrue(method.contains("StructureReachability.isReachable(engine, target.key())"));
|
||||||
|
assertTrue(method.contains("getWorldGenSettings().options().generateStructures()"));
|
||||||
|
assertFalse(method.contains("IrisStructureLocator.isPlaced(engine, key)"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -175,4 +330,8 @@ public class IrisModdedStructureCommandTest {
|
|||||||
.resolve("../modded-common/src/main/java")
|
.resolve("../modded-common/src/main/java")
|
||||||
.normalize();
|
.normalize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private IrisNativeStructureDecision decision(NativeStructureGenerationStatus status) {
|
||||||
|
return new IrisNativeStructureDecision(status, 0, null, false, false, null, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,6 +122,8 @@ nmsBindings.each { key, value ->
|
|||||||
rootProject.file('adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructurePostProcessor.java').absolutePath)
|
rootProject.file('adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructurePostProcessor.java').absolutePath)
|
||||||
systemProperty('iris.nativeStructureStartInjectorSource',
|
systemProperty('iris.nativeStructureStartInjectorSource',
|
||||||
rootProject.file('adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureStartInjector.java').absolutePath)
|
rootProject.file('adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureStartInjector.java').absolutePath)
|
||||||
|
systemProperty('iris.nativeStructureGenerationKeysSource',
|
||||||
|
rootProject.file('adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureGenerationKeys.java').absolutePath)
|
||||||
systemProperty('iris.customBiomeSource',
|
systemProperty('iris.customBiomeSource',
|
||||||
rootProject.file("adapters/bukkit/nms/${key}/src/main/java/art/arcane/iris/core/nms/${key}/CustomBiomeSource.java").absolutePath)
|
rootProject.file("adapters/bukkit/nms/${key}/src/main/java/art/arcane/iris/core/nms/${key}/CustomBiomeSource.java").absolutePath)
|
||||||
systemProperty('iris.vanillaStructureBiomesSource',
|
systemProperty('iris.vanillaStructureBiomesSource',
|
||||||
|
|||||||
@@ -69,9 +69,14 @@ public final class IrisStructureLocator {
|
|||||||
private static final Pattern NAMESPACED_RESOURCE_KEY = Pattern.compile("[a-z0-9_.-]+:[a-z0-9/._-]+");
|
private static final Pattern NAMESPACED_RESOURCE_KEY = Pattern.compile("[a-z0-9_.-]+:[a-z0-9/._-]+");
|
||||||
|
|
||||||
private static final Cache<Engine, PlacementIndex> INDEX_CACHE = Caffeine.newBuilder().weakKeys().build();
|
private static final Cache<Engine, PlacementIndex> INDEX_CACHE = Caffeine.newBuilder().weakKeys().build();
|
||||||
|
private static final Cache<Engine, LocatableIndex> LOCATABLE_INDEX_CACHE =
|
||||||
|
Caffeine.newBuilder().weakKeys().build();
|
||||||
private static final PlacementIndex EMPTY_INDEX = new PlacementIndex(
|
private static final PlacementIndex EMPTY_INDEX = new PlacementIndex(
|
||||||
Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet(),
|
Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet(),
|
||||||
Collections.emptySet(), Collections.emptyList());
|
Collections.emptySet(), Collections.emptyList());
|
||||||
|
private static final LocatableIndex EMPTY_LOCATABLE_INDEX = new LocatableIndex(
|
||||||
|
Collections.emptySet(), Collections.emptySet(), Collections.emptySet(),
|
||||||
|
Collections.emptySet(), Collections.emptySet(), Collections.emptySet());
|
||||||
private static final LocateResult NOT_FOUND_RESULT = new LocateResult(LocateStatus.NOT_FOUND, 0, 0, 0);
|
private static final LocateResult NOT_FOUND_RESULT = new LocateResult(LocateStatus.NOT_FOUND, 0, 0, 0);
|
||||||
private static final LocateResult SEARCH_LIMIT_RESULT =
|
private static final LocateResult SEARCH_LIMIT_RESULT =
|
||||||
new LocateResult(LocateStatus.SEARCH_LIMIT_REACHED, 0, 0, 0);
|
new LocateResult(LocateStatus.SEARCH_LIMIT_REACHED, 0, 0, 0);
|
||||||
@@ -97,6 +102,39 @@ public final class IrisStructureLocator {
|
|||||||
|| placementIndex.vanillaAliases.contains(normalizedKey);
|
|| placementIndex.vanillaAliases.contains(normalizedKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean hasLocatablePlacement(Engine engine, String key) {
|
||||||
|
if (engine == null || key == null || key.isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return locatableIndex(engine).normalizedKeys().contains(normalize(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean hasLocatableEditablePlacement(Engine engine, String key) {
|
||||||
|
if (engine == null || key == null || key.isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return locatableIndex(engine).normalizedEditableKeys().contains(normalize(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean hasLocatableNativePlacement(Engine engine, String key) {
|
||||||
|
if (engine == null || key == null || key.isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return locatableIndex(engine).normalizedNativeKeys().contains(normalize(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Set<String> locatableKeys(Engine engine) {
|
||||||
|
return locatableIndex(engine).keys();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Set<String> locatableEditableKeys(Engine engine) {
|
||||||
|
return locatableIndex(engine).editableKeys();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Set<String> locatableNativeKeys(Engine engine) {
|
||||||
|
return locatableIndex(engine).nativeKeys();
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean hasNativePlacement(Engine engine, String key) {
|
public static boolean hasNativePlacement(Engine engine, String key) {
|
||||||
if (engine == null || key == null || key.isBlank()) {
|
if (engine == null || key == null || key.isBlank()) {
|
||||||
return false;
|
return false;
|
||||||
@@ -126,6 +164,7 @@ public final class IrisStructureLocator {
|
|||||||
public static void invalidate(Engine engine) {
|
public static void invalidate(Engine engine) {
|
||||||
if (engine != null) {
|
if (engine != null) {
|
||||||
INDEX_CACHE.invalidate(engine);
|
INDEX_CACHE.invalidate(engine);
|
||||||
|
LOCATABLE_INDEX_CACHE.invalidate(engine);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -796,7 +835,8 @@ public final class IrisStructureLocator {
|
|||||||
List<IrisStructurePlacement> concentricRings = new ArrayList<>();
|
List<IrisStructurePlacement> concentricRings = new ArrayList<>();
|
||||||
boolean hasDensity = false;
|
boolean hasDensity = false;
|
||||||
for (IrisStructurePlacement placement : index(engine).placements) {
|
for (IrisStructurePlacement placement : index(engine).placements) {
|
||||||
if (!matches(placement, key, engine.getData())) {
|
if (!isSearchablePlacement(engine, placement)
|
||||||
|
|| !matches(placement, key, engine.getData())) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (placement.getDistribution() == StructureDistribution.RANDOM_SPREAD) {
|
if (placement.getDistribution() == StructureDistribution.RANDOM_SPREAD) {
|
||||||
@@ -805,7 +845,7 @@ public final class IrisStructureLocator {
|
|||||||
StructurePlacementGrid.placementSalt(placement)));
|
StructurePlacementGrid.placementSalt(placement)));
|
||||||
} else if (placement.getDistribution() == StructureDistribution.CONCENTRIC_RINGS) {
|
} else if (placement.getDistribution() == StructureDistribution.CONCENTRIC_RINGS) {
|
||||||
concentricRings.add(placement);
|
concentricRings.add(placement);
|
||||||
} else if (isSearchableDensityPlacement(engine, placement)) {
|
} else if (placement.getDistribution() == StructureDistribution.DENSITY) {
|
||||||
hasDensity = true;
|
hasDensity = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -813,8 +853,18 @@ public final class IrisStructureLocator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static boolean isSearchableDensityPlacement(Engine engine, IrisStructurePlacement placement) {
|
static boolean isSearchableDensityPlacement(Engine engine, IrisStructurePlacement placement) {
|
||||||
if (engine == null || placement == null || placement.getDistribution() != StructureDistribution.DENSITY
|
return placement != null
|
||||||
|| !(placement.getDensity() > 0.0) || engine.getHeight() <= 0) {
|
&& placement.getDistribution() == StructureDistribution.DENSITY
|
||||||
|
&& isSearchablePlacement(engine, placement);
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean isSearchablePlacement(Engine engine, IrisStructurePlacement placement) {
|
||||||
|
if (engine == null || placement == null || placement.getDistribution() == null
|
||||||
|
|| engine.getHeight() <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (placement.getDistribution() == StructureDistribution.DENSITY
|
||||||
|
&& !(placement.getDensity() > 0.0)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
long worldMin = (long) engine.getMinHeight() + (placement.isUnderground() ? 1L : 0L);
|
long worldMin = (long) engine.getMinHeight() + (placement.isUnderground() ? 1L : 0L);
|
||||||
@@ -952,6 +1002,64 @@ public final class IrisStructureLocator {
|
|||||||
return INDEX_CACHE.get(engine, ignored -> build(engine));
|
return INDEX_CACHE.get(engine, ignored -> build(engine));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static LocatableIndex locatableIndex(Engine engine) {
|
||||||
|
if (engine == null) {
|
||||||
|
return EMPTY_LOCATABLE_INDEX;
|
||||||
|
}
|
||||||
|
return LOCATABLE_INDEX_CACHE.get(engine, ignored -> buildLocatableIndex(engine));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LocatableIndex buildLocatableIndex(Engine engine) {
|
||||||
|
IrisData data = engine.getData();
|
||||||
|
Set<String> keys = new LinkedHashSet<>();
|
||||||
|
Set<String> normalizedKeys = new LinkedHashSet<>();
|
||||||
|
Set<String> editableKeys = new LinkedHashSet<>();
|
||||||
|
Set<String> normalizedEditableKeys = new LinkedHashSet<>();
|
||||||
|
Set<String> nativeKeys = new LinkedHashSet<>();
|
||||||
|
Set<String> normalizedNativeKeys = new LinkedHashSet<>();
|
||||||
|
for (IrisStructurePlacement placement : index(engine).placements) {
|
||||||
|
if (!isSearchablePlacement(engine, placement)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (placement.hasNativeStructures()) {
|
||||||
|
for (IrisNativeStructure source : placement.getNativeStructures()) {
|
||||||
|
addLocatableKey(source.getStructure(), keys, normalizedKeys);
|
||||||
|
addLocatableKey(source.getStructure(), nativeKeys, normalizedNativeKeys);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (String structureKey : placement.getStructures()) {
|
||||||
|
IrisStructure structure = data.load(IrisStructure.class, structureKey, false);
|
||||||
|
if (structure == null) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Iris structure placement references missing structure '" + structureKey + "'");
|
||||||
|
}
|
||||||
|
addLocatableKey(structureKey, keys, normalizedKeys);
|
||||||
|
addLocatableKey(structureKey, editableKeys, normalizedEditableKeys);
|
||||||
|
addLocatableKey(structure.getLoadKey(), keys, normalizedKeys);
|
||||||
|
addLocatableKey(structure.getLoadKey(), editableKeys, normalizedEditableKeys);
|
||||||
|
addLocatableKey(structure.getVanillaSource(), keys, normalizedKeys);
|
||||||
|
addLocatableKey(structure.getVanillaSource(), editableKeys, normalizedEditableKeys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new LocatableIndex(
|
||||||
|
Collections.unmodifiableSet(keys),
|
||||||
|
Collections.unmodifiableSet(normalizedKeys),
|
||||||
|
Collections.unmodifiableSet(editableKeys),
|
||||||
|
Collections.unmodifiableSet(normalizedEditableKeys),
|
||||||
|
Collections.unmodifiableSet(nativeKeys),
|
||||||
|
Collections.unmodifiableSet(normalizedNativeKeys));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addLocatableKey(String key, Set<String> keys, Set<String> normalizedKeys) {
|
||||||
|
if (key == null || key.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String trimmedKey = key.trim();
|
||||||
|
keys.add(trimmedKey);
|
||||||
|
normalizedKeys.add(normalize(trimmedKey));
|
||||||
|
}
|
||||||
|
|
||||||
private static PlacementIndex build(Engine engine) {
|
private static PlacementIndex build(Engine engine) {
|
||||||
IrisData data = engine.getData();
|
IrisData data = engine.getData();
|
||||||
Set<String> loadKeys = new LinkedHashSet<>();
|
Set<String> loadKeys = new LinkedHashSet<>();
|
||||||
@@ -1112,6 +1220,11 @@ public final class IrisStructureLocator {
|
|||||||
List<IrisStructurePlacement> concentricRings, boolean hasDensity) {
|
List<IrisStructurePlacement> concentricRings, boolean hasDensity) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record LocatableIndex(Set<String> keys, Set<String> normalizedKeys,
|
||||||
|
Set<String> editableKeys, Set<String> normalizedEditableKeys,
|
||||||
|
Set<String> nativeKeys, Set<String> normalizedNativeKeys) {
|
||||||
|
}
|
||||||
|
|
||||||
private record ResolvedStart(int originX, int baseY, int originZ) {
|
private record ResolvedStart(int originX, int baseY, int originZ) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+88
-30
@@ -18,42 +18,57 @@
|
|||||||
|
|
||||||
package art.arcane.iris.util.common.director.specialhandlers;
|
package art.arcane.iris.util.common.director.specialhandlers;
|
||||||
|
|
||||||
import art.arcane.iris.core.nms.INMS;
|
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
||||||
|
import art.arcane.iris.engine.framework.StructureReachability;
|
||||||
|
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
||||||
|
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
|
||||||
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
|
import art.arcane.iris.spi.PlatformStructureHooks;
|
||||||
import art.arcane.iris.util.common.director.DirectorParameterHandler;
|
import art.arcane.iris.util.common.director.DirectorParameterHandler;
|
||||||
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
import art.arcane.volmlib.util.director.exceptions.DirectorParsingException;
|
import art.arcane.volmlib.util.director.exceptions.DirectorParsingException;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
public class StructureHandler implements DirectorParameterHandler<String> {
|
public class StructureHandler implements DirectorParameterHandler<String> {
|
||||||
@Override
|
@Override
|
||||||
public KList<String> getPossibilities() {
|
public KList<String> getPossibilities() {
|
||||||
KList<String> keys = new KList<>();
|
Engine activeEngine = engine();
|
||||||
|
if (activeEngine == null) {
|
||||||
try {
|
return new KList<>();
|
||||||
for (String k : INMS.get().getStructureKeys()) {
|
|
||||||
if (k != null && !k.isEmpty()) {
|
|
||||||
keys.addIfMissing(k);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Throwable ignored) {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
boolean nativeGenerationEnabled = nativeStructureGenerationEnabled();
|
||||||
Engine e = engine();
|
PlatformStructureHooks structureHooks = IrisPlatforms.get().structureHooks();
|
||||||
if (e != null) {
|
Map<String, String> registeredKeys = distinctKeys(structureHooks.structureKeys());
|
||||||
for (String k : IrisStructureLocator.placedKeys(e)) {
|
Set<String> reachableKeys = StructureReachability.reachableKeys(activeEngine);
|
||||||
if (k != null && !k.isEmpty()) {
|
Map<String, String> suggestions = new LinkedHashMap<>();
|
||||||
keys.addIfMissing(k);
|
|
||||||
}
|
for (Map.Entry<String, String> entry : registeredKeys.entrySet()) {
|
||||||
}
|
if (isEligibleRegisteredKey(activeEngine, entry.getValue(), entry.getKey(), reachableKeys,
|
||||||
|
nativeGenerationEnabled)) {
|
||||||
|
suggestions.put(entry.getKey(), entry.getValue());
|
||||||
}
|
}
|
||||||
} catch (Throwable ignored) {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return keys;
|
Set<String> locatableKeys = IrisStructureLocator.locatableEditableKeys(activeEngine);
|
||||||
|
for (String key : locatableKeys) {
|
||||||
|
String normalizedKey = normalizeKey(key);
|
||||||
|
if (!normalizedKey.isEmpty()
|
||||||
|
&& !registeredKeys.containsKey(normalizedKey)
|
||||||
|
&& !IrisStructureLocator.hasNativePlacement(activeEngine, key)) {
|
||||||
|
suggestions.putIfAbsent(normalizedKey, key.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new KList<>(suggestions.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -64,15 +79,12 @@ public class StructureHandler implements DirectorParameterHandler<String> {
|
|||||||
@Override
|
@Override
|
||||||
public String parse(String in, boolean force) throws DirectorParsingException {
|
public String parse(String in, boolean force) throws DirectorParsingException {
|
||||||
KList<String> options = getPossibilities(in);
|
KList<String> options = getPossibilities(in);
|
||||||
|
for (String option : options) {
|
||||||
if (options.isEmpty()) {
|
if (option.equalsIgnoreCase(in)) {
|
||||||
return in;
|
return option;
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
return options.stream().filter((i) -> toString(i).equalsIgnoreCase(in)).collect(Collectors.toList()).get(0);
|
|
||||||
} catch (Throwable e) {
|
|
||||||
return in;
|
|
||||||
}
|
}
|
||||||
|
return in;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -86,4 +98,50 @@ public class StructureHandler implements DirectorParameterHandler<String> {
|
|||||||
|
|
||||||
return f == null ? "minecraft_ancient_city" : f;
|
return f == null ? "minecraft_ancient_city" : f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected boolean nativeStructureGenerationEnabled() {
|
||||||
|
Player activePlayer = player();
|
||||||
|
return activePlayer != null && activePlayer.getWorld().canGenerateStructures();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, String> distinctKeys(List<String> keys) {
|
||||||
|
Map<String, String> distinct = new LinkedHashMap<>();
|
||||||
|
for (String key : keys) {
|
||||||
|
String normalizedKey = normalizeKey(key);
|
||||||
|
if (!normalizedKey.isEmpty()) {
|
||||||
|
distinct.putIfAbsent(normalizedKey, key.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return distinct;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isEligibleRegisteredKey(Engine engine, String key, String normalizedKey,
|
||||||
|
Set<String> reachableKeys, boolean nativeGenerationEnabled) {
|
||||||
|
IrisNativeStructureDecision decision = NativeStructureGenerationPolicy.resolve(engine, key, false);
|
||||||
|
boolean nativePlacement = IrisStructureLocator.hasNativePlacement(engine, key);
|
||||||
|
boolean locatableNativePlacement = nativePlacement
|
||||||
|
&& IrisStructureLocator.hasLocatableNativePlacement(engine, key);
|
||||||
|
boolean locatableEditableReplacement = !nativePlacement
|
||||||
|
&& decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS
|
||||||
|
&& IrisStructureLocator.hasLocatableEditablePlacement(engine, key);
|
||||||
|
return isEligibleRegisteredKey(
|
||||||
|
decision, nativePlacement, locatableNativePlacement, locatableEditableReplacement,
|
||||||
|
reachableKeys.contains(normalizedKey), nativeGenerationEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean isEligibleRegisteredKey(IrisNativeStructureDecision decision, boolean nativePlacement,
|
||||||
|
boolean locatableNativePlacement,
|
||||||
|
boolean locatableEditableReplacement,
|
||||||
|
boolean reachable, boolean nativeGenerationEnabled) {
|
||||||
|
if (decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS) {
|
||||||
|
return locatableEditableReplacement
|
||||||
|
|| nativeGenerationEnabled && nativePlacement && locatableNativePlacement;
|
||||||
|
}
|
||||||
|
return nativeGenerationEnabled && decision.generate()
|
||||||
|
&& (reachable || (nativePlacement && locatableNativePlacement));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalizeKey(String key) {
|
||||||
|
return key == null ? "" : key.trim().toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+64
@@ -77,6 +77,70 @@ public class IrisStructureLocatorContractTest {
|
|||||||
assertFalse(IrisStructureLocator.isPlaced(engine, ""));
|
assertFalse(IrisStructureLocator.isPlaced(engine, ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void locatablePlacementsExcludeDisabledDensityConfigurations() {
|
||||||
|
Engine disabled = densityEngine(0.0, false, -64, 384, -2032, 2032);
|
||||||
|
Engine active = densityEngine(0.01, false, -64, 384, -2032, 2032);
|
||||||
|
|
||||||
|
assertTrue(IrisStructureLocator.isPlaced(disabled, "test:density"));
|
||||||
|
assertFalse(IrisStructureLocator.hasLocatablePlacement(disabled, "test:density"));
|
||||||
|
assertTrue(IrisStructureLocator.locatableKeys(disabled).isEmpty());
|
||||||
|
assertTrue(IrisStructureLocator.hasLocatablePlacement(active, "test:density"));
|
||||||
|
assertTrue(IrisStructureLocator.locatableKeys(active).contains("test:density"));
|
||||||
|
assertFalse(IrisStructureLocator.hasLocatablePlacement(null, "test:density"));
|
||||||
|
assertTrue(IrisStructureLocator.locatableKeys(null).isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void locatablePlacementsExcludeEveryDistributionOutsideWorldHeight() {
|
||||||
|
Engine randomSpread = densityEngine(1.0, false, -64, 384, 400, 500);
|
||||||
|
randomSpread.getDimension().getStructures().get(0)
|
||||||
|
.setDistribution(StructureDistribution.RANDOM_SPREAD);
|
||||||
|
Engine concentricRings = densityEngine(1.0, false, -64, 384, 400, 500);
|
||||||
|
concentricRings.getDimension().getStructures().get(0)
|
||||||
|
.setDistribution(StructureDistribution.CONCENTRIC_RINGS);
|
||||||
|
|
||||||
|
assertFalse(IrisStructureLocator.hasLocatablePlacement(randomSpread, "test:density"));
|
||||||
|
assertFalse(IrisStructureLocator.hasLocatablePlacement(concentricRings, "test:density"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void locatableIndexIncludesAliasesAndSeparatesEditableFromNativePlacements() {
|
||||||
|
IrisData data = mock(IrisData.class);
|
||||||
|
IrisStructure structure = new IrisStructure();
|
||||||
|
structure.setLoadKey("test:city");
|
||||||
|
structure.setVanillaSource("source:city");
|
||||||
|
when(data.load(IrisStructure.class, "test:city_definition", false)).thenReturn(structure);
|
||||||
|
|
||||||
|
IrisStructurePlacement editable = new IrisStructurePlacement();
|
||||||
|
editable.getStructures().add("test:city_definition");
|
||||||
|
IrisStructurePlacement nativePlacement = new IrisStructurePlacement();
|
||||||
|
nativePlacement.getNativeStructures().add(new IrisNativeStructure()
|
||||||
|
.setStructure("source:native_city"));
|
||||||
|
IrisDimension dimension = mock(IrisDimension.class);
|
||||||
|
KList<IrisStructurePlacement> placements = new KList<>();
|
||||||
|
placements.add(editable);
|
||||||
|
placements.add(nativePlacement);
|
||||||
|
Engine engine = mock(Engine.class);
|
||||||
|
when(engine.getData()).thenReturn(data);
|
||||||
|
when(engine.getDimension()).thenReturn(dimension);
|
||||||
|
when(engine.getMinHeight()).thenReturn(-64);
|
||||||
|
when(engine.getHeight()).thenReturn(384);
|
||||||
|
when(dimension.getStructures()).thenReturn(placements);
|
||||||
|
when(dimension.getAllRegions(engine)).thenReturn(new KList<>());
|
||||||
|
when(dimension.getReachableBiomes(engine)).thenReturn(new KList<>());
|
||||||
|
|
||||||
|
assertEquals(Set.of("test:city_definition", "test:city", "source:city", "source:native_city"),
|
||||||
|
IrisStructureLocator.locatableKeys(engine));
|
||||||
|
assertEquals(Set.of("test:city_definition", "test:city", "source:city"),
|
||||||
|
IrisStructureLocator.locatableEditableKeys(engine));
|
||||||
|
assertEquals(Set.of("source:native_city"), IrisStructureLocator.locatableNativeKeys(engine));
|
||||||
|
assertTrue(IrisStructureLocator.hasLocatableEditablePlacement(engine, "SOURCE:CITY"));
|
||||||
|
assertFalse(IrisStructureLocator.hasLocatableEditablePlacement(engine, "source:native_city"));
|
||||||
|
assertTrue(IrisStructureLocator.hasLocatableNativePlacement(engine, "SOURCE:NATIVE_CITY"));
|
||||||
|
assertFalse(IrisStructureLocator.hasLocatableNativePlacement(engine, "source:city"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void suppressesVanillaIsFalseForNullEngine() {
|
public void suppressesVanillaIsFalseForNullEngine() {
|
||||||
assertFalse(IrisStructureLocator.suppressesVanilla(null, "minecraft:ancient_city"));
|
assertFalse(IrisStructureLocator.suppressesVanilla(null, "minecraft:ancient_city"));
|
||||||
|
|||||||
+300
@@ -0,0 +1,300 @@
|
|||||||
|
package art.arcane.iris.util.common.director.specialhandlers;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
|
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||||
|
import art.arcane.iris.engine.framework.StructureReachability;
|
||||||
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
|
import art.arcane.iris.engine.object.IrisImportedStructureControl;
|
||||||
|
import art.arcane.iris.engine.object.IrisNativeStructure;
|
||||||
|
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
||||||
|
import art.arcane.iris.engine.object.IrisStructure;
|
||||||
|
import art.arcane.iris.engine.object.IrisStructurePlacement;
|
||||||
|
import art.arcane.iris.engine.object.IrisWorld;
|
||||||
|
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
|
||||||
|
import art.arcane.iris.engine.object.StructureDistribution;
|
||||||
|
import art.arcane.iris.spi.IrisPlatform;
|
||||||
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
|
import art.arcane.iris.spi.PlatformStructureHooks;
|
||||||
|
import art.arcane.iris.spi.PlatformWorld;
|
||||||
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
|
import art.arcane.volmlib.util.director.exceptions.DirectorParsingException;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class StructureHandlerTest {
|
||||||
|
@Test
|
||||||
|
public void registeredEligibilityMatchesFindExecutionTruthTable() {
|
||||||
|
IrisNativeStructureDecision replacement = decision(NativeStructureGenerationStatus.REPLACED_BY_IRIS);
|
||||||
|
IrisNativeStructureDecision generated = decision(NativeStructureGenerationStatus.GENERATE_NATIVE);
|
||||||
|
IrisNativeStructureDecision disabled = decision(NativeStructureGenerationStatus.DISABLED_BY_PACK);
|
||||||
|
|
||||||
|
assertTrue(StructureHandler.isEligibleRegisteredKey(
|
||||||
|
replacement, false, false, true, false, false));
|
||||||
|
assertFalse(StructureHandler.isEligibleRegisteredKey(
|
||||||
|
replacement, false, false, false, true, true));
|
||||||
|
assertTrue(StructureHandler.isEligibleRegisteredKey(
|
||||||
|
replacement, true, true, false, false, true));
|
||||||
|
assertFalse(StructureHandler.isEligibleRegisteredKey(
|
||||||
|
replacement, true, true, false, false, false));
|
||||||
|
assertTrue(StructureHandler.isEligibleRegisteredKey(
|
||||||
|
generated, true, true, false, false, true));
|
||||||
|
assertFalse(StructureHandler.isEligibleRegisteredKey(
|
||||||
|
generated, true, false, false, false, true));
|
||||||
|
assertTrue(StructureHandler.isEligibleRegisteredKey(
|
||||||
|
generated, false, false, false, true, true));
|
||||||
|
assertFalse(StructureHandler.isEligibleRegisteredKey(
|
||||||
|
generated, false, false, false, true, false));
|
||||||
|
assertFalse(StructureHandler.isEligibleRegisteredKey(
|
||||||
|
disabled, true, true, false, true, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void requiresActiveIrisEngineBeforeReadingPlatformRegistries() throws DirectorParsingException {
|
||||||
|
IrisPlatforms.unbind();
|
||||||
|
TestStructureHandler handler = new TestStructureHandler(null, true);
|
||||||
|
|
||||||
|
assertTrue(handler.getPossibilities().isEmpty());
|
||||||
|
assertEquals("manual:structure", handler.parse("manual:structure", false));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void filtersAndDeduplicatesSuggestionsForActiveEngine() {
|
||||||
|
IrisData data = mock(IrisData.class);
|
||||||
|
Engine engine = mock(Engine.class);
|
||||||
|
IrisDimension dimension = mock(IrisDimension.class);
|
||||||
|
IrisWorld world = mock(IrisWorld.class);
|
||||||
|
PlatformWorld platformWorld = mock(PlatformWorld.class);
|
||||||
|
IrisPlatform platform = mock(IrisPlatform.class);
|
||||||
|
PlatformStructureHooks hooks = mock(PlatformStructureHooks.class);
|
||||||
|
IrisImportedStructureControl control = new IrisImportedStructureControl();
|
||||||
|
control.getDisabled().add("pack:disabled");
|
||||||
|
control.getDisabled().add("pack:replacement");
|
||||||
|
control.getDisabled().add("pack:dormant_replacement");
|
||||||
|
|
||||||
|
KList<IrisStructurePlacement> placements = new KList<>();
|
||||||
|
placements.add(nativePlacement("pack:replacement", StructureDistribution.RANDOM_SPREAD, 1.0));
|
||||||
|
placements.add(nativePlacement("pack:dormant_replacement", StructureDistribution.DENSITY, 0.0));
|
||||||
|
placements.add(nativePlacement("pack:explicit", StructureDistribution.RANDOM_SPREAD, 1.0));
|
||||||
|
placements.add(nativePlacement("pack:dormant_explicit", StructureDistribution.DENSITY, 0.0));
|
||||||
|
placements.add(nativePlacement("pack:unregistered_native", StructureDistribution.RANDOM_SPREAD, 1.0));
|
||||||
|
placements.add(nativePlacement("iris:custom", StructureDistribution.RANDOM_SPREAD, 1.0));
|
||||||
|
placements.add(editablePlacement("iris:custom_definition"));
|
||||||
|
placements.add(editablePlacement("iris:collision_definition"));
|
||||||
|
|
||||||
|
IrisStructure custom = new IrisStructure();
|
||||||
|
custom.setLoadKey("iris:custom");
|
||||||
|
IrisStructure collision = new IrisStructure();
|
||||||
|
collision.setLoadKey("pack:collision");
|
||||||
|
when(data.load(IrisStructure.class, "iris:custom_definition", false)).thenReturn(custom);
|
||||||
|
when(data.load(IrisStructure.class, "iris:collision_definition", false)).thenReturn(collision);
|
||||||
|
when(engine.getData()).thenReturn(data);
|
||||||
|
when(engine.getDimension()).thenReturn(dimension);
|
||||||
|
when(engine.getWorld()).thenReturn(world);
|
||||||
|
when(engine.getMinHeight()).thenReturn(-64);
|
||||||
|
when(engine.getHeight()).thenReturn(384);
|
||||||
|
when(world.platformWorld()).thenReturn(platformWorld);
|
||||||
|
when(dimension.getImportedStructures()).thenReturn(control);
|
||||||
|
when(dimension.getStructures()).thenReturn(placements);
|
||||||
|
when(dimension.getAllRegions(engine)).thenReturn(new KList<>());
|
||||||
|
when(dimension.getReachableBiomes(engine)).thenReturn(new KList<>());
|
||||||
|
when(platform.structureHooks()).thenReturn(hooks);
|
||||||
|
when(hooks.structureKeys()).thenReturn(List.of(
|
||||||
|
"pack:reachable",
|
||||||
|
"PACK:REACHABLE",
|
||||||
|
"pack:unreachable",
|
||||||
|
"pack:disabled",
|
||||||
|
"pack:replacement",
|
||||||
|
"pack:dormant_replacement",
|
||||||
|
"pack:explicit",
|
||||||
|
"pack:dormant_explicit",
|
||||||
|
"PACK:COLLISION"));
|
||||||
|
when(hooks.reachableStructureKeys(platformWorld)).thenReturn(List.of("PACK:REACHABLE"));
|
||||||
|
|
||||||
|
IrisPlatforms.unbind();
|
||||||
|
IrisPlatforms.bind(platform);
|
||||||
|
try {
|
||||||
|
KList<String> possibilities = new TestStructureHandler(engine, true).getPossibilities();
|
||||||
|
Set<String> distinctPossibilities = new LinkedHashSet<>(possibilities);
|
||||||
|
|
||||||
|
assertEquals(Set.of(
|
||||||
|
"pack:reachable",
|
||||||
|
"pack:replacement",
|
||||||
|
"pack:explicit",
|
||||||
|
"iris:custom_definition",
|
||||||
|
"iris:collision_definition"), distinctPossibilities);
|
||||||
|
assertEquals(distinctPossibilities.size(), possibilities.size());
|
||||||
|
assertFalse(possibilities.stream().anyMatch("pack:collision"::equalsIgnoreCase));
|
||||||
|
assertFalse(possibilities.contains("pack:unregistered_native"));
|
||||||
|
verify(hooks, times(1)).structureKeys();
|
||||||
|
verify(hooks, times(1)).reachableStructureKeys(platformWorld);
|
||||||
|
} finally {
|
||||||
|
IrisStructureLocator.invalidate(engine);
|
||||||
|
StructureReachability.invalidate(engine);
|
||||||
|
IrisPlatforms.unbind();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void disabledWorldKeepsOnlyUnregisteredEditablePlacements() {
|
||||||
|
IrisData data = mock(IrisData.class);
|
||||||
|
Engine engine = mock(Engine.class);
|
||||||
|
IrisDimension dimension = mock(IrisDimension.class);
|
||||||
|
IrisWorld world = mock(IrisWorld.class);
|
||||||
|
PlatformWorld platformWorld = mock(PlatformWorld.class);
|
||||||
|
IrisPlatform platform = mock(IrisPlatform.class);
|
||||||
|
PlatformStructureHooks hooks = mock(PlatformStructureHooks.class);
|
||||||
|
IrisImportedStructureControl control = new IrisImportedStructureControl();
|
||||||
|
control.getDisabled().add("minecraft:replacement");
|
||||||
|
|
||||||
|
IrisStructure editable = new IrisStructure();
|
||||||
|
editable.setLoadKey("iris:editable");
|
||||||
|
IrisStructure replacement = new IrisStructure();
|
||||||
|
replacement.setLoadKey("iris:replacement");
|
||||||
|
replacement.setVanillaSource("minecraft:replacement");
|
||||||
|
KList<IrisStructurePlacement> placements = new KList<>();
|
||||||
|
placements.add(editablePlacement("iris:editable_definition"));
|
||||||
|
placements.add(editablePlacement("iris:replacement_definition"));
|
||||||
|
placements.add(nativePlacement("minecraft:explicit", StructureDistribution.RANDOM_SPREAD, 1.0));
|
||||||
|
|
||||||
|
when(data.load(IrisStructure.class, "iris:editable_definition", false)).thenReturn(editable);
|
||||||
|
when(data.load(IrisStructure.class, "iris:replacement_definition", false)).thenReturn(replacement);
|
||||||
|
when(engine.getData()).thenReturn(data);
|
||||||
|
when(engine.getDimension()).thenReturn(dimension);
|
||||||
|
when(engine.getWorld()).thenReturn(world);
|
||||||
|
when(engine.getMinHeight()).thenReturn(-64);
|
||||||
|
when(engine.getHeight()).thenReturn(384);
|
||||||
|
when(world.platformWorld()).thenReturn(platformWorld);
|
||||||
|
when(dimension.getImportedStructures()).thenReturn(control);
|
||||||
|
when(dimension.getStructures()).thenReturn(placements);
|
||||||
|
when(dimension.getAllRegions(engine)).thenReturn(new KList<>());
|
||||||
|
when(dimension.getReachableBiomes(engine)).thenReturn(new KList<>());
|
||||||
|
when(platform.structureHooks()).thenReturn(hooks);
|
||||||
|
when(hooks.structureKeys()).thenReturn(List.of(
|
||||||
|
"minecraft:replacement", "minecraft:explicit", "minecraft:reachable"));
|
||||||
|
when(hooks.reachableStructureKeys(platformWorld)).thenReturn(List.of("minecraft:reachable"));
|
||||||
|
|
||||||
|
IrisPlatforms.unbind();
|
||||||
|
IrisPlatforms.bind(platform);
|
||||||
|
try {
|
||||||
|
Set<String> possibilities = new LinkedHashSet<>(
|
||||||
|
new TestStructureHandler(engine, false).getPossibilities());
|
||||||
|
|
||||||
|
assertEquals(Set.of(
|
||||||
|
"minecraft:replacement",
|
||||||
|
"iris:editable_definition",
|
||||||
|
"iris:editable",
|
||||||
|
"iris:replacement_definition",
|
||||||
|
"iris:replacement"), possibilities);
|
||||||
|
assertFalse(possibilities.contains("minecraft:explicit"));
|
||||||
|
assertFalse(possibilities.contains("minecraft:reachable"));
|
||||||
|
} finally {
|
||||||
|
IrisStructureLocator.invalidate(engine);
|
||||||
|
StructureReachability.invalidate(engine);
|
||||||
|
IrisPlatforms.unbind();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void propagatesRegistryFailures() {
|
||||||
|
Engine engine = mock(Engine.class);
|
||||||
|
IrisPlatform platform = mock(IrisPlatform.class);
|
||||||
|
PlatformStructureHooks hooks = mock(PlatformStructureHooks.class);
|
||||||
|
IllegalStateException failure = new IllegalStateException("registry unavailable");
|
||||||
|
when(platform.structureHooks()).thenReturn(hooks);
|
||||||
|
when(hooks.structureKeys()).thenThrow(failure);
|
||||||
|
|
||||||
|
IrisPlatforms.unbind();
|
||||||
|
IrisPlatforms.bind(platform);
|
||||||
|
try {
|
||||||
|
IllegalStateException thrown = assertThrows(
|
||||||
|
IllegalStateException.class, () -> new TestStructureHandler(engine, true).getPossibilities());
|
||||||
|
assertSame(failure, thrown);
|
||||||
|
} finally {
|
||||||
|
IrisPlatforms.unbind();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void completionUsesOneReachabilitySnapshotAndLocatableKeys() throws IOException {
|
||||||
|
Path sourcePath = Path.of(
|
||||||
|
"src/main/java/art/arcane/iris/util/common/director/specialhandlers/StructureHandler.java");
|
||||||
|
String source = Files.readString(sourcePath);
|
||||||
|
int methodStart = source.indexOf("public KList<String> getPossibilities()");
|
||||||
|
int methodEnd = source.indexOf("@Override\n public String toString", methodStart);
|
||||||
|
String method = source.substring(methodStart, methodEnd);
|
||||||
|
|
||||||
|
assertEquals(1, occurrences(method, "StructureReachability.reachableKeys(activeEngine)"));
|
||||||
|
assertEquals(0, occurrences(method, "IrisStructureLocator.locatableKeys(activeEngine)"));
|
||||||
|
assertEquals(1, occurrences(method, "IrisStructureLocator.locatableEditableKeys(activeEngine)"));
|
||||||
|
assertTrue(method.indexOf("if (activeEngine == null)")
|
||||||
|
< method.indexOf("IrisPlatforms.get().structureHooks()"));
|
||||||
|
assertFalse(method.contains("INMS"));
|
||||||
|
assertFalse(method.contains("catch ("));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IrisNativeStructureDecision decision(NativeStructureGenerationStatus status) {
|
||||||
|
return new IrisNativeStructureDecision(status, 0, null, false, false, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IrisStructurePlacement nativePlacement(String key, StructureDistribution distribution,
|
||||||
|
double density) {
|
||||||
|
IrisStructurePlacement placement = new IrisStructurePlacement();
|
||||||
|
placement.getNativeStructures().add(new IrisNativeStructure().setStructure(key));
|
||||||
|
placement.setDistribution(distribution);
|
||||||
|
placement.setDensity(density);
|
||||||
|
return placement;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IrisStructurePlacement editablePlacement(String key) {
|
||||||
|
IrisStructurePlacement placement = new IrisStructurePlacement();
|
||||||
|
placement.getStructures().add(key);
|
||||||
|
return placement;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int occurrences(String source, String target) {
|
||||||
|
int count = 0;
|
||||||
|
int offset = 0;
|
||||||
|
while ((offset = source.indexOf(target, offset)) >= 0) {
|
||||||
|
count++;
|
||||||
|
offset += target.length();
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class TestStructureHandler extends StructureHandler {
|
||||||
|
private final Engine activeEngine;
|
||||||
|
private final boolean nativeGenerationEnabled;
|
||||||
|
|
||||||
|
private TestStructureHandler(Engine activeEngine, boolean nativeGenerationEnabled) {
|
||||||
|
this.activeEngine = activeEngine;
|
||||||
|
this.nativeGenerationEnabled = nativeGenerationEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Engine engine() {
|
||||||
|
return activeEngine;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean nativeStructureGenerationEnabled() {
|
||||||
|
return nativeGenerationEnabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user