Studio fixes.

- Fixes the Object and Jigsaw studio errors from the coretapper i made.
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-07 15:23:25 -04:00
1 parent 0dcd1900be
commit 894cef73a6
15 files changed
+1628 -83

No files matched your search

@@ -159,6 +159,17 @@ public final class ObjectStudioLayout {
c.getInt("h"),
c.getInt("d")
);
IrisData source = sources.get(pack);
File objectFile = source == null ? null : source.getObjectLoader().findFile(cell.key());
if (objectFile == null) {
return null;
}
IrisBlockVector size = IrisObject.sampleSize(objectFile);
if (cell.w() != Math.max(1, size.getBlockX())
|| cell.h() != Math.max(1, size.getBlockY())
|| cell.d() != Math.max(1, size.getBlockZ())) {
return null;
}
stored.add(cell);
storedIds.add(cell.pack() + "/" + cell.key());
}
@@ -217,6 +228,26 @@ public final class ObjectStudioLayout {
return padding;
}
public ObjectStudioLayout atFloor(int floorY) {
int originY = Math.addExact(floorY, 1);
boolean unchanged = true;
for (GridCell cell : cells) {
if (cell.originY() != originY) {
unchanged = false;
break;
}
}
if (unchanged) {
return this;
}
List<GridCell> placed = new ArrayList<>(cells.size());
for (GridCell cell : cells) {
placed.add(new GridCell(cell.pack(), cell.key(), cell.originX(), originY, cell.originZ(),
cell.w(), cell.h(), cell.d()));
}
return new ObjectStudioLayout(padding, rowWidthCap, placed);
}
public List<GridCell> cells() {
return cells;
}
@@ -3815,16 +3815,15 @@ public final class JigsawStudioService implements IrisService, JigsawStudioMenuC
message(player, "Iris Jigsaw Studio is not active in this world.");
return false;
}
List<JigsawStudioBounds> bounds = new ArrayList<>(pieces.size());
List<JigsawStudioPreviewRenderer.PreviewBounds> bounds = new ArrayList<>(pieces.size());
for (PlacedStructurePiece piece : pieces) {
int width = Math.addExact(Math.subtractExact(piece.getMaxX(), piece.getMinX()), 1);
int height = Math.addExact(Math.subtractExact(piece.getMaxY(), piece.getMinY()), 1);
int depth = Math.addExact(Math.subtractExact(piece.getMaxZ(), piece.getMinZ()), 1);
bounds.add(new JigsawStudioBounds(
bounds.add(new JigsawStudioPreviewRenderer.PreviewBounds(
piece.getMinX(),
piece.getMinY(),
piece.getMinZ(),
new JigsawStudioCellDimensions(width, height, depth)));
piece.getMaxX(),
piece.getMaxY(),
piece.getMaxZ()));
}
assemblyPreviews.put(player.getUniqueId(), new AssemblyPreview(
player.getWorld().getUID(),
@@ -7552,10 +7551,6 @@ public final class JigsawStudioService implements IrisService, JigsawStudioMenuC
return;
}
JigsawStudioPreviewRenderer.PreviewBounds preview = evaluation.previewBounds();
JigsawStudioCellDimensions dimensions = new JigsawStudioCellDimensions(
preview.maximumX() - preview.minimumX() + 1,
preview.maximumY() - preview.minimumY() + 1,
preview.maximumZ() - preview.minimumZ() + 1);
Color color = switch (evaluation.state()) {
case VALID -> ASSEMBLY_PREVIEW_COLOR;
case PENDING, WARNING, STALE -> LIVE_PREVIEW_WARNING_COLOR;
@@ -7564,11 +7559,7 @@ public final class JigsawStudioService implements IrisService, JigsawStudioMenuC
drawBounds(
player,
playerLocation,
new JigsawStudioBounds(
preview.minimumX(),
preview.minimumY(),
preview.minimumZ(),
dimensions),
preview,
color,
0.85F,
2.0D,
@@ -7589,7 +7580,7 @@ public final class JigsawStudioService implements IrisService, JigsawStudioMenuC
assemblyPreviews.remove(player.getUniqueId(), preview);
return;
}
for (JigsawStudioBounds bounds : preview.bounds()) {
for (JigsawStudioPreviewRenderer.PreviewBounds bounds : preview.bounds()) {
drawBounds(player, playerLocation, bounds, ASSEMBLY_PREVIEW_COLOR, 0.85F, 2.0D, budget);
if (budget.empty()) {
return;
@@ -7682,12 +7673,26 @@ public final class JigsawStudioService implements IrisService, JigsawStudioMenuC
double step,
ParticleBudget budget
) {
double minX = bounds.originX();
double minY = bounds.originY();
double minZ = bounds.originZ();
double maxX = bounds.maxX() + 1.0D;
double maxY = bounds.maxY() + 1.0D;
double maxZ = bounds.maxZ() + 1.0D;
drawBounds(player, playerLocation, new JigsawStudioPreviewRenderer.PreviewBounds(
bounds.originX(), bounds.originY(), bounds.originZ(), bounds.maxX(), bounds.maxY(), bounds.maxZ()),
color, size, step, budget);
}
private static void drawBounds(
Player player,
Location playerLocation,
JigsawStudioPreviewRenderer.PreviewBounds bounds,
Color color,
float size,
double step,
ParticleBudget budget
) {
double minX = bounds.minimumX();
double minY = bounds.minimumY();
double minZ = bounds.minimumZ();
double maxX = bounds.maximumX() + 1.0D;
double maxY = bounds.maximumY() + 1.0D;
double maxZ = bounds.maximumZ() + 1.0D;
drawLine(player, playerLocation, minX, minY, minZ, maxX, minY, minZ, color, size, step, budget);
drawLine(player, playerLocation, minX, minY, maxZ, maxX, minY, maxZ, color, size, step, budget);
drawLine(player, playerLocation, minX, maxY, minZ, maxX, maxY, minZ, color, size, step, budget);
@@ -8657,7 +8662,7 @@ public final class JigsawStudioService implements IrisService, JigsawStudioMenuC
private record AssemblyPreview(
UUID worldId,
long expiresAtMillis,
List<JigsawStudioBounds> bounds
List<JigsawStudioPreviewRenderer.PreviewBounds> bounds
) {
AssemblyPreview {
Objects.requireNonNull(worldId, "Jigsaw Studio preview world");
@@ -107,7 +107,7 @@ public class ObjectStudioSaveService implements IrisService {
}
String packKey = engine.getDimension() == null ? null : engine.getDimension().getLoadKey();
studios.put(world.getUID(), new ActiveStudio(world.getUID(), layout, objectsDirs, packKey));
studios.put(world.getUID(), new ActiveStudio(world.getUID(), generator, objectsDirs, packKey));
IrisLogging.debug("Object Studio live-save registered: world=%s cells=%d packs=%d",
world.getName(), layout.cells().size(), objectsDirs.size());
}
@@ -219,7 +219,7 @@ public class ObjectStudioSaveService implements IrisService {
return;
}
IrisObject snapshot = new IrisObject(cell.w(), cell.h(), cell.d());
IrisObject snapshot = studio.generator.createCapture(cell);
int originX = cell.originX();
int originY = cell.originY();
int originZ = cell.originZ();
@@ -324,14 +324,16 @@ public class ObjectStudioSaveService implements IrisService {
private static final class ActiveStudio {
final UUID worldId;
final ObjectStudioGenerator generator;
final ObjectStudioLayout layout;
final Map<String, File> objectsDirs;
final String packKey;
final Map<String, Long> hashes = new ConcurrentHashMap<>();
ActiveStudio(UUID worldId, ObjectStudioLayout layout, Map<String, File> objectsDirs, String packKey) {
ActiveStudio(UUID worldId, ObjectStudioGenerator generator, Map<String, File> objectsDirs, String packKey) {
this.worldId = worldId;
this.layout = layout;
this.generator = generator;
this.layout = generator.getLayout();
this.objectsDirs = objectsDirs;
this.packKey = packKey;
}
@@ -20,6 +20,7 @@ package art.arcane.iris.engine;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.engine.data.cache.Cache;
import art.arcane.iris.engine.history.SavedBiomeUnavailableException;
import art.arcane.iris.engine.platform.EngineBukkitOps;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import art.arcane.iris.spi.IrisLogging;
@@ -262,7 +263,7 @@ final class WorldChunkMaintenance {
}
}
private void updateChunkRegion(World world, int chunkX, int chunkZ) {
void updateChunkRegion(World world, int chunkX, int chunkZ) {
if (world == null || !world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) {
return;
}
@@ -274,7 +275,14 @@ final class WorldChunkMaintenance {
warmupMantleChunkAsync(chunkX, chunkZ);
return;
}
EngineBukkitOps.updateChunk(manager.getEngine(), chunk);
try {
EngineBukkitOps.updateChunk(manager.getEngine(), chunk);
} catch (SavedBiomeUnavailableException unavailable) {
if (!unavailable.isLoading() || unavailable.getSuppressed().length != 0) {
throw unavailable;
}
return;
}
}
if (manager.entitySpawner.isEntitySpawningEnabledForCurrentWorld()) {
@@ -54,6 +54,7 @@ import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -93,22 +94,22 @@ final class WorldEntitySpawner {
}
if (manager.cl.flip()) {
CompletableFuture<Integer> future = new CompletableFuture<>();
try {
World realWorld = BukkitWorldBinding.world(manager.getEngine().getWorld());
if (realWorld == null) {
manager.entityCount = 0;
manager.entityCountValid = false;
} else {
CompletableFuture<Integer> future = new CompletableFuture<>();
boolean scheduled = J.runGlobal(() -> {
try {
future.complete(livingEntityCount(realWorld));
} catch (Throwable ex) {
future.completeExceptionally(ex);
}
});
boolean scheduled = J.runGlobal(managedCompletion(
"bukkit_world_manager_entity_count", future, () -> livingEntityCount(realWorld)));
if (scheduled) {
manager.entityCount = future.get(2, TimeUnit.SECONDS);
Integer count = future.get(2, TimeUnit.SECONDS);
if (count == null || manager.getEngine().isClosing() || manager.getEngine().isClosed()) {
manager.entityCountValid = false;
return false;
}
manager.entityCount = count;
manager.entityCountValid = true;
resetEntityCountFailures();
} else {
@@ -126,6 +127,8 @@ final class WorldEntitySpawner {
reportEntityCountFailure("Failed to count entities; pausing Iris entity spawning until a complete count is available.", cause);
} catch (Throwable e) {
reportEntityCountFailure("Failed to count entities; pausing Iris entity spawning until a complete count is available.", e);
} finally {
future.complete(null);
}
}
@@ -193,7 +196,7 @@ final class WorldEntitySpawner {
return false;
}
CompletableFuture<Void> future = new CompletableFuture<>();
CompletableFuture<Boolean> future = new CompletableFuture<>();
AtomicBoolean failureReported = new AtomicBoolean();
future.whenComplete((ignored, failure) -> {
if (failure != null) {
@@ -202,32 +205,28 @@ final class WorldEntitySpawner {
});
boolean scheduled;
try {
scheduled = J.runRegion(world, chunkX, chunkZ, () -> {
try {
if (!world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) {
future.complete(null);
return;
}
spawnIn(world.getChunkAt(chunkX, chunkZ), initial);
future.complete(null);
} catch (Throwable e) {
future.completeExceptionally(e);
}
});
scheduled = J.runRegion(world, chunkX, chunkZ, managedCompletion(
"bukkit_world_manager_entity_spawn", future, () -> {
if (!world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) {
return true;
}
spawnIn(world.getChunkAt(chunkX, chunkZ), initial);
return true;
}));
} catch (Throwable e) {
future.complete(null);
IrisLogging.reportError("Failed to schedule an Iris entity spawn for chunk " + chunkX + "," + chunkZ + ".", e);
return false;
}
if (!scheduled) {
future.complete(null);
IrisLogging.debug("Skipped Iris entity spawning because the region task was not accepted for chunk " + chunkX + "," + chunkZ + ".");
return false;
}
try {
future.get(5, TimeUnit.SECONDS);
return true;
return Boolean.TRUE.equals(future.get(5, TimeUnit.SECONDS));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
@@ -238,9 +237,28 @@ final class WorldEntitySpawner {
Throwable cause = e.getCause() == null ? e : e.getCause();
reportSpawnFailure(chunkX, chunkZ, cause, failureReported);
return false;
} finally {
future.complete(null);
}
}
private <T> Runnable managedCompletion(String operation, CompletableFuture<T> future, Supplier<T> task) {
Runnable managed = manager.managedTask(operation, () -> {
if (!future.isDone()) {
future.complete(task.get());
}
}, () -> future.complete(null));
return () -> {
try {
managed.run();
} catch (Throwable failure) {
if (!future.completeExceptionally(failure)) {
IrisLogging.reportError("Failed to finish " + operation + ".", failure);
}
}
};
}
private void reportEntityCountFailure(String message, Throwable error) {
manager.entityCountValid = false;
if (error != null) {
@@ -629,6 +629,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
lock.lock();
try {
throwIfInitializationFailed();
if (setup.get()) {
return getEngine();
}
@@ -636,6 +637,19 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
getWorld().setRawWorldSeed(world.getSeed());
setupEngine();
try {
computeStudioGenerator();
} catch (Throwable failure) {
initializationFailure = failure;
try {
engine.close();
} catch (Throwable closeFailure) {
if (failure != closeFailure) {
failure.addSuppressed(closeFailure);
}
}
throw propagateEngineSetupFailure(failure);
}
setup.set(true);
this.hotloader = shouldRunStudioHotload(studio, closing, jigsawStudioActive) ? new Looper() {
@Override
@@ -935,11 +949,11 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
if (engine != expectedEngine) {
throw new IllegalStateException("Iris noise generation belongs to a replaced engine runtime.");
}
computeStudioGenerator();
StudioGenerator selected = computeStudioGenerator();
if (engine != expectedEngine) {
throw new IllegalStateException("Iris noise generation changed engine runtime during Studio resolution.");
}
return studioGenerator;
return selected;
}
public void withExclusiveControl(Runnable r) {
@@ -1077,10 +1091,10 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
try {
Engine engine = getEngine(world);
computeStudioGenerator();
StudioGenerator selected = computeStudioGenerator();
TerrainChunk tc = TerrainChunk.create(d);
if (studioGenerator != null) {
studioGenerator.generateChunk(engine, tc, x, z);
if (selected != null) {
selected.generateChunk(engine, tc, x, z);
} else {
ChunkDataHunkHolder blocks = new ChunkDataHunkHolder(d);
Hunk<PlatformBiome> biomes = Hunk.viewBiomes(tc);
@@ -1191,7 +1205,14 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
}
}
private void computeStudioGenerator() {
private StudioGenerator computeStudioGenerator() {
if (!studio) {
return studioGenerator;
}
return computeStudioGeneratorForStudio();
}
private synchronized StudioGenerator computeStudioGeneratorForStudio() {
String packKey = getEngine().getDimension().getLoadKey();
JigsawStudioActivation.Request jigsawRequest = studio
? JigsawStudioActivation.getGeneratorRequest(packKey)
@@ -1204,7 +1225,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
lastJigsawStudioRequestId = jigsawRequest.requestId();
lastMode = null;
}
return;
return studioGenerator;
}
}
if (lastJigsawStudioRequestId != null) {
@@ -1221,9 +1242,10 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
desired = StudioMode.OBJECT_BUFFET;
}
if (!desired.equals(lastMode)) {
lastMode = desired;
desired.inject(this);
lastMode = desired;
}
return studioGenerator;
}
@NotNull
@@ -60,6 +60,7 @@ import art.arcane.volmlib.util.matter.MatterCavern;
import art.arcane.volmlib.util.matter.MatterUpdate;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import io.papermc.lib.PaperLib;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.Location;
@@ -228,12 +229,33 @@ public final class EngineBukkitOps {
}
}
mantleChunk.iterate(MatterUpdate.class, (x, yf, z, value) -> {
int y = yf + engine.getWorld().minHeight();
if (value != null && value.isUpdate()) {
dispatcher.update(x, y, z);
IntArrayList completed = new IntArrayList();
try {
mantleChunk.iterate(MatterUpdate.class, (x, yf, z, value) -> {
int y = yf + engine.getWorld().minHeight();
if (value != null && value.isUpdate()) {
dispatcher.update(x, y, z);
completed.add((yf << 8) | ((x & 15) << 4) | (z & 15));
}
});
} catch (RuntimeException | Error failure) {
try {
for (int index = 0; index < completed.size(); index++) {
int position = completed.getInt(index);
int y = position >>> 8;
Matter section = mantleChunk.get(y >> 4);
if (section != null && section.hasSlice(MatterUpdate.class)) {
section.<MatterUpdate>getSlice(MatterUpdate.class)
.set((position >> 4) & 15, y & 15, position & 15, null);
}
}
} catch (RuntimeException | Error cleanupFailure) {
if (cleanupFailure != failure) {
failure.addSuppressed(cleanupFailure);
}
}
});
throw failure;
}
mantleChunk.deleteSlices(MatterUpdate.class);
engine.getMetrics().getUpdates().put(stopwatch.getMilliseconds());
}
@@ -60,7 +60,7 @@ public class ObjectStudioGenerator extends EnginedStudioGenerator {
private final PlatformBlockState marker;
private final AtomicBoolean layoutBuilt = new AtomicBoolean(false);
private final Object layoutLock = new Object();
private final Map<String, IrisObject> objectCache = new ConcurrentHashMap<>();
private final Map<String, DisplayObject> objectCache = new ConcurrentHashMap<>();
private final Map<String, IrisData> packData = new ConcurrentHashMap<>();
private volatile ObjectStudioLayout layout;
@@ -130,7 +130,7 @@ public class ObjectStudioGenerator extends EnginedStudioGenerator {
paintFrame(cell, tc, plinthY, chunkWorldX, chunkWorldZ, minHeight, maxHeight);
IrisObject object = loadObject(cell);
DisplayObject object = loadObject(cell);
if (object != null) {
placeSlice(object, cell, tc, chunkWorldX, chunkWorldZ, minHeight, maxHeight);
}
@@ -196,24 +196,19 @@ public class ObjectStudioGenerator extends EnginedStudioGenerator {
}
}
private void placeSlice(IrisObject object, GridCell cell, TerrainChunk tc, int chunkWorldX, int chunkWorldZ, int minHeight, int maxHeight) {
VectorMap<PlatformBlockState> blocks = object.getBlocks();
private void placeSlice(DisplayObject object, GridCell cell, TerrainChunk tc, int chunkWorldX, int chunkWorldZ, int minHeight, int maxHeight) {
VectorMap<PlatformBlockState> blocks = object.source().getBlocks();
if (blocks == null || blocks.isEmpty()) return;
Vector3i center = object.getCenter();
int centerX = center == null ? 0 : center.getBlockX();
int centerY = center == null ? 0 : center.getBlockY();
int centerZ = center == null ? 0 : center.getBlockZ();
int originX = cell.originX();
int originY = cell.originY();
int originZ = cell.originZ();
for (Map.Entry<IrisBlockVector, PlatformBlockState> entry : blocks) {
IrisBlockVector signed = entry.getKey();
int worldX = originX + signed.getBlockX() + centerX;
int worldY = originY + signed.getBlockY() + centerY;
int worldZ = originZ + signed.getBlockZ() + centerZ;
int worldX = originX + signed.getBlockX() - object.minimumX();
int worldY = originY + signed.getBlockY() - object.minimumY();
int worldZ = originZ + signed.getBlockZ() - object.minimumZ();
if (worldX < chunkWorldX || worldX > chunkWorldX + 15) continue;
if (worldZ < chunkWorldZ || worldZ > chunkWorldZ + 15) continue;
@@ -226,17 +221,29 @@ public class ObjectStudioGenerator extends EnginedStudioGenerator {
}
}
private IrisObject loadObject(GridCell cell) {
public IrisObject createCapture(GridCell cell) {
DisplayObject displayed = loadObject(cell);
if (displayed == null) {
throw new IllegalStateException("Object Studio source is unavailable: " + cell.pack() + "/" + cell.key());
}
IrisObject capture = new IrisObject(cell.w(), cell.h(), cell.d());
capture.setCenter(new Vector3i(-displayed.minimumX(), -displayed.minimumY(), -displayed.minimumZ()));
return capture;
}
private DisplayObject loadObject(GridCell cell) {
String cacheKey = cell.pack() + "/" + cell.key();
IrisObject cached = objectCache.get(cacheKey);
DisplayObject cached = objectCache.get(cacheKey);
if (cached != null) return cached;
IrisData data = packData.get(cell.pack());
if (data == null) return null;
IrisObject loaded = data.getObjectLoader().load(cell.key());
if (loaded != null) {
objectCache.put(cacheKey, loaded);
DisplayObject displayed = DisplayObject.of(loaded);
DisplayObject existing = objectCache.putIfAbsent(cacheKey, displayed);
return existing == null ? displayed : existing;
}
return loaded;
return null;
}
public Map<String, IrisData> getPackData() {
@@ -257,6 +264,9 @@ public class ObjectStudioGenerator extends EnginedStudioGenerator {
layout = resumed;
} else {
layout = ObjectStudioLayout.build(sources, padding);
}
layout = layout.atFloor(Math.max(engine.getMinHeight(), ObjectStudioLayout.FLOOR_Y));
if (layout != resumed) {
layout.save(layoutFile);
}
layoutBuilt.set(true);
@@ -300,4 +310,26 @@ public class ObjectStudioGenerator extends EnginedStudioGenerator {
}
return new IrisBlockVector(maxX, 0, maxZ);
}
private record DisplayObject(IrisObject source, int minimumX, int minimumY, int minimumZ) {
private static DisplayObject of(IrisObject object) {
int minimumX = Integer.MAX_VALUE;
int minimumY = Integer.MAX_VALUE;
int minimumZ = Integer.MAX_VALUE;
for (Map.Entry<IrisBlockVector, PlatformBlockState> entry : object.getBlocks()) {
if (entry.getValue() == null) {
continue;
}
IrisBlockVector position = entry.getKey();
minimumX = Math.min(minimumX, position.getBlockX());
minimumY = Math.min(minimumY, position.getBlockY());
minimumZ = Math.min(minimumZ, position.getBlockZ());
}
if (minimumX == Integer.MAX_VALUE) {
Vector3i center = object.getCenter();
return new DisplayObject(object, -center.getBlockX(), -center.getBlockY(), -center.getBlockZ());
}
return new DisplayObject(object, minimumX, minimumY, minimumZ);
}
}
}