mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-19 23:14:10 +00:00
Added treasure loot coverage
- Gameplay opens normal and copper chests before inspecting loot - Map markers and saved centers match verified structure targets - Missing and disabled destinations retain unrelated chest loot - Storage variants, insertion rollback, region ownership, and resource reloads have runtime assertions - Separate data packs cover Minecraft 26.2 and 26.3 loot formats
This commit is contained in:
@@ -2,6 +2,7 @@ name: IrisNativeLifecycleFixture
|
||||
version: 1.0.0
|
||||
main: art.arcane.iris.fixture.NativeLifecycleFixture
|
||||
api-version: '26.1.2'
|
||||
folia-supported: true
|
||||
depend: [Iris]
|
||||
commands:
|
||||
irislifecycle:
|
||||
|
||||
+213
-6
@@ -1,8 +1,12 @@
|
||||
package art.arcane.iris.fixture;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.volmlib.nativelib.NativeAdapters;
|
||||
import art.arcane.iris.world.runtime.RuntimeInjection;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.structure.placement.IrisStructureLocator;
|
||||
import art.arcane.iris.world.IrisCreator;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import art.arcane.iris.world.IrisToolbelt;
|
||||
import art.arcane.iris.world.IrisWorldStorage;
|
||||
import art.arcane.iris.world.WorldCreatorCompat;
|
||||
@@ -13,6 +17,15 @@ import org.bukkit.entity.Player;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.Biome;
|
||||
import org.bukkit.block.Container;
|
||||
import org.bukkit.loot.Lootable;
|
||||
import org.bukkit.loot.LootTable;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.MapMeta;
|
||||
import org.bukkit.map.MapView;
|
||||
import org.bukkit.generator.structure.Structure;
|
||||
import org.bukkit.util.StructureSearchResult;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeTerrainAccess;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeStructureReader;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeWorldRuntime;
|
||||
@@ -38,12 +51,15 @@ public final class NativeLifecycleFixture extends JavaPlugin implements WorldRun
|
||||
private static final NamespacedKey WORLD_KEY = NamespacedKey.minecraft("native_lifecycle_probe");
|
||||
private static final NamespacedKey TERRAIN_KEY = new NamespacedKey("iris", "native_terrain_probe");
|
||||
private static final NamespacedKey COMMAND_KEY = new NamespacedKey("iris", "lifecycle_probe");
|
||||
private static final NamespacedKey TREASURE_KEY = new NamespacedKey("iris", "treasure_map_probe");
|
||||
private static final long WORLD_SEED = 78264193L;
|
||||
private NativeWorldRuntime runtime;
|
||||
private TreasureLootChecks treasureLootChecks;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
runtime = NativeAdapters.require(NativeWorldRuntime.class);
|
||||
treasureLootChecks = new TreasureLootChecks(this);
|
||||
NativeTerrainAccess terrain = NativeAdapters.require(NativeTerrainAccess.class);
|
||||
getLogger().info("Native lifecycle ready: " + runtime.description()
|
||||
+ "; terrain provider=" + terrain.getClass().getName());
|
||||
@@ -56,12 +72,32 @@ public final class NativeLifecycleFixture extends JavaPlugin implements WorldRun
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] arguments) {
|
||||
if (arguments.length != 1) {
|
||||
if (arguments.length == 0) {
|
||||
sender.sendMessage("Use /irislifecycle create|unload|status|structures|terrain-create|terrain-check|terrain-unload|lifecycle-status|lifecycle-entry");
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
switch (arguments[0]) {
|
||||
case "treasure-map-create" -> createTreasureMapWorld(sender);
|
||||
case "treasure-map-limit" -> runTreasureRegion(sender, () -> checkTreasureMapLimit(sender));
|
||||
case "treasure-map-chest" -> {
|
||||
if (arguments.length != 3) {
|
||||
throw new IllegalArgumentException("Use treasure-map-chest <destination> <CHEST|COPPER_CHEST>");
|
||||
}
|
||||
runTreasureRegion(sender, () -> prepareTreasureMapChest(sender, arguments[1], arguments[2]));
|
||||
}
|
||||
case "treasure-map-inspect" -> {
|
||||
if (arguments.length != 2) {
|
||||
throw new IllegalArgumentException("Use treasure-map-inspect <destination>");
|
||||
}
|
||||
runTreasureRegion(sender, () -> inspectTreasureMapChest(sender, arguments[1]));
|
||||
}
|
||||
case "treasure-map-check" -> runTreasureRegion(sender, () -> verifyTreasureMapLoot(sender));
|
||||
case "treasure-extra-check" -> runTreasureRegion(sender, () -> treasureLootChecks.verify(sender));
|
||||
case "treasure-region-check" -> runTreasureRegion(sender, () -> treasureLootChecks.verifyRegion(sender));
|
||||
case "treasure-reload-before" -> runTreasureRegion(sender, () -> treasureLootChecks.verifyReload(sender, false));
|
||||
case "treasure-reload-after" -> runTreasureRegion(sender, () -> treasureLootChecks.verifyReload(sender, true));
|
||||
case "treasure-resources" -> treasureLootChecks.reportResourceReloads(sender);
|
||||
case "create" -> create(sender);
|
||||
case "unload" -> unload(sender);
|
||||
case "structures" -> structures(sender);
|
||||
@@ -80,6 +116,177 @@ public final class NativeLifecycleFixture extends JavaPlugin implements WorldRun
|
||||
return true;
|
||||
}
|
||||
|
||||
private void createTreasureMapWorld(CommandSender sender) {
|
||||
if (!(sender instanceof Player player)) {
|
||||
throw new IllegalStateException("Treasure map fixture requires a player");
|
||||
}
|
||||
runTreasureRegion(sender, () -> prepareTreasureMapWorld(player));
|
||||
}
|
||||
|
||||
private void runTreasureRegion(CommandSender sender, Runnable task) {
|
||||
ensureTreasureMapWorld()
|
||||
.thenCompose(ignored -> J.runRegionFuture(Bukkit.getWorld(TREASURE_KEY), 0, 0, task))
|
||||
.whenComplete((ignored, failure) -> {
|
||||
if (failure != null) {
|
||||
fail(sender, failure);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private CompletableFuture<?> ensureTreasureMapWorld() {
|
||||
if (Bukkit.getWorld(TREASURE_KEY) != null) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
if (WorldCreatorCompat.persistentLevelRoot(TREASURE_KEY).exists()) {
|
||||
return Iris.instance.worldReconciler().loadWorld(ServerProperties.BUKKIT_YML, TREASURE_KEY.toString());
|
||||
}
|
||||
return runAsync(() -> {
|
||||
try {
|
||||
new IrisCreator().name(TREASURE_KEY.toString()).dimension("treasure-map-fixture")
|
||||
.seed(WORLD_SEED).create();
|
||||
} catch (Exception failure) {
|
||||
throw new IllegalStateException("Treasure map fixture creation failed", failure);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void prepareTreasureMapWorld(Player player) {
|
||||
World world = Bukkit.getWorld(TREASURE_KEY);
|
||||
if (world == null || !IrisToolbelt.isIrisWorld(world) || world.getSeed() != WORLD_SEED
|
||||
|| !"treasure-map-fixture".equals(IrisToolbelt.access(world).getEngine().getDimension().getLoadKey())) {
|
||||
throw new IllegalStateException("Treasure map fixture world identity is invalid");
|
||||
}
|
||||
for (int x = 0; x <= 8; x++) {
|
||||
for (int z = 0; z <= 8; z++) {
|
||||
world.getBlockAt(x, 160, z).setType(Material.STONE, false);
|
||||
for (int y = 161; y <= 164; y++) {
|
||||
world.getBlockAt(x, y, z).setType(Material.AIR, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!J.runEntity(player, () -> player.teleportAsync(new Location(world, 0.5, 161, 0.5)).whenComplete((teleported, failure) -> {
|
||||
if (failure != null) {
|
||||
fail(player, failure);
|
||||
} else if (!Boolean.TRUE.equals(teleported)) {
|
||||
fail(player, new IllegalStateException("Treasure map fixture teleport failed"));
|
||||
} else {
|
||||
player.sendMessage("TREASURE_MAP_READY world=" + TREASURE_KEY
|
||||
+ " version=" + Bukkit.getMinecraftVersion() + " folia=" + J.isFolia());
|
||||
}
|
||||
}))) {
|
||||
throw new IllegalStateException("Could not schedule treasure map fixture teleport");
|
||||
}
|
||||
}
|
||||
|
||||
private void checkTreasureMapLimit(CommandSender sender) {
|
||||
World world = Bukkit.getWorld(TREASURE_KEY);
|
||||
if (world == null) {
|
||||
throw new IllegalStateException("Treasure map fixture world is absent");
|
||||
}
|
||||
Engine engine = IrisToolbelt.access(world).getEngine();
|
||||
IrisStructureLocator.LocateResult located = IrisStructureLocator.locateInPlacementRings(
|
||||
engine, "minecraft:buried_treasure", 2, 2, 50, (x, z) -> true);
|
||||
if (located.status() != IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
|
||||
throw new IllegalStateException("Blocked treasure placement returned " + located.status());
|
||||
}
|
||||
sender.sendMessage("TREASURE_MAP_LIMIT status=" + located.status());
|
||||
}
|
||||
|
||||
private void prepareTreasureMapChest(CommandSender sender, String destination, String material) {
|
||||
World world = Bukkit.getWorld(TREASURE_KEY);
|
||||
treasureMapChest(world, destination, material);
|
||||
sender.sendMessage("TREASURE_MAP_CHEST destination=" + destination + " material=" + material);
|
||||
}
|
||||
|
||||
private Container treasureMapChest(World world, String destination, String material) {
|
||||
requireTreasureDestination(destination);
|
||||
if (!List.of("CHEST", "COPPER_CHEST").contains(material)) {
|
||||
throw new IllegalArgumentException("Unknown treasure map container: " + material);
|
||||
}
|
||||
LootTable loot = Bukkit.getLootTable(new NamespacedKey("iris_qa", destination));
|
||||
if (loot == null) {
|
||||
throw new IllegalStateException("Treasure map loot table is absent: " + destination);
|
||||
}
|
||||
world.getBlockAt(2, 161, 2).setType(Material.AIR, false);
|
||||
world.getBlockAt(2, 161, 2).setType(Material.valueOf(material), false);
|
||||
Container chest = (Container) world.getBlockAt(2, 161, 2).getState();
|
||||
((Lootable) chest).setLootTable(loot);
|
||||
((Lootable) chest).setSeed(WORLD_SEED);
|
||||
if (!chest.update(true, false)) {
|
||||
throw new IllegalStateException("Treasure map loot chest was not installed");
|
||||
}
|
||||
return (Container) world.getBlockAt(2, 161, 2).getState();
|
||||
}
|
||||
|
||||
private void verifyTreasureMapLoot(CommandSender sender) {
|
||||
checkTreasureMapLimit(sender);
|
||||
World world = Bukkit.getWorld(TREASURE_KEY);
|
||||
for (String destination : List.of("blocked", "disabled", "found", "mixed")) {
|
||||
Inventory inventory = treasureMapChest(world, destination, "CHEST").getInventory();
|
||||
inspectTreasureMapChest(sender, destination);
|
||||
inventory.remove(Material.DIAMOND);
|
||||
Inventory reopened = ((Container) world.getBlockAt(2, 161, 2).getState()).getInventory();
|
||||
if (reopened.contains(Material.DIAMOND)
|
||||
|| !(reopened.contains(Material.FILLED_MAP) || reopened.contains(Material.MAP))) {
|
||||
throw new IllegalStateException("Reopening changed " + destination + " chest loot");
|
||||
}
|
||||
}
|
||||
sender.sendMessage("TREASURE_MAP_CHECK destinations=4 retained=true");
|
||||
}
|
||||
|
||||
private void inspectTreasureMapChest(CommandSender sender, String destination) {
|
||||
requireTreasureDestination(destination);
|
||||
World world = Bukkit.getWorld(TREASURE_KEY);
|
||||
Inventory inventory = ((Container) world.getBlockAt(2, 161, 2).getState()).getInventory();
|
||||
ItemStack map = null;
|
||||
int diamonds = 0;
|
||||
for (ItemStack item : inventory.getContents()) {
|
||||
if (item == null) {
|
||||
continue;
|
||||
}
|
||||
if ((item.getType() == Material.MAP || item.getType() == Material.FILLED_MAP)
|
||||
&& item.getAmount() == 1 && map == null) {
|
||||
map = item;
|
||||
} else if (item.getType() == Material.DIAMOND) {
|
||||
diamonds += item.getAmount();
|
||||
} else {
|
||||
throw new IllegalStateException("Unexpected chest loot: " + item);
|
||||
}
|
||||
}
|
||||
if (map == null || diamonds != 1) {
|
||||
throw new IllegalStateException("Invalid " + destination + " chest: map=" + map + " diamonds=" + diamonds);
|
||||
}
|
||||
boolean expectedLocated = destination.equals("found") || destination.equals("mixed");
|
||||
MapView view = map.getItemMeta() instanceof MapMeta meta && meta.hasMapView() ? meta.getMapView() : null;
|
||||
if ((view != null) != expectedLocated) {
|
||||
throw new IllegalStateException("Unexpected map data for " + destination + ": " + map);
|
||||
}
|
||||
String result = "TREASURE_MAP_INSPECT destination=" + destination + " diamonds=1 located=" + expectedLocated;
|
||||
if (view != null) {
|
||||
StructureSearchResult target = world.locateNearestStructure(
|
||||
new Location(world, 2, 161, 2), Structure.DESERT_PYRAMID, 50, false);
|
||||
if (target == null || target.getStructure() != Structure.DESERT_PYRAMID) {
|
||||
throw new IllegalStateException("Map has no verified pyramid target");
|
||||
}
|
||||
int targetX = target.getLocation().getBlockX();
|
||||
int targetZ = target.getLocation().getBlockZ();
|
||||
if (view.getWorld() != world || view.getScale() != MapView.Scale.CLOSEST
|
||||
|| view.getCenterX() != Math.floorDiv(targetX + 64, 128) * 128
|
||||
|| view.getCenterZ() != Math.floorDiv(targetZ + 64, 128) * 128) {
|
||||
throw new IllegalStateException("Map data does not cover the verified pyramid at " + target.getLocation());
|
||||
}
|
||||
result += " mapId=" + view.getId() + " targetX=" + targetX + " targetZ=" + targetZ
|
||||
+ " centerX=" + view.getCenterX() + " centerZ=" + view.getCenterZ();
|
||||
}
|
||||
sender.sendMessage(result);
|
||||
}
|
||||
|
||||
private void requireTreasureDestination(String destination) {
|
||||
if (!List.of("blocked", "disabled", "found", "mixed").contains(destination)) {
|
||||
throw new IllegalArgumentException("Unknown treasure map destination: " + destination);
|
||||
}
|
||||
}
|
||||
|
||||
private void lifecycleStatus(CommandSender sender) throws Exception {
|
||||
World world = Bukkit.getWorld(COMMAND_KEY);
|
||||
Path storage = WorldCreatorCompat.persistentLevelRoot(COMMAND_KEY).toPath();
|
||||
@@ -282,11 +489,11 @@ public final class NativeLifecycleFixture extends JavaPlugin implements WorldRun
|
||||
}
|
||||
|
||||
public boolean regionized() {
|
||||
return false;
|
||||
return J.isFolia();
|
||||
}
|
||||
|
||||
public boolean primaryThread() {
|
||||
return Bukkit.isPrimaryThread();
|
||||
return J.isFolia() ? Bukkit.isGlobalTickThread() : Bukkit.isPrimaryThread();
|
||||
}
|
||||
|
||||
public CompletableFuture<Void> runGlobal(Runnable task) {
|
||||
@@ -299,10 +506,10 @@ public final class NativeLifecycleFixture extends JavaPlugin implements WorldRun
|
||||
result.completeExceptionally(failure);
|
||||
}
|
||||
};
|
||||
if (Bukkit.isPrimaryThread()) {
|
||||
if (primaryThread()) {
|
||||
wrapped.run();
|
||||
} else {
|
||||
Bukkit.getScheduler().runTask(this, wrapped);
|
||||
} else if (!J.runGlobal(wrapped)) {
|
||||
result.completeExceptionally(new IllegalStateException("Could not schedule fixture task"));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
+259
@@ -0,0 +1,259 @@
|
||||
package art.arcane.iris.fixture;
|
||||
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.platform.bukkit.BukkitBlockResolution;
|
||||
import art.arcane.iris.platform.generation.EngineBukkitOps;
|
||||
import art.arcane.iris.world.IrisToolbelt;
|
||||
import art.arcane.iris.world.loot.InventorySlotType;
|
||||
import art.arcane.iris.world.loot.IrisVanillaLootTable;
|
||||
import art.arcane.iris.world.loot.IrisLootTable;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import io.papermc.paper.event.world.StructuresLocateEvent;
|
||||
import io.papermc.paper.event.server.ServerResourcesReloadedEvent;
|
||||
import io.papermc.paper.math.Position;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.Container;
|
||||
import org.bukkit.block.Chest;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.world.LootGenerateEvent;
|
||||
import org.bukkit.generator.structure.Structure;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import org.bukkit.util.StructureSearchResult;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public final class TreasureLootChecks implements Listener {
|
||||
private static final NamespacedKey WORLD_KEY = new NamespacedKey("iris", "treasure_map_probe");
|
||||
private static final List<String> CHESTS = List.of(
|
||||
"CHEST", "COPPER_CHEST", "EXPOSED_COPPER_CHEST", "WEATHERED_COPPER_CHEST",
|
||||
"OXIDIZED_COPPER_CHEST", "WAXED_COPPER_CHEST", "WAXED_EXPOSED_COPPER_CHEST",
|
||||
"WAXED_WEATHERED_COPPER_CHEST", "WAXED_OXIDIZED_COPPER_CHEST");
|
||||
private final IrisVanillaLootTable reloadTable = new IrisVanillaLootTable("iris_qa:reload");
|
||||
private final AtomicInteger resourceReloads = new AtomicInteger();
|
||||
private LocateAction locateAction = LocateAction.PASS;
|
||||
private int locateEvents;
|
||||
private int lootEvents;
|
||||
private boolean lootEventsOwned = true;
|
||||
|
||||
public TreasureLootChecks(JavaPlugin plugin) {
|
||||
Bukkit.getPluginManager().registerEvents(this, plugin);
|
||||
}
|
||||
|
||||
public void verify(CommandSender sender) {
|
||||
World world = requireWorld();
|
||||
verifyCopperLoot(world);
|
||||
sender.sendMessage("TREASURE_COPPER_LOOT variants=8 normal=true diamonds=1");
|
||||
verifyInventoryRollback(world);
|
||||
sender.sendMessage("TREASURE_LOOT_ROLLBACK contents=true nativeTable=true seed=true retry=true");
|
||||
verifyLocateEvents(world);
|
||||
sender.sendMessage("TREASURE_LOCATE_EVENTS cancelled=true replaced=true calls=2");
|
||||
sender.sendMessage("TREASURE_EXTRA_CHECK variants=9 rollback=true locateEvents=true");
|
||||
}
|
||||
|
||||
public void verifyRegion(CommandSender sender) {
|
||||
World world = requireWorld();
|
||||
if (!J.isOwnedByCurrentRegion(world, 0, 0)) {
|
||||
throw new IllegalStateException("Fixture does not own its loot region");
|
||||
}
|
||||
lootEvents = 0;
|
||||
lootEventsOwned = true;
|
||||
verifyCopperLoot(world);
|
||||
if (lootEvents != CHESTS.size() || !lootEventsOwned) {
|
||||
throw new IllegalStateException("Loot event ownership failed: count=" + lootEvents
|
||||
+ " owned=" + lootEventsOwned);
|
||||
}
|
||||
sender.sendMessage("TREASURE_REGION_LOOT copper=8 normal=1 events=" + lootEvents
|
||||
+ " owned=true folia=" + J.isFolia());
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onLoot(LootGenerateEvent event) {
|
||||
if (event.getWorld().getKey().equals(WORLD_KEY) && event.isPlugin()) {
|
||||
Location origin = event.getLootContext().getLocation();
|
||||
lootEvents++;
|
||||
lootEventsOwned &= J.isOwnedByCurrentRegion(event.getWorld(),
|
||||
origin.getBlockX() >> 4, origin.getBlockZ() >> 4);
|
||||
}
|
||||
}
|
||||
|
||||
public void verifyReload(CommandSender sender, boolean reloaded) {
|
||||
World world = requireWorld();
|
||||
Material expected = reloaded ? Material.EMERALD : Material.DIAMOND;
|
||||
Collection<ItemStack> items = reloadTable.getLoot(
|
||||
false, 783L, InventorySlotType.STORAGE, world, 2, 161, 2);
|
||||
if (items.size() != 1 || items.iterator().next().getType() != expected) {
|
||||
throw new IllegalStateException("Reloaded loot did not contain " + expected + ": " + items);
|
||||
}
|
||||
sender.sendMessage("TREASURE_RELOAD material=" + expected);
|
||||
}
|
||||
|
||||
public void reportResourceReloads(CommandSender sender) {
|
||||
sender.sendMessage("TREASURE_RESOURCES revision=" + resourceReloads.get());
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onResourcesReloaded(ServerResourcesReloadedEvent event) {
|
||||
resourceReloads.incrementAndGet();
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onLocate(StructuresLocateEvent event) {
|
||||
if (!event.getWorld().getKey().equals(WORLD_KEY) || locateAction == LocateAction.PASS) {
|
||||
return;
|
||||
}
|
||||
locateEvents++;
|
||||
if (locateAction == LocateAction.CANCEL) {
|
||||
event.setCancelled(true);
|
||||
} else {
|
||||
event.setResult(new StructuresLocateEvent.Result(Position.block(123, 100, 456), Structure.DESERT_PYRAMID));
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyCopperLoot(World world) {
|
||||
Engine engine = IrisToolbelt.access(world).getEngine();
|
||||
Block block = world.getBlockAt(4, 161, 4);
|
||||
MantleChunk<Matter> mantle = engine.getMantle().getMantle().useChunk(block.getChunk());
|
||||
try {
|
||||
for (String name : CHESTS) {
|
||||
block.setType(Material.AIR, false);
|
||||
block.setType(Material.valueOf(name), false);
|
||||
if (!BukkitBlockResolution.isStorageChest(block.getBlockData())) {
|
||||
throw new IllegalStateException("Chest is excluded from Iris loot: " + name);
|
||||
}
|
||||
EngineBukkitOps.update(engine, 4, 161, 4, block.getChunk(), mantle);
|
||||
Container chest = (Container) block.getState();
|
||||
int diamonds = 0;
|
||||
for (ItemStack item : chest.getInventory().getContents()) {
|
||||
if (item == null) {
|
||||
continue;
|
||||
}
|
||||
if (item.getType() != Material.DIAMOND) {
|
||||
throw new IllegalStateException("Unexpected " + name + " loot: " + item);
|
||||
}
|
||||
diamonds += item.getAmount();
|
||||
}
|
||||
if (diamonds != 1) {
|
||||
throw new IllegalStateException(name + " contains " + diamonds + " diamonds");
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
mantle.release();
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyInventoryRollback(World world) {
|
||||
Engine engine = IrisToolbelt.access(world).getEngine();
|
||||
Block block = world.getBlockAt(6, 161, 4);
|
||||
block.setType(Material.AIR, false);
|
||||
block.setType(Material.CHEST, false);
|
||||
Chest chest = (Chest) block.getState();
|
||||
chest.getBlockInventory().setItem(0, new ItemStack(Material.EMERALD, 3));
|
||||
chest = (Chest) block.getState();
|
||||
NamespacedKey key = new NamespacedKey("iris_qa", "reload");
|
||||
chest.setLootTable(Bukkit.getLootTable(key));
|
||||
chest.setSeed(913L);
|
||||
if (!chest.update(true, false)) {
|
||||
throw new IllegalStateException("Could not set native loot for rollback check");
|
||||
}
|
||||
Inventory inventory = chest.getBlockInventory();
|
||||
IllegalStateException injected = new IllegalStateException("Fixture insertion interruption");
|
||||
Inventory interrupted = (Inventory) Proxy.newProxyInstance(Inventory.class.getClassLoader(),
|
||||
new Class<?>[]{Inventory.class}, (proxy, method, arguments) -> {
|
||||
Object result;
|
||||
try {
|
||||
result = method.invoke(inventory, arguments);
|
||||
} catch (InvocationTargetException failure) {
|
||||
throw failure.getCause();
|
||||
}
|
||||
if (method.getName().equals("addItem")) {
|
||||
throw injected;
|
||||
}
|
||||
return result;
|
||||
});
|
||||
KList<IrisLootTable> tables = new KList<>();
|
||||
tables.add(new IrisVanillaLootTable("iris_qa:reload"));
|
||||
try {
|
||||
EngineBukkitOps.addItems(engine, false, interrupted, tables, InventorySlotType.STORAGE,
|
||||
world, 6, 161, 4);
|
||||
throw new IllegalStateException("Interrupted insertion did not fail");
|
||||
} catch (IllegalStateException failure) {
|
||||
if (failure != injected) {
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
Chest restored = (Chest) block.getState();
|
||||
if (restored.getLootTable() == null || !restored.getLootTable().getKey().equals(key)
|
||||
|| restored.getSeed() != 913L) {
|
||||
throw new IllegalStateException("Rollback lost the native loot table or seed");
|
||||
}
|
||||
ItemStack existing = restored.getSnapshotInventory().getItem(0);
|
||||
if (existing == null || existing.getType() != Material.EMERALD || existing.getAmount() != 3) {
|
||||
throw new IllegalStateException("Rollback lost the original contents: " + existing);
|
||||
}
|
||||
EngineBukkitOps.addItems(engine, false, restored.getBlockInventory(), tables, InventorySlotType.STORAGE,
|
||||
world, 6, 161, 4);
|
||||
int diamonds = 0;
|
||||
int emeralds = 0;
|
||||
for (ItemStack item : restored.getBlockInventory().getContents()) {
|
||||
if (item == null) {
|
||||
continue;
|
||||
}
|
||||
if (item.getType() == Material.DIAMOND) {
|
||||
diamonds += item.getAmount();
|
||||
} else if (item.getType() == Material.EMERALD) {
|
||||
emeralds += item.getAmount();
|
||||
}
|
||||
}
|
||||
if (diamonds != 2 || emeralds != 3 || ((Chest) block.getState()).getLootTable() != null) {
|
||||
throw new IllegalStateException("Loot retry failed: diamonds=" + diamonds + " emeralds=" + emeralds);
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyLocateEvents(World world) {
|
||||
Location origin = new Location(world, 2, 161, 2);
|
||||
locateEvents = 0;
|
||||
try {
|
||||
locateAction = LocateAction.CANCEL;
|
||||
if (world.locateNearestStructure(origin, Structure.DESERT_PYRAMID, 50, false) != null) {
|
||||
throw new IllegalStateException("Cancelled structure search returned a target");
|
||||
}
|
||||
locateAction = LocateAction.REPLACE;
|
||||
StructureSearchResult result = world.locateNearestStructure(origin, Structure.DESERT_PYRAMID, 50, false);
|
||||
if (result == null || result.getStructure() != Structure.DESERT_PYRAMID
|
||||
|| result.getLocation().getBlockX() != 123 || result.getLocation().getBlockY() != 100
|
||||
|| result.getLocation().getBlockZ() != 456 || locateEvents != 2) {
|
||||
throw new IllegalStateException("Structure search ignored event result: " + result);
|
||||
}
|
||||
} finally {
|
||||
locateAction = LocateAction.PASS;
|
||||
}
|
||||
}
|
||||
|
||||
private World requireWorld() {
|
||||
World world = Bukkit.getWorld(WORLD_KEY);
|
||||
if (world == null || !IrisToolbelt.isIrisWorld(world)) {
|
||||
throw new IllegalStateException("Treasure map fixture world is absent");
|
||||
}
|
||||
return world;
|
||||
}
|
||||
|
||||
private enum LocateAction {
|
||||
PASS, CANCEL, REPLACE
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"type": "minecraft:chest",
|
||||
"pools": [
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:map",
|
||||
"functions": [
|
||||
{
|
||||
"destination": "iris_qa:blocked",
|
||||
"decoration": "minecraft:red_x",
|
||||
"zoom": 0,
|
||||
"search_radius": 50,
|
||||
"skip_existing_chunks": false,
|
||||
"function": "minecraft:exploration_map"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:diamond"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"type": "minecraft:chest",
|
||||
"pools": [
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:map",
|
||||
"functions": [
|
||||
{
|
||||
"destination": "iris_qa:disabled",
|
||||
"decoration": "minecraft:red_x",
|
||||
"zoom": 0,
|
||||
"search_radius": 50,
|
||||
"skip_existing_chunks": false,
|
||||
"function": "minecraft:exploration_map"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:diamond"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"type": "minecraft:chest",
|
||||
"pools": [
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:map",
|
||||
"functions": [
|
||||
{
|
||||
"destination": "iris_qa:found",
|
||||
"decoration": "minecraft:red_x",
|
||||
"zoom": 0,
|
||||
"search_radius": 50,
|
||||
"skip_existing_chunks": false,
|
||||
"function": "minecraft:exploration_map"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:diamond"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"type": "minecraft:chest",
|
||||
"pools": [
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:map",
|
||||
"functions": [
|
||||
{
|
||||
"destination": "iris_qa:mixed",
|
||||
"decoration": "minecraft:red_x",
|
||||
"zoom": 0,
|
||||
"search_radius": 50,
|
||||
"skip_existing_chunks": false,
|
||||
"function": "minecraft:exploration_map"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:diamond"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "minecraft:chest",
|
||||
"pools": [
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:diamond"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"values": [
|
||||
"minecraft:buried_treasure"
|
||||
]
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"values": [
|
||||
"minecraft:mansion"
|
||||
]
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"values": [
|
||||
"minecraft:desert_pyramid"
|
||||
]
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"values": [
|
||||
"minecraft:buried_treasure",
|
||||
"minecraft:desert_pyramid"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"pack": {
|
||||
"description": "Treasure map gameplay fixtures",
|
||||
"min_format": [
|
||||
107,
|
||||
1
|
||||
],
|
||||
"max_format": [
|
||||
107,
|
||||
1
|
||||
]
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"type": "minecraft:chest",
|
||||
"pools": [
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:filled_map",
|
||||
"modifier": {
|
||||
"destination": "#iris_qa:blocked",
|
||||
"decoration": "minecraft:red_x",
|
||||
"zoom": 0,
|
||||
"search_radius": 50,
|
||||
"skip_existing_chunks": false,
|
||||
"type": "minecraft:exploration_map"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:diamond"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"type": "minecraft:chest",
|
||||
"pools": [
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:filled_map",
|
||||
"modifier": {
|
||||
"destination": "#iris_qa:disabled",
|
||||
"decoration": "minecraft:red_x",
|
||||
"zoom": 0,
|
||||
"search_radius": 50,
|
||||
"skip_existing_chunks": false,
|
||||
"type": "minecraft:exploration_map"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:diamond"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"type": "minecraft:chest",
|
||||
"pools": [
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:filled_map",
|
||||
"modifier": {
|
||||
"destination": "#iris_qa:found",
|
||||
"decoration": "minecraft:red_x",
|
||||
"zoom": 0,
|
||||
"search_radius": 50,
|
||||
"skip_existing_chunks": false,
|
||||
"type": "minecraft:exploration_map"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:diamond"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"type": "minecraft:chest",
|
||||
"pools": [
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:filled_map",
|
||||
"modifier": {
|
||||
"destination": "#iris_qa:mixed",
|
||||
"decoration": "minecraft:red_x",
|
||||
"zoom": 0,
|
||||
"search_radius": 50,
|
||||
"skip_existing_chunks": false,
|
||||
"type": "minecraft:exploration_map"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:diamond"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "minecraft:chest",
|
||||
"pools": [
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:diamond"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"values": [
|
||||
"minecraft:buried_treasure"
|
||||
]
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"values": [
|
||||
"minecraft:mansion"
|
||||
]
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"values": [
|
||||
"minecraft:desert_pyramid"
|
||||
]
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"values": [
|
||||
"minecraft:buried_treasure",
|
||||
"minecraft:desert_pyramid"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"pack": {
|
||||
"description": "Treasure map gameplay fixtures",
|
||||
"min_format": [
|
||||
121,
|
||||
0
|
||||
],
|
||||
"max_format": [
|
||||
121,
|
||||
0
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "Flat",
|
||||
"derivative": "minecraft:plains",
|
||||
"vanillaDerivative": "minecraft:plains",
|
||||
"layers": [
|
||||
{
|
||||
"minHeight": 1,
|
||||
"maxHeight": 1,
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:lime_wool"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"minHeight": 384,
|
||||
"maxHeight": 384,
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:green_concrete"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"generators": [
|
||||
{
|
||||
"generator": "flat",
|
||||
"min": 96,
|
||||
"max": 96
|
||||
}
|
||||
]
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"name": "Treasure Map Fixture",
|
||||
"version": 1,
|
||||
"mode": {
|
||||
"type": "OVERWORLD"
|
||||
},
|
||||
"regions": [
|
||||
"flat"
|
||||
],
|
||||
"focusRegion": "flat",
|
||||
"focus": "flat",
|
||||
"landChance": 1,
|
||||
"environment": "NORMAL",
|
||||
"dimensionHeight": {
|
||||
"min": -64,
|
||||
"max": 320
|
||||
},
|
||||
"logicalHeight": 384,
|
||||
"fluidHeight": 0,
|
||||
"bedrock": false,
|
||||
"rockPalette": {
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:red_concrete"
|
||||
}
|
||||
]
|
||||
},
|
||||
"importedStructures": {
|
||||
"disabled": [
|
||||
"minecraft:"
|
||||
]
|
||||
},
|
||||
"structures": [
|
||||
{
|
||||
"placementId": "blocked-treasure",
|
||||
"nativeStructures": [
|
||||
{
|
||||
"structure": "minecraft:buried_treasure"
|
||||
}
|
||||
],
|
||||
"distribution": "RANDOM_SPREAD",
|
||||
"spacing": 1,
|
||||
"separation": 0,
|
||||
"minHeight": 0,
|
||||
"maxHeight": 0
|
||||
},
|
||||
{
|
||||
"placementId": "reachable-pyramid",
|
||||
"nativeStructures": [
|
||||
{
|
||||
"structure": "minecraft:desert_pyramid"
|
||||
}
|
||||
],
|
||||
"distribution": "RANDOM_SPREAD",
|
||||
"spacing": 16,
|
||||
"separation": 4
|
||||
}
|
||||
],
|
||||
"loot": {
|
||||
"tables": [
|
||||
"fixture-diamond"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"interpolator": {
|
||||
"function": "NONE",
|
||||
"horizontalScale": 1
|
||||
},
|
||||
"seed": 310,
|
||||
"composite": [
|
||||
{
|
||||
"seed": 310,
|
||||
"style": {
|
||||
"style": "FLAT"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "Fixture Diamond",
|
||||
"rarity": 1,
|
||||
"minPicked": 1,
|
||||
"maxPicked": 1,
|
||||
"maxTries": 1,
|
||||
"loot": [
|
||||
{
|
||||
"type": "diamond",
|
||||
"rarity": 1,
|
||||
"minAmount": 1,
|
||||
"maxAmount": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "Flat",
|
||||
"landBiomes": [
|
||||
"flat"
|
||||
],
|
||||
"seaBiomes": [
|
||||
"flat"
|
||||
],
|
||||
"shoreBiomes": [
|
||||
"flat"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import { readFile, writeFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
|
||||
const simplifyNbt = value => {
|
||||
if (Array.isArray(value)) return value.map(simplifyNbt)
|
||||
if (value === null || typeof value !== 'object') return value
|
||||
if ('type' in value && 'value' in value) return simplifyNbt(value.value)
|
||||
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, simplifyNbt(entry)]))
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'iris-treasure-maps',
|
||||
description: 'Verify exploration-map targets, bounded misses, retained loot, and normal/copper storage on Iris.',
|
||||
async run(context) {
|
||||
context.bot.physicsEnabled = false
|
||||
context.bot.clearControlStates()
|
||||
context.bot.entity.velocity.set(0, 0, 0)
|
||||
const source = await readFile(path.join(context.server.directory, '.server-source'), 'utf8')
|
||||
context.expect(/^isolated=true\s*$/m.test(source), 'Treasure map fixtures require an isolated instance')
|
||||
const command = async (operation, expected, timeoutMs = 30000) => {
|
||||
const token = expected.split(' ')[0]
|
||||
const response = await context.command(`/irislifecycle ${operation}`,
|
||||
new RegExp(`${token}|NATIVE_FAILED|TREASURE_.*_FAILED`), timeoutMs)
|
||||
context.expect(response.includes(expected), response)
|
||||
return response
|
||||
}
|
||||
const openChest = async material => {
|
||||
const position = context.bot.entity.position.clone().set(2, 161, 2)
|
||||
await context.waitUntil(() => context.bot.blockAt(position)?.name === material, {
|
||||
label: `${material} reached the client`, timeoutMs: 10000
|
||||
})
|
||||
const opened = context.waitForEvent('windowOpen', undefined, 120000)
|
||||
await context.bot.activateBlock(context.bot.blockAt(position))
|
||||
const [window] = await opened
|
||||
return window
|
||||
}
|
||||
const chestItems = window => window.slots.slice(0, window.inventoryStart).filter(Boolean)
|
||||
const mapId = item => item.componentMap?.get('map_id')?.data
|
||||
let missItem
|
||||
await context.step('create and enter the Iris treasure map world', async () => {
|
||||
const response = await command('treasure-map-create', 'TREASURE_MAP_READY world=iris:treasure_map_probe', 180000)
|
||||
const version = response.match(/version=(\S+)/)?.[1]
|
||||
context.expect(/^26\.[23](?:[.-]|$)/.test(version ?? ''), 'Fixture requires its matching 26.2 or 26.3 data pack', { version })
|
||||
missItem = version.startsWith('26.2') ? 'map' : 'filled_map'
|
||||
context.report.treasurePlatform = { version, folia: response.includes('folia=true'), missItem }
|
||||
await context.waitUntil(() => context.bot.entity.position.distanceTo(
|
||||
context.bot.entity.position.clone().set(0.5, 161, 0.5)
|
||||
) < 2, { label: 'Fixture teleport reached the client', timeoutMs: 30000 })
|
||||
})
|
||||
await context.step('confirm the missing target exhausts the placement search budget', async () => {
|
||||
await command('treasure-map-limit', 'TREASURE_MAP_LIMIT status=SEARCH_LIMIT_REACHED', 120000)
|
||||
})
|
||||
for (const material of ['chest', 'copper_chest']) {
|
||||
for (const destination of ['blocked', 'disabled', 'found', 'mixed']) {
|
||||
await context.step(`open ${material} with ${destination} map target and retain all loot`, async () => {
|
||||
const located = destination === 'found' || destination === 'mixed'
|
||||
const mapName = located ? 'filled_map' : missItem
|
||||
await command(`treasure-map-chest ${destination} ${material.toUpperCase()}`,
|
||||
`TREASURE_MAP_CHEST destination=${destination} material=${material.toUpperCase()}`)
|
||||
const window = await openChest(material)
|
||||
let targetId
|
||||
let diamondType
|
||||
let diamondsBefore
|
||||
let marker
|
||||
let verified
|
||||
try {
|
||||
await context.waitUntil(() => {
|
||||
const items = chestItems(window)
|
||||
return items.some(item => item.name === mapName) && items.some(item => item.name === 'diamond')
|
||||
}, { label: `${destination} chest contains map and diamond`, timeoutMs: 30000 })
|
||||
const items = chestItems(window)
|
||||
const map = items.find(item => item.name === mapName)
|
||||
const diamond = items.find(item => item.name === 'diamond')
|
||||
targetId = mapId(map)
|
||||
context.expect(items.length === 2 && map.count === 1 && diamond.count === 1,
|
||||
'Loot table must produce exactly one map and one diamond')
|
||||
context.expect((targetId !== undefined) === located, 'Only reachable destinations receive map data', {
|
||||
destination, components: map.components
|
||||
})
|
||||
const response = await command(`treasure-map-inspect ${destination}`,
|
||||
`TREASURE_MAP_INSPECT destination=${destination} diamonds=1 located=${located}`, 120000)
|
||||
verified = Object.fromEntries([...response.matchAll(/(mapId|targetX|targetZ|centerX|centerZ)=(-?\d+)/g)]
|
||||
.map(([, key, value]) => [key, Number(value)]))
|
||||
const decorations = simplifyNbt(map.componentMap?.get('map_decorations')?.data)
|
||||
marker = decorations?.['+']
|
||||
if (located) {
|
||||
context.expect(targetId === verified.mapId, 'Client and saved map IDs agree', { targetId, verified })
|
||||
context.expect(marker?.type === 'minecraft:red_x'
|
||||
&& marker.x === verified.targetX && marker.z === verified.targetZ,
|
||||
'Map marker points to the verified pyramid', { marker, verified, components: map.components })
|
||||
} else {
|
||||
context.expect(marker === undefined, 'Missing destinations must not invent a map marker', { decorations })
|
||||
}
|
||||
diamondType = diamond.type
|
||||
diamondsBefore = context.bot.inventory.count(diamondType)
|
||||
await context.bot.clickWindow(diamond.slot, 0, 1)
|
||||
await context.waitUntil(() => chestItems(window).every(item => item.name !== 'diamond'), {
|
||||
label: 'Diamond transferred out of the chest', timeoutMs: 10000
|
||||
})
|
||||
} finally {
|
||||
context.bot.closeWindow(window)
|
||||
}
|
||||
const reopened = await openChest(material)
|
||||
try {
|
||||
await context.waitUntil(() => chestItems(reopened).some(item => item.name === mapName), {
|
||||
label: 'Saved map remains in the reopened chest', timeoutMs: 10000
|
||||
})
|
||||
const retained = chestItems(reopened)
|
||||
context.expect(retained.length === 1 && retained[0].name === mapName && retained[0].count === 1,
|
||||
'Reopening must not regenerate chest loot')
|
||||
context.expect(mapId(retained[0]) === targetId, 'Reopening must retain the same map data')
|
||||
context.expect(JSON.stringify(simplifyNbt(retained[0].componentMap?.get('map_decorations')?.data)?.['+'])
|
||||
=== JSON.stringify(marker), 'Reopening must retain the same destination marker')
|
||||
await context.waitUntil(() => context.bot.inventory.count(diamondType) === diamondsBefore + 1, {
|
||||
label: 'Chest awards exactly one diamond', timeoutMs: 10000
|
||||
})
|
||||
context.report.treasureMaps ??= []
|
||||
context.report.treasureMaps.push({ destination, material, mapItem: mapName,
|
||||
mapId: targetId ?? null, marker: marker ?? null, verified, reopened: true })
|
||||
} finally {
|
||||
context.bot.closeWindow(reopened)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
await context.step('verify storage variants, insertion rollback, and locate event results', async () => {
|
||||
await command('treasure-extra-check', 'TREASURE_EXTRA_CHECK variants=9 rollback=true locateEvents=true', 120000)
|
||||
})
|
||||
await context.step('verify plugin loot runs on its owning region', async () => {
|
||||
await command('treasure-region-check', 'TREASURE_REGION_LOOT copper=8 normal=1 events=9 owned=true', 120000)
|
||||
})
|
||||
await context.step('reload a data pack without reusing the previous native loot table', async () => {
|
||||
const properties = await readFile(path.join(context.server.directory, 'server.properties'), 'utf8')
|
||||
const levelName = properties.match(/^level-name=(.+)$/m)?.[1].trim() ?? 'world'
|
||||
const tablePath = path.join(context.server.directory, levelName, 'datapacks', 'iris_treasure',
|
||||
'data', 'iris_qa', 'loot_table', 'reload.json')
|
||||
const original = await readFile(tablePath, 'utf8')
|
||||
const replacement = JSON.parse(original)
|
||||
context.expect(replacement.pools[0].entries[0].name === 'minecraft:diamond',
|
||||
'Reload fixture must start with its diamond table')
|
||||
const revision = async () => {
|
||||
const response = await command('treasure-resources', 'TREASURE_RESOURCES revision=')
|
||||
return Number(response.match(/revision=(\d+)/)?.[1])
|
||||
}
|
||||
const reload = async () => {
|
||||
const before = await revision()
|
||||
await context.command('/minecraft:reload', /Reloading/i, 30000)
|
||||
await context.waitUntil(async () => await revision() > before, {
|
||||
label: 'Server resource reload completed', timeoutMs: 120000, intervalMs: 500
|
||||
})
|
||||
}
|
||||
await command('treasure-reload-before', 'TREASURE_RELOAD material=DIAMOND')
|
||||
replacement.pools[0].entries[0].name = 'minecraft:emerald'
|
||||
try {
|
||||
await writeFile(tablePath, `${JSON.stringify(replacement, null, 2)}\n`)
|
||||
await reload()
|
||||
await command('treasure-reload-after', 'TREASURE_RELOAD material=EMERALD')
|
||||
context.report.treasureReload = { before: 'diamond', after: 'emerald' }
|
||||
} finally {
|
||||
await writeFile(tablePath, original)
|
||||
if (!context.signal.aborted) {
|
||||
await reload()
|
||||
await command('treasure-reload-before', 'TREASURE_RELOAD material=DIAMOND')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user