mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-27 06:20:02 +00:00
Merged container loot fixes
- Container loot rolls retain retry state and restore contents after failed writes
This commit is contained in:
commit
57882a3f1c
14 files changed
+875
-255
No files matched your search
@@ -289,6 +289,8 @@ final class WorldChunkMaintenance {
|
||||
boolean scheduled = J.runRegion(world, chunkX, chunkZ, manager.managedTask("bukkit_world_manager_chunk_update", () -> {
|
||||
try {
|
||||
updateChunkRegion(world, chunkX, chunkZ);
|
||||
} catch (RuntimeException | Error failure) {
|
||||
reportMaterializationFailure(new MaterializationTarget(world, chunkX, chunkZ), failure);
|
||||
} finally {
|
||||
chunkUpdateQueue.remove(key);
|
||||
}
|
||||
@@ -508,6 +510,13 @@ final class WorldChunkMaintenance {
|
||||
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 long key() {
|
||||
return Cache.key(x, z);
|
||||
@@ -614,7 +623,7 @@ final class WorldChunkMaintenance {
|
||||
}
|
||||
spawnInitially(chunk);
|
||||
} catch (Throwable failure) {
|
||||
IrisLogging.reportError("Failed to materialize saved chunk at " + target.x() + "," + target.z() + ".", failure);
|
||||
reportMaterializationFailure(target, failure);
|
||||
} finally {
|
||||
synchronized (this) {
|
||||
applying = false;
|
||||
|
||||
@@ -20,7 +20,6 @@ package art.arcane.iris.platform.generation;
|
||||
|
||||
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.integration.Identifier;
|
||||
import art.arcane.iris.integration.ExternalDataSVC;
|
||||
@@ -73,6 +72,7 @@ import org.bukkit.World;
|
||||
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.entity.Player;
|
||||
@@ -82,6 +82,8 @@ import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.loot.Lootable;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@@ -89,9 +91,7 @@ import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import art.arcane.iris.localization.BukkitRuntimeMessages;
|
||||
import art.arcane.iris.localization.IrisLanguage;
|
||||
@@ -124,14 +124,7 @@ public final class EngineBukkitOps {
|
||||
}
|
||||
|
||||
if (!J.isFolia() && !J.isPrimaryThread()) {
|
||||
CompletableFuture<?> scheduled = J.sfut(() -> updateChunk(engine, c, mantle));
|
||||
if (scheduled != null) {
|
||||
try {
|
||||
scheduled.join();
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
}
|
||||
awaitMaterialization(J.sfut(() -> updateChunk(engine, c, mantle)));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -303,21 +296,25 @@ public final class EngineBukkitOps {
|
||||
});
|
||||
|
||||
if (!scheduled) {
|
||||
if (J.isPrimaryThread()) {
|
||||
runnable.run();
|
||||
}
|
||||
return;
|
||||
throw new IllegalStateException("Failed to schedule chunk materialization for "
|
||||
+ contextChunk.getWorld().getName() + " at chunk " + contextChunk.getX() + "," + contextChunk.getZ() + ".");
|
||||
}
|
||||
|
||||
awaitMaterialization(completion);
|
||||
}
|
||||
|
||||
private static void awaitMaterialization(CompletableFuture<?> completion) {
|
||||
try {
|
||||
completion.join();
|
||||
} catch (CompletionException error) {
|
||||
Throwable cause = error.getCause();
|
||||
IrisLogging.reportError(
|
||||
"Failed deferred chunk materialization for " + contextChunk.getWorld().getName()
|
||||
+ " at chunk " + contextChunk.getX() + "," + contextChunk.getZ() + ".",
|
||||
cause == null ? error : cause
|
||||
);
|
||||
if (cause instanceof RuntimeException runtimeFailure) {
|
||||
throw runtimeFailure;
|
||||
}
|
||||
if (cause instanceof Error fatalFailure) {
|
||||
throw fatalFailure;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,8 +348,12 @@ public final class EngineBukkitOps {
|
||||
InventoryHolder m = (InventoryHolder) block.getState();
|
||||
addItems(engine, false, m.getInventory(), tables, slot, c.getWorld(), worldX, y, worldZ);
|
||||
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
} catch (RuntimeException | Error failure) {
|
||||
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 {
|
||||
@@ -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) {
|
||||
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<>();
|
||||
|
||||
for (IrisLootTable i : tables) {
|
||||
@@ -525,57 +530,69 @@ public final class EngineBukkitOps {
|
||||
if (IrisLootEvent.callLootEvent(items, inv, world, x, y, z))
|
||||
return;
|
||||
|
||||
if (PaperLib.isPaper() && engine.getWorld().hasPlatformWorld()) {
|
||||
PaperLib.getChunkAtAsync(BukkitWorldBinding.world(engine.getWorld()), x >> 4, z >> 4).thenAccept((c) -> {
|
||||
Runnable mutation = () -> {
|
||||
for (ItemStack i : items) {
|
||||
inv.addItem(i);
|
||||
}
|
||||
populateInventory(engine, inv, items, LootResolver.containerRng(engine.getSeedManager().getLoot(), x, y, z));
|
||||
}
|
||||
|
||||
scramble(engine, inv, LootResolver.containerRng(engine.getSeedManager().getLoot(), x, y, z));
|
||||
};
|
||||
|
||||
LootMutationTask task = new LootMutationTask(world, x >> 4, z >> 4, mutation);
|
||||
boolean scheduled = dispatchLootMutation(
|
||||
task,
|
||||
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) + ".");
|
||||
}
|
||||
});
|
||||
static void populateInventory(Engine engine, Inventory inv, List<ItemStack> items, RNG rng) {
|
||||
List<BlockState> states = new ArrayList<>(2);
|
||||
InventoryHolder holder = inv.getHolder();
|
||||
if (holder instanceof DoubleChest chest) {
|
||||
snapshotContainer(states, chest.getLeftSide());
|
||||
snapshotContainer(states, chest.getRightSide());
|
||||
} else {
|
||||
for (ItemStack i : items) {
|
||||
inv.addItem(i);
|
||||
snapshotContainer(states, holder);
|
||||
}
|
||||
ItemStack[] contents = null;
|
||||
try {
|
||||
if (states.isEmpty()) {
|
||||
contents = copyContents(inv.getContents());
|
||||
}
|
||||
|
||||
scramble(engine, inv, LootResolver.containerRng(engine.getSeedManager().getLoot(), x, y, z));
|
||||
for (ItemStack i : items) {
|
||||
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(
|
||||
LootMutationTask task,
|
||||
boolean folia,
|
||||
BooleanSupplier primaryThread,
|
||||
Predicate<LootMutationTask> regionScheduler,
|
||||
Consumer<Runnable> syncScheduler) {
|
||||
if (folia) {
|
||||
return regionScheduler.test(task);
|
||||
private static void snapshotContainer(List<BlockState> snapshots, InventoryHolder holder) {
|
||||
if (holder instanceof BlockState state) {
|
||||
snapshots.add(state.getBlock().getState());
|
||||
}
|
||||
}
|
||||
|
||||
if (primaryThread.getAsBoolean()) {
|
||||
task.mutation().run();
|
||||
} else {
|
||||
syncScheduler.accept(task.mutation());
|
||||
private static ItemStack[] copyContents(ItemStack[] contents) {
|
||||
ItemStack[] copy = new ItemStack[contents.length];
|
||||
for (int index = 0; index < contents.length; index++) {
|
||||
copy[index] = contents[index] == null ? null : contents[index].clone();
|
||||
}
|
||||
return true;
|
||||
return copy;
|
||||
}
|
||||
|
||||
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();
|
||||
EngineBukkitOps.addItems(engine, false, holder.getInventory(), tables, InventorySlotType.STORAGE, world, x, y, z);
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
} catch (RuntimeException | Error failure) {
|
||||
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.IrisLootTable;
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import lombok.Getter;
|
||||
@@ -92,6 +91,10 @@ public class IrisLootEvent extends Event {
|
||||
* @return true when the event was canceled
|
||||
*/
|
||||
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();
|
||||
Location loc = new Location(world, x, y, z);
|
||||
if (holder == null) {
|
||||
@@ -106,39 +109,8 @@ public class IrisLootEvent extends Event {
|
||||
|
||||
LootContext context = new LootContext.Builder(loc).build();
|
||||
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.");
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Bukkit.getPluginManager().callEvent(event);
|
||||
|
||||
return event.isCancelled();
|
||||
}
|
||||
|
||||
@@ -33,13 +33,12 @@ public class IrisVanillaLootTable extends IrisLootTable {
|
||||
private final String lootTableKey;
|
||||
|
||||
/**
|
||||
* The resolved table, looked up once. Every chest pull resolved it again - a NamespacedKey parse plus a registry
|
||||
* lookup per container, and a dungeon room is a lot of containers.
|
||||
* Cache the parsed key; resolve its table from the current registry for every container.
|
||||
* <p>
|
||||
* 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.
|
||||
*/
|
||||
private final transient AtomicCache<LootTable> resolvedTable = new AtomicCache<>();
|
||||
private final transient AtomicCache<NamespacedKey> resolvedKey = new AtomicCache<>();
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
@@ -83,10 +82,8 @@ public class IrisVanillaLootTable extends IrisLootTable {
|
||||
}
|
||||
|
||||
private LootTable resolveTable() {
|
||||
return resolvedTable.aquire(() -> {
|
||||
NamespacedKey key = NamespacedKey.fromString(lootTableKey);
|
||||
return key == null ? null : Bukkit.getLootTable(key);
|
||||
});
|
||||
NamespacedKey key = resolvedKey.aquire(() -> NamespacedKey.fromString(lootTableKey));
|
||||
return key == null ? null : Bukkit.getLootTable(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
Reference in new issue
Block a user