mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 14:00:26 +00:00
Merged treasure loot fixes
- Failed container fills retain pending work for retry - Loot events and inventory writes run on the owning thread - Native loot tables resolve against the current registry - Gameplay checks cover map destinations and copper containers
This commit is contained in:
commit
3246e88f88
43 files changed
+2049
-261
No files matched your search
@@ -81,7 +81,6 @@ import org.bukkit.FluidCollisionMode;
|
|||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.event.inventory.InventoryType;
|
|
||||||
import org.bukkit.inventory.Inventory;
|
import org.bukkit.inventory.Inventory;
|
||||||
import org.bukkit.util.Vector;
|
import org.bukkit.util.Vector;
|
||||||
|
|
||||||
@@ -309,23 +308,33 @@ public class CommandStudio implements DirectorExecutor {
|
|||||||
O<Integer> ta = new O<>();
|
O<Integer> ta = new O<>();
|
||||||
ta.set(-1);
|
ta.set(-1);
|
||||||
|
|
||||||
var sender = sender();
|
VolmitSender sender = sender();
|
||||||
var player = player();
|
Player player = player();
|
||||||
var engine = engine();
|
Engine engine = engine();
|
||||||
|
|
||||||
ta.set(J.sr(() ->
|
ta.set(J.sr(() -> {
|
||||||
{
|
if (!J.runEntity(player, () -> {
|
||||||
if (!player.getOpenInventory().getType().equals(InventoryType.CHEST)) {
|
if (player.getOpenInventory().getTopInventory() != inv) {
|
||||||
|
J.csr(ta.get());
|
||||||
|
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_OPENED_INVENTORY));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!add) {
|
||||||
|
inv.clear();
|
||||||
|
}
|
||||||
|
Location location = player.getLocation();
|
||||||
|
try {
|
||||||
|
EngineBukkitOps.addItems(engine, true, inv, tables, InventorySlotType.STORAGE,
|
||||||
|
location.getWorld(), location.getBlockX(), location.getBlockY(), location.getBlockZ());
|
||||||
|
} catch (RuntimeException | Error failure) {
|
||||||
|
J.csr(ta.get());
|
||||||
|
Iris.reportError(failure);
|
||||||
|
sender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_CANNOT_ADD_ITEMS_VIRTUAL_INVENTORY_BECAUSE,
|
||||||
|
MessageArgument.untrusted("value", String.valueOf(failure.getMessage()))));
|
||||||
|
}
|
||||||
|
}, 0, () -> J.csr(ta.get()))) {
|
||||||
J.csr(ta.get());
|
J.csr(ta.get());
|
||||||
sender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_OPENED_INVENTORY));
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!add) {
|
|
||||||
inv.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
EngineBukkitOps.addItems(engine, true, inv, tables, InventorySlotType.STORAGE, player.getWorld(), player.getLocation().getBlockX(), player.getLocation().getBlockY(), player.getLocation().getBlockZ());
|
|
||||||
}, fast ? 5 : 35));
|
}, fast ? 5 : 35));
|
||||||
|
|
||||||
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_INVENTORY_NOW));
|
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_OPENING_INVENTORY_NOW));
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ name: IrisNativeLifecycleFixture
|
|||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
main: art.arcane.iris.fixture.NativeLifecycleFixture
|
main: art.arcane.iris.fixture.NativeLifecycleFixture
|
||||||
api-version: '26.1.2'
|
api-version: '26.1.2'
|
||||||
|
folia-supported: true
|
||||||
depend: [Iris]
|
depend: [Iris]
|
||||||
commands:
|
commands:
|
||||||
irislifecycle:
|
irislifecycle:
|
||||||
|
|||||||
+213
-6
@@ -1,8 +1,12 @@
|
|||||||
package art.arcane.iris.fixture;
|
package art.arcane.iris.fixture;
|
||||||
|
|
||||||
|
import art.arcane.iris.Iris;
|
||||||
import art.arcane.volmlib.nativelib.NativeAdapters;
|
import art.arcane.volmlib.nativelib.NativeAdapters;
|
||||||
import art.arcane.iris.world.runtime.RuntimeInjection;
|
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.IrisCreator;
|
||||||
|
import art.arcane.iris.world.task.J;
|
||||||
import art.arcane.iris.world.IrisToolbelt;
|
import art.arcane.iris.world.IrisToolbelt;
|
||||||
import art.arcane.iris.world.IrisWorldStorage;
|
import art.arcane.iris.world.IrisWorldStorage;
|
||||||
import art.arcane.iris.world.WorldCreatorCompat;
|
import art.arcane.iris.world.WorldCreatorCompat;
|
||||||
@@ -13,6 +17,15 @@ import org.bukkit.entity.Player;
|
|||||||
import org.bukkit.configuration.file.YamlConfiguration;
|
import org.bukkit.configuration.file.YamlConfiguration;
|
||||||
import org.bukkit.Material;
|
import org.bukkit.Material;
|
||||||
import org.bukkit.block.Biome;
|
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.NativeTerrainAccess;
|
||||||
import art.arcane.volmlib.nativelib.terrain.NativeStructureReader;
|
import art.arcane.volmlib.nativelib.terrain.NativeStructureReader;
|
||||||
import art.arcane.volmlib.nativelib.terrain.NativeWorldRuntime;
|
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 WORLD_KEY = NamespacedKey.minecraft("native_lifecycle_probe");
|
||||||
private static final NamespacedKey TERRAIN_KEY = new NamespacedKey("iris", "native_terrain_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 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 static final long WORLD_SEED = 78264193L;
|
||||||
private NativeWorldRuntime runtime;
|
private NativeWorldRuntime runtime;
|
||||||
|
private TreasureLootChecks treasureLootChecks;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
runtime = NativeAdapters.require(NativeWorldRuntime.class);
|
runtime = NativeAdapters.require(NativeWorldRuntime.class);
|
||||||
|
treasureLootChecks = new TreasureLootChecks(this);
|
||||||
NativeTerrainAccess terrain = NativeAdapters.require(NativeTerrainAccess.class);
|
NativeTerrainAccess terrain = NativeAdapters.require(NativeTerrainAccess.class);
|
||||||
getLogger().info("Native lifecycle ready: " + runtime.description()
|
getLogger().info("Native lifecycle ready: " + runtime.description()
|
||||||
+ "; terrain provider=" + terrain.getClass().getName());
|
+ "; terrain provider=" + terrain.getClass().getName());
|
||||||
@@ -56,12 +72,32 @@ public final class NativeLifecycleFixture extends JavaPlugin implements WorldRun
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] arguments) {
|
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");
|
sender.sendMessage("Use /irislifecycle create|unload|status|structures|terrain-create|terrain-check|terrain-unload|lifecycle-status|lifecycle-entry");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
switch (arguments[0]) {
|
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 "create" -> create(sender);
|
||||||
case "unload" -> unload(sender);
|
case "unload" -> unload(sender);
|
||||||
case "structures" -> structures(sender);
|
case "structures" -> structures(sender);
|
||||||
@@ -80,6 +116,177 @@ public final class NativeLifecycleFixture extends JavaPlugin implements WorldRun
|
|||||||
return true;
|
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 {
|
private void lifecycleStatus(CommandSender sender) throws Exception {
|
||||||
World world = Bukkit.getWorld(COMMAND_KEY);
|
World world = Bukkit.getWorld(COMMAND_KEY);
|
||||||
Path storage = WorldCreatorCompat.persistentLevelRoot(COMMAND_KEY).toPath();
|
Path storage = WorldCreatorCompat.persistentLevelRoot(COMMAND_KEY).toPath();
|
||||||
@@ -282,11 +489,11 @@ public final class NativeLifecycleFixture extends JavaPlugin implements WorldRun
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean regionized() {
|
public boolean regionized() {
|
||||||
return false;
|
return J.isFolia();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean primaryThread() {
|
public boolean primaryThread() {
|
||||||
return Bukkit.isPrimaryThread();
|
return J.isFolia() ? Bukkit.isGlobalTickThread() : Bukkit.isPrimaryThread();
|
||||||
}
|
}
|
||||||
|
|
||||||
public CompletableFuture<Void> runGlobal(Runnable task) {
|
public CompletableFuture<Void> runGlobal(Runnable task) {
|
||||||
@@ -299,10 +506,10 @@ public final class NativeLifecycleFixture extends JavaPlugin implements WorldRun
|
|||||||
result.completeExceptionally(failure);
|
result.completeExceptionally(failure);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (Bukkit.isPrimaryThread()) {
|
if (primaryThread()) {
|
||||||
wrapped.run();
|
wrapped.run();
|
||||||
} else {
|
} else if (!J.runGlobal(wrapped)) {
|
||||||
Bukkit.getScheduler().runTask(this, wrapped);
|
result.completeExceptionally(new IllegalStateException("Could not schedule fixture task"));
|
||||||
}
|
}
|
||||||
return result;
|
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,211 @@
|
|||||||
|
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)]))
|
||||||
|
}
|
||||||
|
|
||||||
|
const completeClientTicks = context => {
|
||||||
|
const client = context.bot._client
|
||||||
|
context.expect(context.bot.supportFeature('sendsClientTickEndPacket'), 'Fixture client supports tick delimiters')
|
||||||
|
let tickTimer
|
||||||
|
const finishTick = () => {
|
||||||
|
clearTimeout(tickTimer)
|
||||||
|
client.write('tick_end', {})
|
||||||
|
tickTimer = setTimeout(finishTick, 50)
|
||||||
|
}
|
||||||
|
context.bot.on('move', finishTick)
|
||||||
|
finishTick()
|
||||||
|
context.signal.addEventListener('abort', () => {
|
||||||
|
clearTimeout(tickTimer)
|
||||||
|
context.bot.removeListener('move', finishTick)
|
||||||
|
}, { once: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'iris-treasure-maps',
|
||||||
|
description: 'Verify exploration-map targets, bounded misses, retained loot, and normal/copper storage on Iris.',
|
||||||
|
async run(context) {
|
||||||
|
completeClientTicks(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 bounded = (label, action) => context.waitUntil(async () => {
|
||||||
|
await action()
|
||||||
|
return true
|
||||||
|
}, { label, timeoutMs: 10000 })
|
||||||
|
let lastActivation = 0
|
||||||
|
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 delay = 150 - (performance.now() - lastActivation)
|
||||||
|
if (delay > 0) await context.sleep(delay)
|
||||||
|
const opened = context.waitForEvent('windowOpen', undefined, 30000)
|
||||||
|
await bounded(`Activate ${material}`, () => context.bot.activateBlock(context.bot.blockAt(position)))
|
||||||
|
lastActivation = performance.now()
|
||||||
|
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 evidence = { destination, material, phase: 'prepare' }
|
||||||
|
context.report.treasureMaps ??= []
|
||||||
|
context.report.treasureMaps.push(evidence)
|
||||||
|
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()}`)
|
||||||
|
evidence.phase = 'open'
|
||||||
|
const window = await openChest(material)
|
||||||
|
evidence.windowType = window.type
|
||||||
|
evidence.inventoryStart = window.inventoryStart
|
||||||
|
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)
|
||||||
|
evidence.phase = 'withdraw'
|
||||||
|
evidence.diamondSlot = diamond.slot
|
||||||
|
await bounded(`Withdraw diamond from ${material} ${destination}`,
|
||||||
|
() => context.bot.clickWindow(diamond.slot, 0, 1))
|
||||||
|
evidence.phase = 'withdrawn'
|
||||||
|
await context.waitUntil(() => chestItems(window).every(item => item.name !== 'diamond'), {
|
||||||
|
label: 'Diamond transferred out of the chest', timeoutMs: 10000
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
context.bot.closeWindow(window)
|
||||||
|
}
|
||||||
|
evidence.phase = 'reopen'
|
||||||
|
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
|
||||||
|
})
|
||||||
|
Object.assign(evidence, { phase: 'complete', 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('verify native loot table reload support', async () => {
|
||||||
|
await command('treasure-reload-before', 'TREASURE_RELOAD material=DIAMOND')
|
||||||
|
if (context.report.treasurePlatform.folia) {
|
||||||
|
const response = await context.command('/minecraft:reload', /Unknown or incomplete command|Reloading/i, 10000)
|
||||||
|
context.expect(response.includes('Unknown or incomplete command'),
|
||||||
|
'Folia fixture expects its unavailable data pack reload command', { response })
|
||||||
|
context.report.treasureReload = { status: 'unavailable', before: 'diamond', evidence: response }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
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 = { status: 'passed', before: 'diamond', after: 'emerald' }
|
||||||
|
} finally {
|
||||||
|
await writeFile(tablePath, original)
|
||||||
|
if (!context.signal.aborted) {
|
||||||
|
await reload()
|
||||||
|
await command('treasure-reload-before', 'TREASURE_RELOAD material=DIAMOND')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-11
@@ -30,7 +30,6 @@ import art.arcane.iris.studio.view.PregeneratorJob;
|
|||||||
import art.arcane.iris.integration.Identifier;
|
import art.arcane.iris.integration.Identifier;
|
||||||
import art.arcane.iris.generation.cache.Cache;
|
import art.arcane.iris.generation.cache.Cache;
|
||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
import art.arcane.iris.generation.runtime.GenerationFailures;
|
|
||||||
import art.arcane.iris.generation.block.TileData;
|
import art.arcane.iris.generation.block.TileData;
|
||||||
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||||
import art.arcane.iris.modded.ModdedBlockResolution;
|
import art.arcane.iris.modded.ModdedBlockResolution;
|
||||||
@@ -382,16 +381,7 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
|
|||||||
if (!state.isStorageChest()) {
|
if (!state.isStorageChest()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
ModdedLootApplier.apply(engine, level, pos, state, chunk);
|
||||||
ModdedLootApplier.apply(engine, level, pos, state, chunk);
|
|
||||||
} catch (SavedBiomeUnavailableException unavailable) {
|
|
||||||
GenerationFailures.rethrowEngineFailure(unavailable);
|
|
||||||
IrisLogging.warnOnce("chunk-update-loot:" + level.name() + ":" + unavailable.getMessage(),
|
|
||||||
"Iris left the container at " + pos + " in " + level.name() + " without loot: " + unavailable.getMessage());
|
|
||||||
} catch (Throwable e) {
|
|
||||||
GenerationFailures.rethrowEngineFailure(e);
|
|
||||||
IrisLogging.reportError(e);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
NativeWorldMaintenance.refresh(level, pos);
|
NativeWorldMaintenance.refresh(level, pos);
|
||||||
}
|
}
|
||||||
|
|||||||
+39
@@ -8,9 +8,12 @@ import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
|||||||
import art.arcane.volmlib.util.hunk.Hunk;
|
import art.arcane.volmlib.util.hunk.Hunk;
|
||||||
|
|
||||||
import net.minecraft.SharedConstants;
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
import net.minecraft.server.Bootstrap;
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.world.RandomizableContainer;
|
||||||
import net.minecraft.world.level.block.Block;
|
import net.minecraft.world.level.block.Block;
|
||||||
import net.minecraft.world.level.block.Blocks;
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.EntityBlock;
|
||||||
import net.minecraft.world.level.block.SpeleothemBlock;
|
import net.minecraft.world.level.block.SpeleothemBlock;
|
||||||
import net.minecraft.world.level.block.state.properties.SpeleothemThickness;
|
import net.minecraft.world.level.block.state.properties.SpeleothemThickness;
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
@@ -62,6 +65,42 @@ public class ModdedBlockResolutionContractTest {
|
|||||||
assertEquals(Blocks.OAK_LOG, state.handle().getBlock());
|
assertEquals(Blocks.OAK_LOG, state.handle().getBlock());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void copperChestVariantsReceiveIrisLootAndPostLoadUpdates() {
|
||||||
|
for (String key : new String[]{"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"}) {
|
||||||
|
ModdedBlockState state = ModdedBlockResolution.BLOCKS.getOrNull("minecraft:" + key);
|
||||||
|
assertNotNull(key, state);
|
||||||
|
assertTrue(key, state.isStorage());
|
||||||
|
assertTrue(key, state.isStorageChest());
|
||||||
|
assertTrue(key, NativeBlockProperties.isUpdatable(state.handle()));
|
||||||
|
assertTrue(key, ((EntityBlock) state.handle().getBlock()).newBlockEntity(BlockPos.ZERO, state.handle())
|
||||||
|
instanceof RandomizableContainer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void lootContainerClassificationPreservesExistingContainerBoundaries() {
|
||||||
|
for (String key : new String[]{"chest", "trapped_chest", "barrel", "shulker_box",
|
||||||
|
"blue_shulker_box", "dispenser", "dropper", "hopper"}) {
|
||||||
|
ModdedBlockState state = ModdedBlockResolution.BLOCKS.getOrNull("minecraft:" + key);
|
||||||
|
assertNotNull(key, state);
|
||||||
|
assertTrue(key, state.isStorage());
|
||||||
|
assertTrue(key, state.isStorageChest());
|
||||||
|
}
|
||||||
|
for (Block block : new Block[]{Blocks.FURNACE, Blocks.BLAST_FURNACE, Blocks.SMOKER}) {
|
||||||
|
assertTrue(block.toString(), NativeBlockProperties.isStorage(block.defaultBlockState()));
|
||||||
|
assertFalse(block.toString(), NativeBlockProperties.isStorageChest(block.defaultBlockState()));
|
||||||
|
}
|
||||||
|
for (String key : new String[]{"stone", "air", "ender_chest", "copper_block"}) {
|
||||||
|
ModdedBlockState state = ModdedBlockResolution.BLOCKS.getOrNull("minecraft:" + key);
|
||||||
|
assertNotNull(key, state);
|
||||||
|
assertFalse(key, state.isStorage());
|
||||||
|
assertFalse(key, state.isStorageChest());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void cactusPlacementRequiresNativeCactusSupport() {
|
public void cactusPlacementRequiresNativeCactusSupport() {
|
||||||
assertTrue(NativeBlockProperties.canPlaceOnto(Blocks.CACTUS, Blocks.CACTUS));
|
assertTrue(NativeBlockProperties.canPlaceOnto(Blocks.CACTUS, Blocks.CACTUS));
|
||||||
|
|||||||
+190
-13
@@ -1,33 +1,64 @@
|
|||||||
package art.arcane.iris.modded;
|
package art.arcane.iris.modded;
|
||||||
|
|
||||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeContainerLoot;
|
|
||||||
|
|
||||||
import art.arcane.iris.structure.placement.LootResolver;
|
import art.arcane.iris.structure.placement.LootResolver;
|
||||||
import art.arcane.iris.world.loot.IrisLootMode;
|
import art.arcane.iris.world.loot.IrisLootMode;
|
||||||
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeContainerLoot;
|
||||||
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeItemStack;
|
||||||
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockPoint;
|
||||||
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||||
|
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||||
import art.arcane.volmlib.util.math.RNG;
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
import net.minecraft.resources.Identifier;
|
import java.lang.reflect.Proxy;
|
||||||
import net.minecraft.resources.ResourceKey;
|
|
||||||
import net.minecraft.core.BlockPos;
|
|
||||||
import net.minecraft.core.registries.Registries;
|
|
||||||
import net.minecraft.world.RandomizableContainer;
|
|
||||||
import net.minecraft.world.SimpleContainer;
|
|
||||||
import net.minecraft.world.item.ItemStack;
|
|
||||||
import net.minecraft.world.level.storage.loot.LootTable;
|
|
||||||
import org.junit.Test;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.lang.reflect.Proxy;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.component.DataComponentMap;
|
||||||
|
import net.minecraft.core.component.DataComponents;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.world.CompoundContainer;
|
||||||
|
import net.minecraft.world.RandomizableContainer;
|
||||||
|
import net.minecraft.world.SimpleContainer;
|
||||||
|
import net.minecraft.world.entity.player.Player;
|
||||||
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
import net.minecraft.world.item.Items;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.ChestBlock;
|
||||||
|
import net.minecraft.world.level.block.entity.ChestBlockEntity;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.block.state.properties.ChestType;
|
||||||
|
import net.minecraft.world.level.storage.loot.LootTable;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNotNull;
|
||||||
import static org.junit.Assert.assertNull;
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
public class ModdedLootApplierTest {
|
public class ModdedLootApplierTest {
|
||||||
|
@BeforeClass
|
||||||
|
public static void bootstrapMinecraftRegistries() {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
if (!Items.DIAMOND.builtInRegistryHolder().areComponentsBound()) {
|
||||||
|
Items.DIAMOND.builtInRegistryHolder().bindComponents(DataComponentMap.EMPTY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void addAppendsSourcesInOrder() {
|
public void addAppendsSourcesInOrder() {
|
||||||
List<String> sources = new ArrayList<>(List.of("placement-native"));
|
List<String> sources = new ArrayList<>(List.of("placement-native"));
|
||||||
@@ -171,6 +202,152 @@ public class ModdedLootApplierTest {
|
|||||||
assertTrue(NativeContainerLoot.hasNativeLootTable(nativeLoot));
|
assertTrue(NativeContainerLoot.hasNativeLootTable(nativeLoot));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void failedFillRestoresContentsAndDoesNotDuplicateLootOnRetry() {
|
||||||
|
RejectingContainer container = new RejectingContainer();
|
||||||
|
container.setItem(0, diamondStack(2));
|
||||||
|
assertEquals(2, container.getItem(0).getCount());
|
||||||
|
container.reject = true;
|
||||||
|
ItemStack reward = diamondStack(7);
|
||||||
|
|
||||||
|
assertThrows(IllegalStateException.class,
|
||||||
|
() -> NativeContainerLoot.fillContainer(container, List.of(reward), new RNG(17), message -> {}));
|
||||||
|
|
||||||
|
assertEquals(2, container.getItem(0).getCount());
|
||||||
|
assertTrue(container.getItem(1).isEmpty());
|
||||||
|
assertEquals(7, reward.getCount());
|
||||||
|
NativeContainerLoot.fillContainer(container, List.of(reward), new RNG(17), message -> {});
|
||||||
|
assertEquals(9, container.getItem(0).getCount() + container.getItem(1).getCount());
|
||||||
|
assertEquals(7, reward.getCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void failedNativeUnpackRestoresTheLootTableSeedAndPriorContents() {
|
||||||
|
DeferredLootChest container = new DeferredLootChest(BlockPos.ZERO, Blocks.CHEST.defaultBlockState());
|
||||||
|
container.setItem(0, diamondStack(2));
|
||||||
|
container.reject = true;
|
||||||
|
ResourceKey<LootTable> table = ResourceKey.create(Registries.LOOT_TABLE, Identifier.parse("minecraft:chests/simple_dungeon"));
|
||||||
|
container.setLootTable(table, 41L);
|
||||||
|
|
||||||
|
assertThrows(IllegalStateException.class, () -> NativeContainerLoot.fillContainer(container,
|
||||||
|
List.of(new ItemStack(Items.DIAMOND)), new RNG(17), message -> {}));
|
||||||
|
|
||||||
|
assertSame(table, container.getLootTable());
|
||||||
|
assertEquals(41L, container.getLootTableSeed());
|
||||||
|
container.setLootTable(null);
|
||||||
|
assertEquals(2, container.getItem(0).getCount());
|
||||||
|
for (int slot = 1; slot < container.getContainerSize(); slot++) {
|
||||||
|
assertTrue(container.getItem(slot).isEmpty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void failedDoubleChestFillRestoresBothDeferredHalvesBeforeRetry() {
|
||||||
|
BlockPos leftPos = new BlockPos(0, 64, 0);
|
||||||
|
BlockPos rightPos = new BlockPos(1, 64, 0);
|
||||||
|
BlockState leftState = Blocks.CHEST.defaultBlockState().setValue(ChestBlock.TYPE, ChestType.LEFT);
|
||||||
|
BlockState rightState = Blocks.CHEST.defaultBlockState().setValue(ChestBlock.TYPE, ChestType.RIGHT);
|
||||||
|
DeferredLootChest left = new DeferredLootChest(leftPos, leftState);
|
||||||
|
DeferredLootChest right = new DeferredLootChest(rightPos, rightState);
|
||||||
|
left.setItem(0, diamondStack(2));
|
||||||
|
right.setItem(0, diamondStack(3));
|
||||||
|
right.reject = true;
|
||||||
|
ResourceKey<LootTable> table = ResourceKey.create(Registries.LOOT_TABLE, Identifier.parse("minecraft:chests/simple_dungeon"));
|
||||||
|
left.setLootTable(table, 41L);
|
||||||
|
right.setLootTable(table, 42L);
|
||||||
|
ServerLevel level = mock(ServerLevel.class);
|
||||||
|
NativeWorld world = mock(NativeWorld.class);
|
||||||
|
NativeBlockState state = mock(NativeBlockState.class);
|
||||||
|
when(world.nativeHandle()).thenReturn(level);
|
||||||
|
when(state.nativeHandle()).thenReturn(leftState);
|
||||||
|
when(level.getBlockEntity(leftPos)).thenReturn(left);
|
||||||
|
when(level.getBlockEntity(rightPos)).thenReturn(right);
|
||||||
|
CompoundContainer inventory = new CompoundContainer(left, right);
|
||||||
|
ItemStack reward = diamondStack(7);
|
||||||
|
try (MockedStatic<ChestBlock> chests = mockStatic(ChestBlock.class)) {
|
||||||
|
chests.when(() -> ChestBlock.getContainer((ChestBlock) Blocks.CHEST, leftState, level, leftPos, true))
|
||||||
|
.thenReturn(inventory);
|
||||||
|
chests.when(() -> ChestBlock.getConnectedBlockPos(leftPos, leftState)).thenReturn(rightPos);
|
||||||
|
NativeContainerLoot access = new NativeContainerLoot(new NativeContainerLoot.Target(
|
||||||
|
world, new NativeBlockPoint(0, 64, 0), state));
|
||||||
|
NativeContainerLoot.Session session = access.open();
|
||||||
|
assertNotNull(session);
|
||||||
|
session.add(List.of(NativeItemStack.of(reward)));
|
||||||
|
|
||||||
|
assertThrows(IllegalStateException.class, () -> session.fill(new RNG(17), message -> {}));
|
||||||
|
|
||||||
|
assertSame(table, left.getLootTable());
|
||||||
|
assertSame(table, right.getLootTable());
|
||||||
|
assertEquals(41L, left.getLootTableSeed());
|
||||||
|
assertEquals(42L, right.getLootTableSeed());
|
||||||
|
left.setLootTable(null);
|
||||||
|
right.setLootTable(null);
|
||||||
|
assertEquals(2, countItems(left));
|
||||||
|
assertEquals(3, countItems(right));
|
||||||
|
assertEquals(7, reward.getCount());
|
||||||
|
left.setLootTable(table, 41L);
|
||||||
|
right.setLootTable(table, 42L);
|
||||||
|
right.reject = false;
|
||||||
|
|
||||||
|
session.fill(new RNG(17), message -> {});
|
||||||
|
|
||||||
|
assertNull(left.getLootTable());
|
||||||
|
assertNull(right.getLootTable());
|
||||||
|
assertEquals(15, countItems(left) + countItems(right));
|
||||||
|
assertEquals(7, reward.getCount());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int countItems(ChestBlockEntity container) {
|
||||||
|
int total = 0;
|
||||||
|
for (int slot = 0; slot < container.getContainerSize(); slot++) {
|
||||||
|
total += container.getItem(slot).getCount();
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ItemStack diamondStack(int count) {
|
||||||
|
ItemStack stack = new ItemStack(Items.DIAMOND, count);
|
||||||
|
stack.set(DataComponents.MAX_STACK_SIZE, 64);
|
||||||
|
return stack;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class RejectingContainer extends SimpleContainer {
|
||||||
|
private boolean reject;
|
||||||
|
|
||||||
|
private RejectingContainer() {
|
||||||
|
super(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setItem(int slot, ItemStack stack) {
|
||||||
|
super.setItem(slot, stack);
|
||||||
|
if (reject) {
|
||||||
|
reject = false;
|
||||||
|
throw new IllegalStateException("Inventory write failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class DeferredLootChest extends ChestBlockEntity {
|
||||||
|
private boolean reject;
|
||||||
|
|
||||||
|
private DeferredLootChest(BlockPos position, BlockState state) {
|
||||||
|
super(position, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unpackLootTable(Player player) {
|
||||||
|
if (getLootTable() != null) {
|
||||||
|
setLootTable(null);
|
||||||
|
setItem(0, diamondStack(4));
|
||||||
|
if (reject) {
|
||||||
|
throw new IllegalStateException("Native loot failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private RandomizableContainer containerWithLootTable(ResourceKey<LootTable> key) {
|
private RandomizableContainer containerWithLootTable(ResourceKey<LootTable> key) {
|
||||||
return (RandomizableContainer) Proxy.newProxyInstance(
|
return (RandomizableContainer) Proxy.newProxyInstance(
|
||||||
RandomizableContainer.class.getClassLoader(),
|
RandomizableContainer.class.getClassLoader(),
|
||||||
|
|||||||
+38
-6
@@ -57,7 +57,6 @@ import static org.junit.Assert.assertTrue;
|
|||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyInt;
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
import static org.mockito.ArgumentMatchers.anyString;
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
import static org.mockito.ArgumentMatchers.contains;
|
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.doAnswer;
|
import static org.mockito.Mockito.doAnswer;
|
||||||
import static org.mockito.Mockito.inOrder;
|
import static org.mockito.Mockito.inOrder;
|
||||||
@@ -171,7 +170,7 @@ public class ModdedChunkUpdateServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void permanentSavedBiomeUnavailabilityCompletesThePassWithAWarning() {
|
public void permanentSavedBiomeUnavailabilityRetainsPendingLoot() {
|
||||||
ChestUpdateMantleChunk chunk = new ChestUpdateMantleChunk();
|
ChestUpdateMantleChunk chunk = new ChestUpdateMantleChunk();
|
||||||
Engine engine = updateEngine(chunk);
|
Engine engine = updateEngine(chunk);
|
||||||
NativeWorld level = chestLevel();
|
NativeWorld level = chestLevel();
|
||||||
@@ -187,13 +186,46 @@ public class ModdedChunkUpdateServiceTest {
|
|||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
new ModdedChunkUpdateService().updateRegeneratedChunk(engine, level, 0, 0);
|
assertThrows(SavedBiomeUnavailableException.class,
|
||||||
|
() -> new ModdedChunkUpdateService().updateRegeneratedChunk(engine, level, 0, 0));
|
||||||
|
|
||||||
|
assertFalse(chunk.isFlagged(MantleFlag.ETCHED));
|
||||||
|
assertFalse(chunk.isFlagged(MantleFlag.UPDATE));
|
||||||
|
assertFalse(chunk.deleted());
|
||||||
|
verify(chunk.slice, never()).set(2, 5, 2, null);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void ordinaryLootFailureRetainsPendingWorkAndRetries() {
|
||||||
|
ChestUpdateMantleChunk chunk = new ChestUpdateMantleChunk();
|
||||||
|
Engine engine = updateEngine(chunk);
|
||||||
|
NativeWorld level = chestLevel();
|
||||||
|
AtomicInteger attempts = new AtomicInteger();
|
||||||
|
ModdedChunkUpdateService service = new ModdedChunkUpdateService();
|
||||||
|
IllegalStateException failure = new IllegalStateException("Loot provider unavailable");
|
||||||
|
try (MockedStatic<ModdedLootApplier> loot = mockStatic(ModdedLootApplier.class)) {
|
||||||
|
loot.when(() -> ModdedLootApplier.apply(any(), any(), any(), any(), any())).thenAnswer(invocation -> {
|
||||||
|
NativeBlockPoint position = invocation.getArgument(2);
|
||||||
|
if (position.x() == 2 && attempts.getAndIncrement() == 0) {
|
||||||
|
throw failure;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
assertSame(failure, assertThrows(IllegalStateException.class,
|
||||||
|
() -> service.updateRegeneratedChunk(engine, level, 0, 0)));
|
||||||
|
assertFalse(chunk.isFlagged(MantleFlag.UPDATE));
|
||||||
|
assertFalse(chunk.isFlagged(MantleFlag.ETCHED));
|
||||||
|
assertFalse(chunk.deleted());
|
||||||
|
verify(chunk.slice, never()).set(2, 5, 2, null);
|
||||||
|
|
||||||
|
service.updateRegeneratedChunk(engine, level, 0, 0);
|
||||||
|
|
||||||
assertTrue(chunk.isFlagged(MantleFlag.ETCHED));
|
assertTrue(chunk.isFlagged(MantleFlag.ETCHED));
|
||||||
assertTrue(chunk.deleted());
|
assertTrue(chunk.deleted());
|
||||||
logging.verify(() -> IrisLogging.warnOnce(anyString(), contains(reason)));
|
assertEquals(2, attempts.get());
|
||||||
logging.verify(() -> IrisLogging.reportError(any(Throwable.class)), never());
|
|
||||||
logging.verify(() -> IrisLogging.reportError(anyString(), any(Throwable.class)), never());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -289,6 +289,8 @@ final class WorldChunkMaintenance {
|
|||||||
boolean scheduled = J.runRegion(world, chunkX, chunkZ, manager.managedTask("bukkit_world_manager_chunk_update", () -> {
|
boolean scheduled = J.runRegion(world, chunkX, chunkZ, manager.managedTask("bukkit_world_manager_chunk_update", () -> {
|
||||||
try {
|
try {
|
||||||
updateChunkRegion(world, chunkX, chunkZ);
|
updateChunkRegion(world, chunkX, chunkZ);
|
||||||
|
} catch (RuntimeException | Error failure) {
|
||||||
|
reportMaterializationFailure(new MaterializationTarget(world, chunkX, chunkZ), failure);
|
||||||
} finally {
|
} finally {
|
||||||
chunkUpdateQueue.remove(key);
|
chunkUpdateQueue.remove(key);
|
||||||
}
|
}
|
||||||
@@ -508,6 +510,13 @@ final class WorldChunkMaintenance {
|
|||||||
return loadedChunkPositions;
|
return loadedChunkPositions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void reportMaterializationFailure(MaterializationTarget target, Throwable failure) {
|
||||||
|
String context = "Failed to materialize chunk " + target.x() + "," + target.z() + " in " + target.world().getName() + ".";
|
||||||
|
if (IrisLogging.errorOnce("chunk-materialization:" + target.world().getUID() + ":" + target.x() + "," + target.z() + ":" + failure, context)) {
|
||||||
|
IrisLogging.reportError(context, failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private record MaterializationTarget(World world, int x, int z) {
|
private record MaterializationTarget(World world, int x, int z) {
|
||||||
private long key() {
|
private long key() {
|
||||||
return Cache.key(x, z);
|
return Cache.key(x, z);
|
||||||
@@ -614,7 +623,7 @@ final class WorldChunkMaintenance {
|
|||||||
}
|
}
|
||||||
spawnInitially(chunk);
|
spawnInitially(chunk);
|
||||||
} catch (Throwable failure) {
|
} catch (Throwable failure) {
|
||||||
IrisLogging.reportError("Failed to materialize saved chunk at " + target.x() + "," + target.z() + ".", failure);
|
reportMaterializationFailure(target, failure);
|
||||||
} finally {
|
} finally {
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
applying = false;
|
applying = false;
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ package art.arcane.iris.platform.generation;
|
|||||||
|
|
||||||
import art.arcane.iris.world.history.GenerationFindCatalog;
|
import art.arcane.iris.world.history.GenerationFindCatalog;
|
||||||
|
|
||||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
|
||||||
import art.arcane.iris.world.event.IrisLootEvent;
|
import art.arcane.iris.world.event.IrisLootEvent;
|
||||||
import art.arcane.iris.integration.Identifier;
|
import art.arcane.iris.integration.Identifier;
|
||||||
import art.arcane.iris.integration.ExternalDataSVC;
|
import art.arcane.iris.integration.ExternalDataSVC;
|
||||||
@@ -73,6 +72,7 @@ import org.bukkit.World;
|
|||||||
import org.bukkit.block.Block;
|
import org.bukkit.block.Block;
|
||||||
import org.bukkit.block.BlockFace;
|
import org.bukkit.block.BlockFace;
|
||||||
import org.bukkit.block.BlockState;
|
import org.bukkit.block.BlockState;
|
||||||
|
import org.bukkit.block.DoubleChest;
|
||||||
import org.bukkit.block.data.BlockData;
|
import org.bukkit.block.data.BlockData;
|
||||||
import org.bukkit.block.data.type.Chest;
|
import org.bukkit.block.data.type.Chest;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
@@ -82,6 +82,8 @@ import org.bukkit.inventory.ItemStack;
|
|||||||
import org.bukkit.loot.Lootable;
|
import org.bukkit.loot.Lootable;
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.CancellationException;
|
import java.util.concurrent.CancellationException;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
@@ -89,9 +91,7 @@ import java.util.concurrent.CompletionException;
|
|||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
import java.util.function.BooleanSupplier;
|
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Predicate;
|
|
||||||
|
|
||||||
import art.arcane.iris.localization.BukkitRuntimeMessages;
|
import art.arcane.iris.localization.BukkitRuntimeMessages;
|
||||||
import art.arcane.iris.localization.IrisLanguage;
|
import art.arcane.iris.localization.IrisLanguage;
|
||||||
@@ -124,14 +124,7 @@ public final class EngineBukkitOps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!J.isFolia() && !J.isPrimaryThread()) {
|
if (!J.isFolia() && !J.isPrimaryThread()) {
|
||||||
CompletableFuture<?> scheduled = J.sfut(() -> updateChunk(engine, c, mantle));
|
awaitMaterialization(J.sfut(() -> updateChunk(engine, c, mantle)));
|
||||||
if (scheduled != null) {
|
|
||||||
try {
|
|
||||||
scheduled.join();
|
|
||||||
} catch (Throwable e) {
|
|
||||||
IrisLogging.reportError(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,21 +296,25 @@ public final class EngineBukkitOps {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!scheduled) {
|
if (!scheduled) {
|
||||||
if (J.isPrimaryThread()) {
|
throw new IllegalStateException("Failed to schedule chunk materialization for "
|
||||||
runnable.run();
|
+ contextChunk.getWorld().getName() + " at chunk " + contextChunk.getX() + "," + contextChunk.getZ() + ".");
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
awaitMaterialization(completion);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void awaitMaterialization(CompletableFuture<?> completion) {
|
||||||
try {
|
try {
|
||||||
completion.join();
|
completion.join();
|
||||||
} catch (CompletionException error) {
|
} catch (CompletionException error) {
|
||||||
Throwable cause = error.getCause();
|
Throwable cause = error.getCause();
|
||||||
IrisLogging.reportError(
|
if (cause instanceof RuntimeException runtimeFailure) {
|
||||||
"Failed deferred chunk materialization for " + contextChunk.getWorld().getName()
|
throw runtimeFailure;
|
||||||
+ " at chunk " + contextChunk.getX() + "," + contextChunk.getZ() + ".",
|
}
|
||||||
cause == null ? error : cause
|
if (cause instanceof Error fatalFailure) {
|
||||||
);
|
throw fatalFailure;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,8 +348,12 @@ public final class EngineBukkitOps {
|
|||||||
InventoryHolder m = (InventoryHolder) block.getState();
|
InventoryHolder m = (InventoryHolder) block.getState();
|
||||||
addItems(engine, false, m.getInventory(), tables, slot, c.getWorld(), worldX, y, worldZ);
|
addItems(engine, false, m.getInventory(), tables, slot, c.getWorld(), worldX, y, worldZ);
|
||||||
|
|
||||||
} catch (Throwable e) {
|
} catch (RuntimeException | Error failure) {
|
||||||
IrisLogging.reportError(e);
|
if (IrisLogging.errorOnce("container-loot:" + world.getUID() + ":" + worldX + "," + y + "," + worldZ + ":" + failure,
|
||||||
|
"Failed to populate loot at " + world.getName() + " [" + worldX + "," + y + "," + worldZ + "].")) {
|
||||||
|
IrisLogging.reportError(failure);
|
||||||
|
}
|
||||||
|
throw failure;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -515,6 +516,10 @@ public final class EngineBukkitOps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void addItems(Engine engine, boolean debug, Inventory inv, KList<IrisLootTable> tables, InventorySlotType slot, World world, int x, int y, int z) {
|
public static void addItems(Engine engine, boolean debug, Inventory inv, KList<IrisLootTable> tables, InventorySlotType slot, World world, int x, int y, int z) {
|
||||||
|
if (!J.isOwnedByCurrentRegion(world, x >> 4, z >> 4)) {
|
||||||
|
throw new IllegalStateException("Loot inventory mutation requires the owning region at "
|
||||||
|
+ world.getName() + " [" + x + "," + y + "," + z + "].");
|
||||||
|
}
|
||||||
KList<ItemStack> items = new KList<>();
|
KList<ItemStack> items = new KList<>();
|
||||||
|
|
||||||
for (IrisLootTable i : tables) {
|
for (IrisLootTable i : tables) {
|
||||||
@@ -525,57 +530,69 @@ public final class EngineBukkitOps {
|
|||||||
if (IrisLootEvent.callLootEvent(items, inv, world, x, y, z))
|
if (IrisLootEvent.callLootEvent(items, inv, world, x, y, z))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (PaperLib.isPaper() && engine.getWorld().hasPlatformWorld()) {
|
populateInventory(engine, inv, items, LootResolver.containerRng(engine.getSeedManager().getLoot(), x, y, z));
|
||||||
PaperLib.getChunkAtAsync(BukkitWorldBinding.world(engine.getWorld()), x >> 4, z >> 4).thenAccept((c) -> {
|
}
|
||||||
Runnable mutation = () -> {
|
|
||||||
for (ItemStack i : items) {
|
|
||||||
inv.addItem(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
scramble(engine, inv, LootResolver.containerRng(engine.getSeedManager().getLoot(), x, y, z));
|
static void populateInventory(Engine engine, Inventory inv, List<ItemStack> items, RNG rng) {
|
||||||
};
|
List<BlockState> states = new ArrayList<>(2);
|
||||||
|
InventoryHolder holder = inv.getHolder();
|
||||||
LootMutationTask task = new LootMutationTask(world, x >> 4, z >> 4, mutation);
|
if (holder instanceof DoubleChest chest) {
|
||||||
boolean scheduled = dispatchLootMutation(
|
snapshotContainer(states, chest.getLeftSide());
|
||||||
task,
|
snapshotContainer(states, chest.getRightSide());
|
||||||
J.isFolia(),
|
|
||||||
Bukkit::isPrimaryThread,
|
|
||||||
regionTask -> J.runRegion(
|
|
||||||
regionTask.world(),
|
|
||||||
regionTask.chunkX(),
|
|
||||||
regionTask.chunkZ(),
|
|
||||||
regionTask.mutation()),
|
|
||||||
J::s
|
|
||||||
);
|
|
||||||
if (!scheduled) {
|
|
||||||
IrisLogging.error("Failed to schedule loot inventory mutation for " + world.getName() + "@" + (x >> 4) + "," + (z >> 4) + ".");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
for (ItemStack i : items) {
|
snapshotContainer(states, holder);
|
||||||
inv.addItem(i);
|
}
|
||||||
|
ItemStack[] contents = null;
|
||||||
|
try {
|
||||||
|
if (states.isEmpty()) {
|
||||||
|
contents = copyContents(inv.getContents());
|
||||||
}
|
}
|
||||||
|
for (ItemStack i : items) {
|
||||||
scramble(engine, inv, LootResolver.containerRng(engine.getSeedManager().getLoot(), x, y, z));
|
if (i != null) {
|
||||||
|
inv.addItem(i.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scramble(engine, inv, rng);
|
||||||
|
} catch (RuntimeException | Error failure) {
|
||||||
|
if (states.isEmpty()) {
|
||||||
|
if (contents != null) {
|
||||||
|
try {
|
||||||
|
inv.setContents(contents);
|
||||||
|
} catch (RuntimeException | Error rollbackFailure) {
|
||||||
|
if (rollbackFailure != failure) {
|
||||||
|
failure.addSuppressed(rollbackFailure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (BlockState state : states) {
|
||||||
|
try {
|
||||||
|
if (!state.update(true, false)) {
|
||||||
|
throw new IllegalStateException("Failed to restore loot container at " + state.getLocation());
|
||||||
|
}
|
||||||
|
} catch (RuntimeException | Error rollbackFailure) {
|
||||||
|
if (rollbackFailure != failure) {
|
||||||
|
failure.addSuppressed(rollbackFailure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw failure;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static boolean dispatchLootMutation(
|
private static void snapshotContainer(List<BlockState> snapshots, InventoryHolder holder) {
|
||||||
LootMutationTask task,
|
if (holder instanceof BlockState state) {
|
||||||
boolean folia,
|
snapshots.add(state.getBlock().getState());
|
||||||
BooleanSupplier primaryThread,
|
|
||||||
Predicate<LootMutationTask> regionScheduler,
|
|
||||||
Consumer<Runnable> syncScheduler) {
|
|
||||||
if (folia) {
|
|
||||||
return regionScheduler.test(task);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (primaryThread.getAsBoolean()) {
|
private static ItemStack[] copyContents(ItemStack[] contents) {
|
||||||
task.mutation().run();
|
ItemStack[] copy = new ItemStack[contents.length];
|
||||||
} else {
|
for (int index = 0; index < contents.length; index++) {
|
||||||
syncScheduler.accept(task.mutation());
|
copy[index] = contents[index] == null ? null : contents[index].clone();
|
||||||
}
|
}
|
||||||
return true;
|
return copy;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IrisBiome getBiome(Engine engine, Location l) {
|
public static IrisBiome getBiome(Engine engine, Location l) {
|
||||||
@@ -838,6 +855,4 @@ public final class EngineBukkitOps {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
record LootMutationTask(World world, int chunkX, int chunkZ, Runnable mutation) {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -93,8 +93,10 @@ public class WorldObjectPlacer implements IObjectPlacer {
|
|||||||
}
|
}
|
||||||
InventoryHolder holder = (InventoryHolder) block.getState();
|
InventoryHolder holder = (InventoryHolder) block.getState();
|
||||||
EngineBukkitOps.addItems(engine, false, holder.getInventory(), tables, InventorySlotType.STORAGE, world, x, y, z);
|
EngineBukkitOps.addItems(engine, false, holder.getInventory(), tables, InventorySlotType.STORAGE, world, x, y, z);
|
||||||
} catch (Throwable e) {
|
} catch (RuntimeException | Error failure) {
|
||||||
IrisLogging.reportError(e);
|
IrisLogging.reportError("Failed to populate placed container at " + world.getName()
|
||||||
|
+ " [" + x + "," + y + "," + z + "].", failure);
|
||||||
|
throw failure;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import art.arcane.iris.generation.runtime.Engine;
|
|||||||
import art.arcane.iris.world.loot.InventorySlotType;
|
import art.arcane.iris.world.loot.InventorySlotType;
|
||||||
import art.arcane.iris.world.loot.IrisLootTable;
|
import art.arcane.iris.world.loot.IrisLootTable;
|
||||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
import art.arcane.iris.world.task.J;
|
import art.arcane.iris.world.task.J;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
@@ -92,6 +91,10 @@ public class IrisLootEvent extends Event {
|
|||||||
* @return true when the event was canceled
|
* @return true when the event was canceled
|
||||||
*/
|
*/
|
||||||
public static boolean callLootEvent(KList<ItemStack> loot, Inventory inv, World world, int x, int y, int z) {
|
public static boolean callLootEvent(KList<ItemStack> loot, Inventory inv, World world, int x, int y, int z) {
|
||||||
|
if (!J.isOwnedByCurrentRegion(world, x >> 4, z >> 4)) {
|
||||||
|
throw new IllegalStateException("LootGenerateEvent requires the owning region at "
|
||||||
|
+ world.getName() + " [" + x + "," + y + "," + z + "].");
|
||||||
|
}
|
||||||
InventoryHolder holder = inv.getHolder();
|
InventoryHolder holder = inv.getHolder();
|
||||||
Location loc = new Location(world, x, y, z);
|
Location loc = new Location(world, x, y, z);
|
||||||
if (holder == null) {
|
if (holder == null) {
|
||||||
@@ -106,39 +109,8 @@ public class IrisLootEvent extends Event {
|
|||||||
|
|
||||||
LootContext context = new LootContext.Builder(loc).build();
|
LootContext context = new LootContext.Builder(loc).build();
|
||||||
LootGenerateEvent event = new LootGenerateEvent(world, null, holder, EMPTY, context, loot, true);
|
LootGenerateEvent event = new LootGenerateEvent(world, null, holder, EMPTY, context, loot, true);
|
||||||
if (!Bukkit.isPrimaryThread()) {
|
|
||||||
IrisLogging.warn("LootGenerateEvent was not called on the main thread, please report this issue.");
|
Bukkit.getPluginManager().callEvent(event);
|
||||||
Thread.dumpStack();
|
|
||||||
J.sfut(() -> {
|
|
||||||
try {
|
|
||||||
Bukkit.getPluginManager().callEvent(event);
|
|
||||||
} catch (Throwable e) {
|
|
||||||
IrisLogging.reportError("LootGenerateEvent dispatch failed at "
|
|
||||||
+ world.getName() + " [" + x + "," + y + "," + z + "].", e);
|
|
||||||
if (e instanceof RuntimeException runtimeException) {
|
|
||||||
throw runtimeException;
|
|
||||||
}
|
|
||||||
if (e instanceof Error error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
throw new IllegalStateException(e);
|
|
||||||
}
|
|
||||||
}).join();
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
Bukkit.getPluginManager().callEvent(event);
|
|
||||||
} catch (Throwable e) {
|
|
||||||
IrisLogging.reportError("LootGenerateEvent dispatch failed at "
|
|
||||||
+ world.getName() + " [" + x + "," + y + "," + z + "].", e);
|
|
||||||
if (e instanceof RuntimeException runtimeException) {
|
|
||||||
throw runtimeException;
|
|
||||||
}
|
|
||||||
if (e instanceof Error error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
throw new IllegalStateException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return event.isCancelled();
|
return event.isCancelled();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,13 +33,12 @@ public class IrisVanillaLootTable extends IrisLootTable {
|
|||||||
private final String lootTableKey;
|
private final String lootTableKey;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The resolved table, looked up once. Every chest pull resolved it again - a NamespacedKey parse plus a registry
|
* Cache the parsed key; resolve its table from the current registry for every container.
|
||||||
* lookup per container, and a dungeon room is a lot of containers.
|
|
||||||
* <p>
|
* <p>
|
||||||
* Transient for the same reason the field above is a String: it keeps the Bukkit type argument out of the Gson
|
* Transient for the same reason the field above is a String: it keeps the Bukkit type argument out of the Gson
|
||||||
* field walk and out of the pack purity gate, both of which resolve generic field types for the fields they visit.
|
* field walk and out of the pack purity gate, both of which resolve generic field types for the fields they visit.
|
||||||
*/
|
*/
|
||||||
private final transient AtomicCache<LootTable> resolvedTable = new AtomicCache<>();
|
private final transient AtomicCache<NamespacedKey> resolvedKey = new AtomicCache<>();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getName() {
|
public String getName() {
|
||||||
@@ -83,10 +82,8 @@ public class IrisVanillaLootTable extends IrisLootTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private LootTable resolveTable() {
|
private LootTable resolveTable() {
|
||||||
return resolvedTable.aquire(() -> {
|
NamespacedKey key = resolvedKey.aquire(() -> NamespacedKey.fromString(lootTableKey));
|
||||||
NamespacedKey key = NamespacedKey.fromString(lootTableKey);
|
return key == null ? null : Bukkit.getLootTable(key);
|
||||||
return key == null ? null : Bukkit.getLootTable(key);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
+5
@@ -175,6 +175,7 @@ public class WorldChunkMaintenanceRoutingTest {
|
|||||||
public void failedMaterializationRetainsRetryAndReportsTheFailure() throws Exception {
|
public void failedMaterializationRetainsRetryAndReportsTheFailure() throws Exception {
|
||||||
try (Fixture fixture = new Fixture()) {
|
try (Fixture fixture = new Fixture()) {
|
||||||
IllegalStateException failure = new IllegalStateException("Provider placement failed");
|
IllegalStateException failure = new IllegalStateException("Provider placement failed");
|
||||||
|
fixture.logging.when(() -> IrisLogging.errorOnce(anyString(), anyString())).thenReturn(true, false);
|
||||||
fixture.updates.when(() -> EngineBukkitOps.updateChunk(fixture.engine, fixture.chunk, fixture.savedMantle)).thenThrow(failure);
|
fixture.updates.when(() -> EngineBukkitOps.updateChunk(fixture.engine, fixture.chunk, fixture.savedMantle)).thenThrow(failure);
|
||||||
fixture.schedule();
|
fixture.schedule();
|
||||||
fixture.prepare();
|
fixture.prepare();
|
||||||
@@ -182,6 +183,10 @@ public class WorldChunkMaintenanceRoutingTest {
|
|||||||
fixture.apply();
|
fixture.apply();
|
||||||
fixture.awaitRelease();
|
fixture.awaitRelease();
|
||||||
|
|
||||||
|
fixture.schedule();
|
||||||
|
fixture.prepare();
|
||||||
|
fixture.apply();
|
||||||
|
fixture.awaitRelease();
|
||||||
fixture.logging.verify(() -> IrisLogging.reportError(anyString(), eq(failure)));
|
fixture.logging.verify(() -> IrisLogging.reportError(anyString(), eq(failure)));
|
||||||
fixture.schedule();
|
fixture.schedule();
|
||||||
assertEquals(1, fixture.async.size());
|
assertEquals(1, fixture.async.size());
|
||||||
|
|||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
package art.arcane.iris.platform.bukkit;
|
||||||
|
|
||||||
|
import art.arcane.volmlib.util.data.BSupport;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.block.data.BlockData;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class BukkitStorageClassificationTest {
|
||||||
|
@Test
|
||||||
|
public void copperChestsAndExistingLootContainersReceiveStorageUpdates() {
|
||||||
|
BlockData air = blockData(Material.AIR);
|
||||||
|
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) {
|
||||||
|
bukkit.when(() -> Bukkit.createBlockData(Material.AIR)).thenReturn(air);
|
||||||
|
BSupport<Object> support = new BSupport<Object>() {
|
||||||
|
};
|
||||||
|
for (Material material : new Material[]{Material.COPPER_CHEST, Material.EXPOSED_COPPER_CHEST,
|
||||||
|
Material.WEATHERED_COPPER_CHEST, Material.OXIDIZED_COPPER_CHEST,
|
||||||
|
Material.WAXED_COPPER_CHEST, Material.WAXED_EXPOSED_COPPER_CHEST,
|
||||||
|
Material.WAXED_WEATHERED_COPPER_CHEST, Material.WAXED_OXIDIZED_COPPER_CHEST,
|
||||||
|
Material.CHEST, Material.TRAPPED_CHEST, Material.BARREL, Material.SHULKER_BOX,
|
||||||
|
Material.BLUE_SHULKER_BOX, Material.DISPENSER, Material.DROPPER, Material.HOPPER}) {
|
||||||
|
BlockData state = blockData(material);
|
||||||
|
assertTrue(material.name(), support.isStorage(state));
|
||||||
|
assertTrue(material.name(), support.isStorageChest(state));
|
||||||
|
assertTrue(material.name(), support.isUpdatable(state));
|
||||||
|
}
|
||||||
|
for (Material material : new Material[]{Material.FURNACE, Material.BLAST_FURNACE, Material.SMOKER}) {
|
||||||
|
BlockData state = blockData(material);
|
||||||
|
assertTrue(material.name(), support.isStorage(state));
|
||||||
|
assertFalse(material.name(), support.isStorageChest(state));
|
||||||
|
}
|
||||||
|
for (Material material : new Material[]{Material.STONE, Material.AIR, Material.ENDER_CHEST,
|
||||||
|
Material.COPPER_BLOCK, Material.COPPER_CHESTPLATE}) {
|
||||||
|
BlockData state = blockData(material);
|
||||||
|
assertFalse(material.name(), support.isStorage(state));
|
||||||
|
assertFalse(material.name(), support.isStorageChest(state));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BlockData blockData(Material material) {
|
||||||
|
BlockData state = mock(BlockData.class);
|
||||||
|
when(state.getMaterial()).thenReturn(material);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
+343
-100
@@ -1,20 +1,66 @@
|
|||||||
package art.arcane.iris.platform.generation;
|
package art.arcane.iris.platform.generation;
|
||||||
|
|
||||||
import org.bukkit.World;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
import org.bukkit.block.Block;
|
import art.arcane.iris.generation.runtime.EngineMetrics;
|
||||||
import org.bukkit.block.BlockFace;
|
import art.arcane.iris.platform.bukkit.BukkitBlockResolution;
|
||||||
import org.bukkit.block.data.type.Chest;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
import org.bukkit.loot.LootTable;
|
import art.arcane.iris.testsupport.BukkitTestServer;
|
||||||
import org.junit.Test;
|
import art.arcane.iris.world.event.IrisLootEvent;
|
||||||
|
import art.arcane.iris.world.loot.InventorySlotType;
|
||||||
|
import art.arcane.iris.world.loot.IrisLootTable;
|
||||||
|
import art.arcane.iris.world.task.J;
|
||||||
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
|
import art.arcane.volmlib.util.function.Consumer4;
|
||||||
|
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||||
|
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||||
|
import art.arcane.volmlib.util.mantle.runtime.MantleDataAdapter;
|
||||||
|
import art.arcane.volmlib.util.mantle.runtime.MantleHooks;
|
||||||
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
|
import art.arcane.volmlib.util.matter.Matter;
|
||||||
|
import art.arcane.volmlib.util.matter.MatterUpdate;
|
||||||
|
import art.arcane.volmlib.util.matter.slices.UpdateMatter;
|
||||||
|
import io.papermc.paper.registry.RegistryAccess;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Chunk;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.block.Barrel;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.block.BlockFace;
|
||||||
|
import org.bukkit.block.BlockState;
|
||||||
|
import org.bukkit.block.DoubleChest;
|
||||||
|
import org.bukkit.block.data.BlockData;
|
||||||
|
import org.bukkit.block.data.type.Chest;
|
||||||
|
import org.bukkit.event.world.LootGenerateEvent;
|
||||||
|
import org.bukkit.inventory.Inventory;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import org.bukkit.loot.LootTable;
|
||||||
|
import org.bukkit.plugin.PluginManager;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertSame;
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
|
||||||
|
import static org.mockito.Mockito.doAnswer;
|
||||||
|
import static org.mockito.Mockito.doThrow;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
import static org.mockito.Mockito.spy;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.verifyNoInteractions;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
public class EngineBukkitOpsLootTest {
|
public class EngineBukkitOpsLootTest {
|
||||||
@@ -46,117 +92,314 @@ public class EngineBukkitOpsLootTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void foliaLootMutationUsesOwningRegionOnly() {
|
public void failedInsertionRestoresExistingLootBeforeRetry() {
|
||||||
World world = mock(World.class);
|
Inventory inventory = mock(Inventory.class);
|
||||||
AtomicInteger primaryChecks = new AtomicInteger();
|
AtomicReference<ItemStack[]> contents = new AtomicReference<>(new ItemStack[]{item(2), null});
|
||||||
AtomicInteger regionSchedules = new AtomicInteger();
|
AtomicBoolean reject = new AtomicBoolean(true);
|
||||||
AtomicInteger syncSchedules = new AtomicInteger();
|
when(inventory.getSize()).thenReturn(2);
|
||||||
AtomicBoolean mutated = new AtomicBoolean();
|
when(inventory.getContents()).thenAnswer(invocation -> contents.get());
|
||||||
Runnable mutation = () -> mutated.set(true);
|
doAnswer(invocation -> {
|
||||||
EngineBukkitOps.LootMutationTask task = new EngineBukkitOps.LootMutationTask(world, 12, -3, mutation);
|
contents.set(invocation.getArgument(0));
|
||||||
|
return null;
|
||||||
|
}).when(inventory).setContents(any(ItemStack[].class));
|
||||||
|
doAnswer(invocation -> {
|
||||||
|
ItemStack incoming = invocation.getArgument(0);
|
||||||
|
contents.get()[0].setAmount(contents.get()[0].getAmount() + incoming.getAmount());
|
||||||
|
if (reject.getAndSet(false)) {
|
||||||
|
throw new IllegalStateException("Insertion failed");
|
||||||
|
}
|
||||||
|
return new HashMap<Integer, ItemStack>();
|
||||||
|
}).when(inventory).addItem(any(ItemStack.class));
|
||||||
|
ItemStack reward = item(7);
|
||||||
|
|
||||||
boolean scheduled = EngineBukkitOps.dispatchLootMutation(
|
assertThrows(IllegalStateException.class,
|
||||||
task,
|
() -> EngineBukkitOps.populateInventory(null, inventory, List.of(reward), new RNG(17)));
|
||||||
true,
|
|
||||||
() -> {
|
|
||||||
primaryChecks.incrementAndGet();
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
regionTask -> {
|
|
||||||
regionSchedules.incrementAndGet();
|
|
||||||
assertSame(world, regionTask.world());
|
|
||||||
assertEquals(12, regionTask.chunkX());
|
|
||||||
assertEquals(-3, regionTask.chunkZ());
|
|
||||||
assertSame(mutation, regionTask.mutation());
|
|
||||||
regionTask.mutation().run();
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
runnable -> syncSchedules.incrementAndGet()
|
|
||||||
);
|
|
||||||
|
|
||||||
assertTrue(scheduled);
|
assertEquals(2, contents.get()[0].getAmount());
|
||||||
assertTrue(mutated.get());
|
EngineBukkitOps.populateInventory(null, inventory, List.of(reward), new RNG(17));
|
||||||
assertEquals(1, regionSchedules.get());
|
int total = 0;
|
||||||
assertEquals(0, primaryChecks.get());
|
for (ItemStack item : contents.get()) {
|
||||||
assertEquals(0, syncSchedules.get());
|
total += item == null ? 0 : item.getAmount();
|
||||||
|
}
|
||||||
|
assertEquals(9, total);
|
||||||
|
assertEquals(7, reward.getAmount());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void failedFoliaRegionScheduleDoesNotMutateInventoryElsewhere() {
|
public void failedNativeInventoryAccessRestoresBothChestSnapshots() {
|
||||||
World world = mock(World.class);
|
Inventory inventory = mock(Inventory.class);
|
||||||
AtomicInteger primaryChecks = new AtomicInteger();
|
DoubleChest holder = mock(DoubleChest.class);
|
||||||
AtomicInteger syncSchedules = new AtomicInteger();
|
org.bukkit.block.Chest left = mock(org.bukkit.block.Chest.class);
|
||||||
AtomicBoolean mutated = new AtomicBoolean();
|
org.bukkit.block.Chest right = mock(org.bukkit.block.Chest.class);
|
||||||
EngineBukkitOps.LootMutationTask task = new EngineBukkitOps.LootMutationTask(world, 1, 2, () -> mutated.set(true));
|
Block leftBlock = mock(Block.class);
|
||||||
|
Block rightBlock = mock(Block.class);
|
||||||
|
BlockState leftSnapshot = mock(BlockState.class);
|
||||||
|
BlockState rightSnapshot = mock(BlockState.class);
|
||||||
|
when(inventory.getHolder()).thenReturn(holder);
|
||||||
|
when(holder.getLeftSide()).thenReturn(left);
|
||||||
|
when(holder.getRightSide()).thenReturn(right);
|
||||||
|
when(left.getBlock()).thenReturn(leftBlock);
|
||||||
|
when(right.getBlock()).thenReturn(rightBlock);
|
||||||
|
when(leftBlock.getState()).thenReturn(leftSnapshot);
|
||||||
|
when(rightBlock.getState()).thenReturn(rightSnapshot);
|
||||||
|
when(leftSnapshot.update(true, false)).thenReturn(true);
|
||||||
|
when(rightSnapshot.update(true, false)).thenReturn(true);
|
||||||
|
IllegalStateException failure = new IllegalStateException("Native loot failed");
|
||||||
|
doThrow(failure).when(inventory).addItem(any(ItemStack.class));
|
||||||
|
|
||||||
boolean scheduled = EngineBukkitOps.dispatchLootMutation(
|
assertSame(failure, assertThrows(IllegalStateException.class,
|
||||||
task,
|
() -> EngineBukkitOps.populateInventory(null, inventory,
|
||||||
true,
|
List.of(item(1)), new RNG(17))));
|
||||||
() -> {
|
|
||||||
primaryChecks.incrementAndGet();
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
regionTask -> false,
|
|
||||||
runnable -> syncSchedules.incrementAndGet()
|
|
||||||
);
|
|
||||||
|
|
||||||
assertFalse(scheduled);
|
verify(leftSnapshot).update(true, false);
|
||||||
assertFalse(mutated.get());
|
verify(rightSnapshot).update(true, false);
|
||||||
assertEquals(0, primaryChecks.get());
|
|
||||||
assertEquals(0, syncSchedules.get());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void nonFoliaPrimaryThreadMutatesInline() {
|
public void failedBarrelFillRestoresItsBlockSnapshot() {
|
||||||
World world = mock(World.class);
|
Inventory inventory = mock(Inventory.class);
|
||||||
AtomicInteger regionSchedules = new AtomicInteger();
|
Barrel holder = mock(Barrel.class);
|
||||||
AtomicInteger syncSchedules = new AtomicInteger();
|
Block block = mock(Block.class);
|
||||||
AtomicBoolean mutated = new AtomicBoolean();
|
Barrel snapshot = mock(Barrel.class);
|
||||||
EngineBukkitOps.LootMutationTask task = new EngineBukkitOps.LootMutationTask(world, 4, 5, () -> mutated.set(true));
|
when(inventory.getHolder()).thenReturn(holder);
|
||||||
|
when(holder.getBlock()).thenReturn(block);
|
||||||
|
when(block.getState()).thenReturn(snapshot);
|
||||||
|
when(snapshot.update(true, false)).thenReturn(true);
|
||||||
|
IllegalStateException failure = new IllegalStateException("Native barrel loot failed");
|
||||||
|
when(inventory.getContents()).thenThrow(failure);
|
||||||
|
|
||||||
boolean scheduled = EngineBukkitOps.dispatchLootMutation(
|
assertSame(failure, assertThrows(IllegalStateException.class,
|
||||||
task,
|
() -> EngineBukkitOps.populateInventory(null, inventory, List.of(), new RNG(17))));
|
||||||
false,
|
|
||||||
() -> true,
|
|
||||||
regionTask -> {
|
|
||||||
regionSchedules.incrementAndGet();
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
runnable -> syncSchedules.incrementAndGet()
|
|
||||||
);
|
|
||||||
|
|
||||||
assertTrue(scheduled);
|
verify(snapshot).update(true, false);
|
||||||
assertTrue(mutated.get());
|
|
||||||
assertEquals(0, regionSchedules.get());
|
|
||||||
assertEquals(0, syncSchedules.get());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void nonFoliaAsyncCompletionUsesSyncScheduler() {
|
public void failedRollbackPreservesTheCauseAndRestoresTheOtherHalf() {
|
||||||
|
Inventory inventory = mock(Inventory.class);
|
||||||
|
DoubleChest holder = mock(DoubleChest.class);
|
||||||
|
org.bukkit.block.Chest left = mock(org.bukkit.block.Chest.class, RETURNS_DEEP_STUBS);
|
||||||
|
org.bukkit.block.Chest right = mock(org.bukkit.block.Chest.class, RETURNS_DEEP_STUBS);
|
||||||
|
BlockState leftSnapshot = mock(BlockState.class);
|
||||||
|
BlockState rightSnapshot = mock(BlockState.class);
|
||||||
|
when(inventory.getHolder()).thenReturn(holder);
|
||||||
|
when(holder.getLeftSide()).thenReturn(left);
|
||||||
|
when(holder.getRightSide()).thenReturn(right);
|
||||||
|
when(left.getBlock().getState()).thenReturn(leftSnapshot);
|
||||||
|
when(right.getBlock().getState()).thenReturn(rightSnapshot);
|
||||||
|
when(rightSnapshot.update(true, false)).thenReturn(true);
|
||||||
|
IllegalStateException failure = new IllegalStateException("Native loot failed");
|
||||||
|
IllegalStateException rollbackFailure = new IllegalStateException("Snapshot restore failed");
|
||||||
|
when(leftSnapshot.update(true, false)).thenThrow(rollbackFailure);
|
||||||
|
doThrow(failure).when(inventory).addItem(any(ItemStack.class));
|
||||||
|
|
||||||
|
assertSame(failure, assertThrows(IllegalStateException.class,
|
||||||
|
() -> EngineBukkitOps.populateInventory(null, inventory, List.of(item(1)), new RNG(17))));
|
||||||
|
|
||||||
|
assertEquals(1, failure.getSuppressed().length);
|
||||||
|
assertSame(rollbackFailure, failure.getSuppressed()[0]);
|
||||||
|
verify(rightSnapshot).update(true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cancelledLootLeavesTheInventoryUntouched() {
|
||||||
|
Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS);
|
||||||
|
Inventory inventory = mock(Inventory.class);
|
||||||
World world = mock(World.class);
|
World world = mock(World.class);
|
||||||
AtomicInteger regionSchedules = new AtomicInteger();
|
try (MockedStatic<J> scheduling = mockStatic(J.class);
|
||||||
AtomicInteger syncSchedules = new AtomicInteger();
|
MockedStatic<IrisLootEvent> events = mockStatic(IrisLootEvent.class)) {
|
||||||
AtomicBoolean mutated = new AtomicBoolean();
|
scheduling.when(() -> J.isOwnedByCurrentRegion(world, 2, -2)).thenReturn(true);
|
||||||
EngineBukkitOps.LootMutationTask task = new EngineBukkitOps.LootMutationTask(world, 6, 7, () -> mutated.set(true));
|
events.when(() -> IrisLootEvent.callLootEvent(any(), eq(inventory), eq(world), eq(33), eq(64), eq(-17)))
|
||||||
|
.thenReturn(true);
|
||||||
|
|
||||||
boolean scheduled = EngineBukkitOps.dispatchLootMutation(
|
EngineBukkitOps.addItems(engine, false, inventory, new KList<>(), InventorySlotType.STORAGE, world, 33, 64, -17);
|
||||||
task,
|
|
||||||
false,
|
|
||||||
() -> false,
|
|
||||||
regionTask -> {
|
|
||||||
regionSchedules.incrementAndGet();
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
runnable -> {
|
|
||||||
syncSchedules.incrementAndGet();
|
|
||||||
runnable.run();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
assertTrue(scheduled);
|
verifyNoInteractions(inventory);
|
||||||
assertTrue(mutated.get());
|
}
|
||||||
assertEquals(0, regionSchedules.get());
|
}
|
||||||
assertEquals(1, syncSchedules.get());
|
|
||||||
|
@Test
|
||||||
|
public void offRegionLootDoesNotRollOrTouchInventory() {
|
||||||
|
Engine engine = mock(Engine.class);
|
||||||
|
Inventory inventory = mock(Inventory.class);
|
||||||
|
World world = mock(World.class);
|
||||||
|
IrisLootTable table = mock(IrisLootTable.class);
|
||||||
|
try (MockedStatic<J> scheduling = mockStatic(J.class)) {
|
||||||
|
assertThrows(IllegalStateException.class, () -> EngineBukkitOps.addItems(engine, false, inventory,
|
||||||
|
new KList<>(table), InventorySlotType.STORAGE, world, 33, 64, -17));
|
||||||
|
|
||||||
|
scheduling.verify(() -> J.isOwnedByCurrentRegion(world, 2, -2));
|
||||||
|
verifyNoInteractions(table, inventory, engine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void owningRegionLootCompletesInlineWithoutGlobalDispatch() {
|
||||||
|
Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS);
|
||||||
|
Inventory inventory = mock(Inventory.class);
|
||||||
|
World world = mock(World.class);
|
||||||
|
when(inventory.getContents()).thenReturn(new ItemStack[0]);
|
||||||
|
try (MockedStatic<J> scheduling = mockStatic(J.class);
|
||||||
|
MockedStatic<IrisLootEvent> events = mockStatic(IrisLootEvent.class)) {
|
||||||
|
scheduling.when(() -> J.isOwnedByCurrentRegion(world, 2, -2)).thenReturn(true);
|
||||||
|
|
||||||
|
EngineBukkitOps.addItems(engine, false, inventory, new KList<>(), InventorySlotType.STORAGE, world, 33, 64, -17);
|
||||||
|
|
||||||
|
verify(inventory).setContents(any(ItemStack[].class));
|
||||||
|
scheduling.verify(() -> J.isOwnedByCurrentRegion(world, 2, -2));
|
||||||
|
scheduling.verifyNoMoreInteractions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void foliaLootEventRunsOnTheOwningRegionWithoutGlobalDispatch() {
|
||||||
|
World world = mock(World.class);
|
||||||
|
Inventory inventory = mock(Inventory.class);
|
||||||
|
PluginManager manager = mock(PluginManager.class);
|
||||||
|
Thread owner = Thread.currentThread();
|
||||||
|
AtomicInteger eventCalls = new AtomicInteger();
|
||||||
|
try (MockedStatic<J> scheduling = mockStatic(J.class);
|
||||||
|
MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) {
|
||||||
|
scheduling.when(() -> J.isOwnedByCurrentRegion(world, 2, -2)).thenReturn(true);
|
||||||
|
bukkit.when(Bukkit::getPluginManager).thenReturn(manager);
|
||||||
|
doAnswer(invocation -> {
|
||||||
|
assertSame(owner, Thread.currentThread());
|
||||||
|
eventCalls.incrementAndGet();
|
||||||
|
return null;
|
||||||
|
}).when(manager).callEvent(any(LootGenerateEvent.class));
|
||||||
|
|
||||||
|
assertFalse(IrisLootEvent.callLootEvent(new KList<>(), inventory, world, 33, 64, -17));
|
||||||
|
|
||||||
|
assertEquals(1, eventCalls.get());
|
||||||
|
scheduling.verify(() -> J.isOwnedByCurrentRegion(world, 2, -2));
|
||||||
|
scheduling.verifyNoMoreInteractions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void lootEventRetainsListenerChangesAndCancellation() {
|
||||||
|
World world = mock(World.class);
|
||||||
|
Inventory inventory = mock(Inventory.class);
|
||||||
|
PluginManager manager = mock(PluginManager.class);
|
||||||
|
ItemStack original = item(2);
|
||||||
|
ItemStack replacement = item(3);
|
||||||
|
KList<ItemStack> items = new KList<>(original);
|
||||||
|
try (MockedStatic<J> scheduling = mockStatic(J.class);
|
||||||
|
MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) {
|
||||||
|
scheduling.when(() -> J.isOwnedByCurrentRegion(world, 2, -2)).thenReturn(true);
|
||||||
|
bukkit.when(Bukkit::getPluginManager).thenReturn(manager);
|
||||||
|
doAnswer(invocation -> {
|
||||||
|
LootGenerateEvent event = invocation.getArgument(0);
|
||||||
|
event.setLoot(List.of(replacement));
|
||||||
|
event.setCancelled(true);
|
||||||
|
return null;
|
||||||
|
}).when(manager).callEvent(any(LootGenerateEvent.class));
|
||||||
|
|
||||||
|
assertTrue(IrisLootEvent.callLootEvent(items, inventory, world, 33, 64, -17));
|
||||||
|
|
||||||
|
assertEquals(List.of(replacement), items);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public void failedLootRollKeepsPendingMarkerUntilSuccessfulRetry() {
|
||||||
|
BukkitTestServer.install();
|
||||||
|
AtomicBoolean pending = new AtomicBoolean(true);
|
||||||
|
AtomicInteger attempts = new AtomicInteger();
|
||||||
|
Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS);
|
||||||
|
when(engine.getMetrics()).thenReturn(new EngineMetrics(8));
|
||||||
|
World world = mock(World.class);
|
||||||
|
when(world.getMaxHeight()).thenReturn(256);
|
||||||
|
Chunk chunk = mock(Chunk.class);
|
||||||
|
when(chunk.getWorld()).thenReturn(world);
|
||||||
|
Block block = mock(Block.class);
|
||||||
|
BlockData state = mock(BlockData.class);
|
||||||
|
org.bukkit.block.Chest chest = mock(org.bukkit.block.Chest.class);
|
||||||
|
Inventory inventory = mock(Inventory.class);
|
||||||
|
when(chunk.getBlock(1, 64, 1)).thenReturn(block);
|
||||||
|
when(block.getBlockData()).thenReturn(state);
|
||||||
|
when(block.getX()).thenReturn(1);
|
||||||
|
when(block.getZ()).thenReturn(1);
|
||||||
|
when(block.getState()).thenReturn(chest);
|
||||||
|
when(chest.getInventory()).thenReturn(inventory);
|
||||||
|
when(inventory.getContents()).thenReturn(new ItemStack[0]);
|
||||||
|
MantleChunk<Matter> mantle = spy(new MantleChunk<>(16, 0, 0, mock(MantleDataAdapter.class), MantleHooks.NONE));
|
||||||
|
doAnswer(invocation -> {
|
||||||
|
if (pending.get()) {
|
||||||
|
Consumer4<Integer, Integer, Integer, MatterUpdate> visitor = invocation.getArgument(1);
|
||||||
|
visitor.accept(1, 64, 1, UpdateMatter.ON);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}).when(mantle).iterate(eq(MatterUpdate.class), any());
|
||||||
|
doAnswer(invocation -> {
|
||||||
|
pending.set(false);
|
||||||
|
return null;
|
||||||
|
}).when(mantle).deleteSlices(MatterUpdate.class);
|
||||||
|
IrisLootTable table = mock(IrisLootTable.class);
|
||||||
|
IllegalStateException failure = new IllegalStateException("Loot provider unavailable");
|
||||||
|
when(table.getLoot(false, 0L, InventorySlotType.STORAGE, world, 1, 64, 1)).thenAnswer(invocation -> {
|
||||||
|
if (attempts.getAndIncrement() < 2) {
|
||||||
|
throw failure;
|
||||||
|
}
|
||||||
|
return new KList<ItemStack>();
|
||||||
|
});
|
||||||
|
KList<IrisLootTable> tables = new KList<>(table);
|
||||||
|
PluginManager manager = mock(PluginManager.class);
|
||||||
|
try (MockedStatic<RegistryAccess> registries = mockStatic(RegistryAccess.class)) {
|
||||||
|
registries.when(RegistryAccess::registryAccess).thenReturn(BukkitTestServer.registryAccess());
|
||||||
|
try (MockedStatic<EngineBukkitOps> operations = mockStatic(EngineBukkitOps.class);
|
||||||
|
MockedStatic<BukkitBlockResolution> resolution = mockStatic(BukkitBlockResolution.class);
|
||||||
|
MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
|
||||||
|
MockedStatic<IrisLootEvent> events = mockStatic(IrisLootEvent.class);
|
||||||
|
MockedStatic<J> scheduling = mockStatic(J.class);
|
||||||
|
MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
|
||||||
|
bukkit.when(Bukkit::getPluginManager).thenReturn(manager);
|
||||||
|
logging.when(() -> IrisLogging.errorOnce(anyString(), anyString())).thenReturn(true, false);
|
||||||
|
scheduling.when(() -> J.isOwnedByCurrentRegion(world, 0, 0)).thenReturn(true);
|
||||||
|
resolution.when(() -> BukkitBlockResolution.isStorage(state)).thenReturn(true);
|
||||||
|
resolution.when(() -> BukkitBlockResolution.isStorageChest(state)).thenReturn(true);
|
||||||
|
operations.when(() -> EngineBukkitOps.isCanonicalContainer(block)).thenReturn(true);
|
||||||
|
operations.when(() -> EngineBukkitOps.getLootTables(eq(engine), any(RNG.class), eq(block), eq(mantle), eq(false))).thenReturn(tables);
|
||||||
|
operations.when(() -> EngineBukkitOps.addItems(engine, false, inventory, tables, InventorySlotType.STORAGE, world, 1, 64, 1)).thenCallRealMethod();
|
||||||
|
operations.when(() -> EngineBukkitOps.populateInventory(eq(engine), eq(inventory), any(), any())).thenCallRealMethod();
|
||||||
|
operations.when(() -> EngineBukkitOps.update(engine, 1, 64, 1, chunk, mantle)).thenCallRealMethod();
|
||||||
|
operations.when(() -> EngineBukkitOps.materializeUpdates(engine, chunk, mantle)).thenCallRealMethod();
|
||||||
|
operations.when(() -> EngineBukkitOps.materializeUpdates(eq(engine), eq(chunk), eq(mantle), any(EngineBukkitOps.UpdateDispatcher.class))).thenCallRealMethod();
|
||||||
|
operations.when(() -> EngineBukkitOps.runMaterializationPasses(eq(mantle), any(Runnable.class), any(Runnable.class), any(Runnable.class))).thenCallRealMethod();
|
||||||
|
Runnable pass = () -> EngineBukkitOps.runMaterializationPasses(mantle, () -> {}, () -> {},
|
||||||
|
() -> EngineBukkitOps.materializeUpdates(engine, chunk, mantle));
|
||||||
|
|
||||||
|
assertSame(failure, assertThrows(IllegalStateException.class, pass::run));
|
||||||
|
assertTrue(pending.get());
|
||||||
|
assertFalse(mantle.isFlagged(MantleFlag.UPDATE));
|
||||||
|
assertFalse(mantle.isFlagged(MantleFlag.ETCHED));
|
||||||
|
assertSame(failure, assertThrows(IllegalStateException.class, pass::run));
|
||||||
|
assertTrue(pending.get());
|
||||||
|
logging.verify(() -> IrisLogging.reportError(failure));
|
||||||
|
|
||||||
|
pass.run();
|
||||||
|
pass.run();
|
||||||
|
|
||||||
|
assertFalse(pending.get());
|
||||||
|
assertTrue(mantle.isFlagged(MantleFlag.UPDATE));
|
||||||
|
assertTrue(mantle.isFlagged(MantleFlag.ETCHED));
|
||||||
|
assertEquals(3, attempts.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ItemStack item(int amount) {
|
||||||
|
ItemStack item = mock(ItemStack.class);
|
||||||
|
AtomicInteger count = new AtomicInteger(amount);
|
||||||
|
when(item.getType()).thenReturn(Material.DIAMOND);
|
||||||
|
when(item.getAmount()).thenAnswer(invocation -> count.get());
|
||||||
|
when(item.clone()).thenAnswer(invocation -> item(count.get()));
|
||||||
|
doAnswer(invocation -> {
|
||||||
|
count.set(invocation.getArgument(0));
|
||||||
|
return null;
|
||||||
|
}).when(item).setAmount(anyInt());
|
||||||
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Block chestBlock(int x, int z, Chest.Type type, BlockFace facing) {
|
private Block chestBlock(int x, int z, Chest.Type type, BlockFace facing) {
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package art.arcane.iris.world.loot;
|
||||||
|
|
||||||
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.NamespacedKey;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import org.bukkit.loot.LootTable;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class IrisVanillaLootTableTest {
|
||||||
|
@Test
|
||||||
|
public void registryReplacementRemovalAndRestorationApplyToTheNextContainer() {
|
||||||
|
NamespacedKey key = new NamespacedKey("test", "reloadable_loot");
|
||||||
|
World world = mock(World.class);
|
||||||
|
LootTable before = mock(LootTable.class);
|
||||||
|
LootTable after = mock(LootTable.class);
|
||||||
|
ItemStack previousItem = mock(ItemStack.class);
|
||||||
|
ItemStack currentItem = mock(ItemStack.class);
|
||||||
|
when(before.populateLoot(any(), any())).thenReturn(List.of(previousItem));
|
||||||
|
when(after.populateLoot(any(), any())).thenReturn(List.of(currentItem));
|
||||||
|
|
||||||
|
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
|
||||||
|
MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
|
||||||
|
IrisVanillaLootTable table = new IrisVanillaLootTable(key.toString());
|
||||||
|
bukkit.when(() -> Bukkit.getLootTable(key)).thenReturn(before);
|
||||||
|
assertSame(previousItem, table.getLoot(false, 1L, InventorySlotType.STORAGE, world, 0, 64, 0).getFirst());
|
||||||
|
|
||||||
|
bukkit.when(() -> Bukkit.getLootTable(key)).thenReturn(after);
|
||||||
|
assertSame(currentItem, table.getLoot(false, 1L, InventorySlotType.STORAGE, world, 1, 64, 0).getFirst());
|
||||||
|
|
||||||
|
bukkit.when(() -> Bukkit.getLootTable(key)).thenReturn(null);
|
||||||
|
assertTrue(table.getLoot(false, 1L, InventorySlotType.STORAGE, world, 2, 64, 0).isEmpty());
|
||||||
|
|
||||||
|
bukkit.when(() -> Bukkit.getLootTable(key)).thenReturn(after);
|
||||||
|
assertSame(currentItem, table.getLoot(false, 1L, InventorySlotType.STORAGE, world, 3, 64, 0).getFirst());
|
||||||
|
verify(before).populateLoot(any(), any());
|
||||||
|
verify(after, times(2)).populateLoot(any(), any());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void failedTableGenerationDoesNotRetainTheReplacedTable() {
|
||||||
|
NamespacedKey key = new NamespacedKey("test", "repaired_loot");
|
||||||
|
World world = mock(World.class);
|
||||||
|
LootTable failed = mock(LootTable.class);
|
||||||
|
LootTable repaired = mock(LootTable.class);
|
||||||
|
ItemStack item = mock(ItemStack.class);
|
||||||
|
IllegalStateException failure = new IllegalStateException("Loot generation failed");
|
||||||
|
when(failed.populateLoot(any(), any())).thenThrow(failure);
|
||||||
|
when(repaired.populateLoot(any(), any())).thenReturn(List.of(item));
|
||||||
|
|
||||||
|
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) {
|
||||||
|
IrisVanillaLootTable table = new IrisVanillaLootTable(key.toString());
|
||||||
|
bukkit.when(() -> Bukkit.getLootTable(key)).thenReturn(failed);
|
||||||
|
assertSame(failure, assertThrows(IllegalStateException.class,
|
||||||
|
() -> table.getLoot(false, 1L, InventorySlotType.STORAGE, world, 0, 64, 0)));
|
||||||
|
|
||||||
|
bukkit.when(() -> Bukkit.getLootTable(key)).thenReturn(repaired);
|
||||||
|
assertSame(item, table.getLoot(false, 1L, InventorySlotType.STORAGE, world, 0, 64, 0).getFirst());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user