diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObject.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObject.java index 4aa79dba4..9d4a37f9e 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisObject.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObject.java @@ -110,7 +110,7 @@ public class IrisObject extends IrisRegistrant { this.d = d; center = new Vector3i(w / 2, h / 2, d / 2); shrinkOffset = new Vector3i(0, 0, 0); - var lock = new ReentrantReadWriteLock(); + ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); readLock = lock.readLock(); writeLock = lock.writeLock(); } @@ -135,17 +135,22 @@ public class IrisObject extends IrisRegistrant { return aabb.aquire(() -> getAABBFor(new IrisBlockVector(w, h, d))); } - public synchronized IrisObject copy() { - IrisObject o = new IrisObject(w, h, d); - o.setLoadKey(getLoadKey()); - o.setLoader(getLoader()); - o.setLoadFile(getLoadFile()); - o.setCenter(getCenter().clone()); + public IrisObject copy() { + readLock.lock(); + try { + IrisObject copy = new IrisObject(w, h, d); + copy.setLoadKey(getLoadKey()); + copy.setLoader(getLoader()); + copy.setLoadFile(getLoadFile()); + copy.setCenter(getCenter().clone()); - blocks.forEach((i, v) -> o.blocks.put(i.clone(), v)); - states.forEach((i, v) -> o.states.put(i.clone(), v.clone())); + blocks.forEach(copy.blocks::put); + states.forEach((position, tile) -> copy.states.put(position, tile.clone())); - return o; + return copy; + } finally { + readLock.unlock(); + } } public void readLegacy(InputStream in) throws IOException { diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacementRunner.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacementRunner.java index 1d6a38deb..8af511f43 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacementRunner.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacementRunner.java @@ -350,7 +350,7 @@ final class IrisObjectPlacementRunner { return -1; } - if (!rawStructurePiece && nativeStructureVetoes(placer, config, spin, translating, translateOffset, ceilingHang, + if (!rawStructurePiece && nativeStructureVetoes(placer, config, spin, placementBounds, translating, translateOffset, ceilingHang, yv < 0 && config.getMode() == ObjectPlaceMode.PAINT, warpMargin, x, y + yrand, z)) { return -1; } @@ -955,7 +955,7 @@ final class IrisObjectPlacementRunner { * near this placement means no per block work at all. Only when the placement envelope meets a piece does the * precise pass run, and the first solid block inside a piece rejects the whole object before any write. */ - private boolean nativeStructureVetoes(IObjectPlacer placer, IrisObjectPlacement config, SpinKernel spin, + private boolean nativeStructureVetoes(IObjectPlacer placer, IrisObjectPlacement config, SpinKernel spin, TransformedBounds bounds, boolean translating, IrisBlockVector translateOffset, boolean ceilingHang, boolean paint, int warpMargin, int x, int y, int z) { Engine engine = placer.getEngine(); @@ -963,23 +963,21 @@ final class IrisObjectPlacementRunner { return false; } - int margin = (Math.max(self.getW(), Math.max(self.getH(), self.getD())) / 2) + 1 + warpMargin; - if (translating) { - margin += Math.max(Math.abs(translateOffset.getBlockX()), - Math.max(Math.abs(translateOffset.getBlockY()), Math.abs(translateOffset.getBlockZ()))); - } - - KList volumes = engine.getNativeStructureVolumes(x - margin, z - margin, x + margin, z + margin); + int minX = x + bounds.minX(); + int maxX = x + bounds.maxX(); + int minZ = z + bounds.minZ(); + int maxZ = z + bounds.maxZ(); + KList volumes = engine.getNativeStructureVolumes(minX, minZ, maxX, maxZ); if (volumes == null || volumes.isEmpty()) { return false; } int worldY = y + engine.getMinHeight(); - int envelopeMinY = paint ? Integer.MIN_VALUE : worldY - margin; - int envelopeMaxY = paint ? Integer.MAX_VALUE : worldY + margin; + int envelopeMinY = paint ? Integer.MIN_VALUE : worldY + bounds.minY() - warpMargin; + int envelopeMaxY = paint ? Integer.MAX_VALUE : worldY + bounds.maxY() + warpMargin; boolean envelopeMeetsPiece = false; for (NativeStructureVolume volume : volumes) { - if (volume.intersects(x - margin, envelopeMinY, z - margin, x + margin, envelopeMaxY, z + margin)) { + if (volume.intersects(minX, envelopeMinY, minZ, maxX, envelopeMaxY, maxZ)) { envelopeMeetsPiece = true; break; } diff --git a/core/src/test/java/art/arcane/iris/engine/object/IrisObjectCopyTest.java b/core/src/test/java/art/arcane/iris/engine/object/IrisObjectCopyTest.java new file mode 100644 index 000000000..07cd45d59 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/object/IrisObjectCopyTest.java @@ -0,0 +1,74 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.common.math.IrisBlockVector; +import art.arcane.iris.util.common.math.Vector3i; +import art.arcane.volmlib.util.collection.KMap; +import org.junit.Test; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +public class IrisObjectCopyTest { + @Test + public void copyWaitsForVolumeMutationToComplete() throws Exception { + IrisObject source = new IrisObject(1, 1, 1); + PlatformBlockState block = mock(PlatformBlockState.class); + CountDownLatch started = new CountDownLatch(1); + try (ExecutorService executor = Executors.newSingleThreadExecutor()) { + Future result; + source.writeLock.lock(); + try { + result = executor.submit(() -> { + started.countDown(); + return source.copy(); + }); + assertTrue(started.await(5, TimeUnit.SECONDS)); + assertThrows(TimeoutException.class, () -> result.get(150, TimeUnit.MILLISECONDS)); + source.setW(3); + source.setCenter(new Vector3i(1, 0, 0)); + source.setUnsigned(2, 0, 0, block); + } finally { + source.writeLock.unlock(); + } + + IrisObject copy = result.get(5, TimeUnit.SECONDS); + assertEquals(3, copy.getW()); + assertEquals(new Vector3i(1, 0, 0), copy.getCenter()); + assertSame(block, copy.getBlocks().get(new IrisBlockVector(1, 0, 0))); + } + } + + @Test + public void copyOwnsItsVolumeCoordinatesAndTiles() { + IrisObject source = new IrisObject(1, 1, 1); + PlatformBlockState block = mock(PlatformBlockState.class); + TileData tile = new TileData("minecraft:chest", new KMap<>()); + tile.getProperties().put("name", "source"); + source.setUnsigned(0, 0, 0, block); + source.setUnsignedTile(0, 0, 0, tile); + + IrisObject copy = source.copy(); + IrisBlockVector position = new IrisBlockVector(0, 0, 0); + TileData copiedTile = copy.getStates().get(position); + assertNotSame(source.getCenter(), copy.getCenter()); + assertNotSame(tile, copiedTile); + assertNotSame(tile.getProperties(), copiedTile.getProperties()); + copiedTile.getProperties().put("name", "copy"); + copy.getBlocks().remove(position); + + assertEquals("source", tile.getProperties().get("name")); + assertSame(block, source.getBlocks().get(position)); + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/object/NativeStructureObjectVetoTest.java b/core/src/test/java/art/arcane/iris/engine/object/NativeStructureObjectVetoTest.java index f3f94280b..368f731f4 100644 --- a/core/src/test/java/art/arcane/iris/engine/object/NativeStructureObjectVetoTest.java +++ b/core/src/test/java/art/arcane/iris/engine/object/NativeStructureObjectVetoTest.java @@ -8,6 +8,7 @@ import art.arcane.iris.spi.IrisPlatform; import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.spi.PlatformBlockState; import art.arcane.iris.spi.PlatformRegistries; +import art.arcane.iris.util.common.math.Vector3i; import art.arcane.iris.util.project.stream.ProceduralStream; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.math.RNG; @@ -101,6 +102,35 @@ public class NativeStructureObjectVetoTest { assertTrue(placer.written().isEmpty()); } + @Test + public void obliqueRotationRejectsNativePiecesBeyondTheUnrotatedRadius() { + IrisObject object = new IrisObject(21, 1, 21); + object.setUnsigned(20, 0, 20, log); + IrisObjectPlacement placement = placement(); + placement.setBottom(false); + placement.setRotation(IrisObjectRotation.of(0, 45, 0)); + RecordingPlacer placer = new RecordingPlacer(engine); + volumes(volume(14, SURFACE_Y, 0, 14, SURFACE_Y, 0)); + + assertEquals(-1, object.place(0, SURFACE_Y, 0, placer, placement, new RNG(1234L), data)); + assertTrue(placer.written().isEmpty()); + } + + @Test + public void offCenterOriginRejectsNativePiecesAtTheFarEdge() { + IrisObject object = new IrisObject(9, 1, 1); + object.setCenter(new Vector3i(0, 0, 0)); + object.setUnsigned(8, 0, 0, log); + IrisObjectPlacement placement = placement(); + placement.setBottom(false); + placement.setRotation(IrisObjectRotation.of(0, 0, 0)); + RecordingPlacer placer = new RecordingPlacer(engine); + volumes(volume(8, SURFACE_Y, 0, 8, SURFACE_Y, 0)); + + assertEquals(-1, object.place(0, SURFACE_Y, 0, placer, placement, new RNG(1234L), data)); + assertTrue(placer.written().isEmpty()); + } + @Test public void pieceOverlappingOnlyTheEnvelopeStillPlaces() { RecordingPlacer placer = new RecordingPlacer(engine);