Merged object stability fixes

- Keep scaling geometry and volume in one locked snapshot
- Skip unsupported block orientations and their tile data
- Replace smart bore executor scaffolding with ordered loops
This commit is contained in:
Brian Neumann-Fopiano
2026-09-06 03:44:26 -04:00
6 changed files with 344 additions and 117 deletions
@@ -533,6 +533,9 @@ final class IrisObjectPlacementRunner {
}
data = config.getRotation().rotate(data, spinx, spiny, spinz);
if (data == null) {
continue;
}
xx = x + (int) Math.round(i.getX());
int yy = y + (int) Math.round(i.getY());
@@ -23,13 +23,9 @@ import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.VectorMap;
import art.arcane.iris.util.common.math.IrisBlockVector;
import art.arcane.iris.util.common.math.Vector3i;
import art.arcane.iris.util.common.parallel.BurstExecutor;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Volume shaping for {@link IrisObject}: smart boring, shrinkwrapping, compaction and the shared block
* classification helpers used by placement.
@@ -59,30 +55,23 @@ final class IrisObjectShaping {
private static void ensureSmartBoredLocked(IrisObject self) {
PrecisionStopwatch p = PrecisionStopwatch.start();
PlatformBlockState vair = IrisObject.States.VAIR;
AtomicInteger applied = new AtomicInteger();
IrisBlockVector max = new IrisBlockVector(Double.MIN_VALUE, Double.MIN_VALUE, Double.MIN_VALUE);
IrisBlockVector min = new IrisBlockVector(Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE);
VectorMap<PlatformBlockState> source;
self.readLock.lock();
try {
if (self.blocks.isEmpty()) {
IrisLogging.warn("Cannot Smart Bore " + self.getLoadKey() + " because it has 0 blocks in it.");
self.smartBored = true;
return;
}
int applied = 0;
IrisBlockVector max = new IrisBlockVector(Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE);
IrisBlockVector min = new IrisBlockVector(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE);
VectorMap<PlatformBlockState> source = self.blocks;
if (source.isEmpty()) {
IrisLogging.warn("Cannot Smart Bore " + self.getLoadKey() + " because it has 0 blocks in it.");
self.smartBored = true;
return;
}
source = self.blocks;
for (IrisBlockVector i : self.blocks.keys()) {
max.setX(Math.max(i.getX(), max.getX()));
min.setX(Math.min(i.getX(), min.getX()));
max.setY(Math.max(i.getY(), max.getY()));
min.setY(Math.min(i.getY(), min.getY()));
max.setZ(Math.max(i.getZ(), max.getZ()));
min.setZ(Math.min(i.getZ(), min.getZ()));
}
} finally {
self.readLock.unlock();
for (IrisBlockVector i : source.keys()) {
max.setX(Math.max(i.getX(), max.getX()));
min.setX(Math.min(i.getX(), min.getX()));
max.setY(Math.max(i.getY(), max.getY()));
min.setY(Math.min(i.getY(), min.getY()));
max.setZ(Math.max(i.getZ(), max.getZ()));
min.setZ(Math.min(i.getZ(), min.getZ()));
}
VectorMap<PlatformBlockState> bore = new VectorMap<>();
@@ -90,114 +79,94 @@ final class IrisObjectShaping {
// bored volume must come from a fixed X->Y->Z order on the calling (lock-owning)
// thread, not from pool scheduling. This matches what burst-worker callers (studio,
// generation threads) already produced, so output is unchanged where it was stable.
BurstExecutor burst = MultiBurst.burst.burst();
burst.setMulticore(false);
// Smash X
for (int rayY = min.getBlockY(); rayY <= max.getBlockY(); rayY++) {
int finalRayY = rayY;
burst.queue(() -> {
for (int rayZ = min.getBlockZ(); rayZ <= max.getBlockZ(); rayZ++) {
int start = Integer.MAX_VALUE;
int end = Integer.MIN_VALUE;
for (int rayZ = min.getBlockZ(); rayZ <= max.getBlockZ(); rayZ++) {
int start = Integer.MAX_VALUE;
int end = Integer.MIN_VALUE;
for (int ray = min.getBlockX(); ray <= max.getBlockX(); ray++) {
if (boreContains(source, bore, new IrisBlockVector(ray, finalRayY, rayZ))) {
start = Math.min(ray, start);
end = Math.max(ray, end);
}
}
if (start != Integer.MAX_VALUE && end != Integer.MIN_VALUE) {
for (int i = start; i <= end; i++) {
boreCell(source, bore, new IrisBlockVector(i, finalRayY, rayZ), vair, applied);
}
for (int ray = min.getBlockX(); ray <= max.getBlockX(); ray++) {
if (boreContains(source, bore, new IrisBlockVector(ray, rayY, rayZ))) {
start = Math.min(ray, start);
end = Math.max(ray, end);
}
}
});
if (start != Integer.MAX_VALUE && end != Integer.MIN_VALUE) {
for (int i = start; i <= end; i++) {
applied += boreCell(source, bore, new IrisBlockVector(i, rayY, rayZ), vair);
}
}
}
}
// Smash Y
for (int rayX = min.getBlockX(); rayX <= max.getBlockX(); rayX++) {
int finalRayX = rayX;
burst.queue(() -> {
for (int rayZ = min.getBlockZ(); rayZ <= max.getBlockZ(); rayZ++) {
int start = Integer.MAX_VALUE;
int end = Integer.MIN_VALUE;
for (int rayZ = min.getBlockZ(); rayZ <= max.getBlockZ(); rayZ++) {
int start = Integer.MAX_VALUE;
int end = Integer.MIN_VALUE;
for (int ray = min.getBlockY(); ray <= max.getBlockY(); ray++) {
if (boreContains(source, bore, new IrisBlockVector(finalRayX, ray, rayZ))) {
start = Math.min(ray, start);
end = Math.max(ray, end);
}
}
if (start != Integer.MAX_VALUE && end != Integer.MIN_VALUE) {
for (int i = start; i <= end; i++) {
boreCell(source, bore, new IrisBlockVector(finalRayX, i, rayZ), vair, applied);
}
for (int ray = min.getBlockY(); ray <= max.getBlockY(); ray++) {
if (boreContains(source, bore, new IrisBlockVector(rayX, ray, rayZ))) {
start = Math.min(ray, start);
end = Math.max(ray, end);
}
}
});
if (start != Integer.MAX_VALUE && end != Integer.MIN_VALUE) {
for (int i = start; i <= end; i++) {
applied += boreCell(source, bore, new IrisBlockVector(rayX, i, rayZ), vair);
}
}
}
}
// Smash Z
for (int rayX = min.getBlockX(); rayX <= max.getBlockX(); rayX++) {
int finalRayX = rayX;
burst.queue(() -> {
for (int rayY = min.getBlockY(); rayY <= max.getBlockY(); rayY++) {
int start = Integer.MAX_VALUE;
int end = Integer.MIN_VALUE;
for (int rayY = min.getBlockY(); rayY <= max.getBlockY(); rayY++) {
int start = Integer.MAX_VALUE;
int end = Integer.MIN_VALUE;
for (int ray = min.getBlockZ(); ray <= max.getBlockZ(); ray++) {
if (boreContains(source, bore, new IrisBlockVector(finalRayX, rayY, ray))) {
start = Math.min(ray, start);
end = Math.max(ray, end);
}
}
if (start != Integer.MAX_VALUE && end != Integer.MIN_VALUE) {
for (int i = start; i <= end; i++) {
boreCell(source, bore, new IrisBlockVector(finalRayX, rayY, i), vair, applied);
}
for (int ray = min.getBlockZ(); ray <= max.getBlockZ(); ray++) {
if (boreContains(source, bore, new IrisBlockVector(rayX, rayY, ray))) {
start = Math.min(ray, start);
end = Math.max(ray, end);
}
}
});
}
burst.complete();
self.writeLock.lock();
try {
if (!self.smartBored) {
bore.forEach((v, s) -> self.blocks.computeIfAbsent(v, (vv) -> s));
self.smartBored = true;
if (start != Integer.MAX_VALUE && end != Integer.MIN_VALUE) {
for (int i = start; i <= end; i++) {
applied += boreCell(source, bore, new IrisBlockVector(rayX, rayY, i), vair);
}
}
}
} finally {
self.writeLock.unlock();
}
IrisLogging.debug("Smart Bore: " + self.getLoadKey() + " in " + Form.duration(p.getMilliseconds(), 2) + " (" + Form.f(applied.get()) + ")");
source.putAll(bore);
self.smartBored = true;
IrisLogging.debug("Smart Bore: " + self.getLoadKey() + " in " + Form.duration(p.getMilliseconds(), 2) + " (" + Form.f(applied) + ")");
}
private static boolean boreContains(VectorMap<PlatformBlockState> source, VectorMap<PlatformBlockState> bore, IrisBlockVector v) {
return source.containsKey(v) || bore.containsKey(v);
}
private static void boreCell(VectorMap<PlatformBlockState> source, VectorMap<PlatformBlockState> bore, IrisBlockVector v, PlatformBlockState vair, AtomicInteger applied) {
private static int boreCell(VectorMap<PlatformBlockState> source, VectorMap<PlatformBlockState> bore, IrisBlockVector v, PlatformBlockState vair) {
PlatformBlockState existing = source.get(v);
if (existing == null) {
if (vair.equals(bore.get(v))) {
return;
return 0;
}
bore.computeIfAbsent(v, (vv) -> vair);
bore.put(v, vair);
} else if (vair.equals(existing)) {
return;
return 0;
}
applied.getAndIncrement();
return 1;
}
static void shrinkwrap(IrisObject self) {
@@ -30,10 +30,12 @@ import art.arcane.iris.util.project.interpolation.Interpolation3D;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
/**
* Geometric transforms for {@link IrisObject}: rotation, scaling and the interpolated upscalers.
@@ -52,15 +54,23 @@ final class IrisObjectTransforms {
self.writeLock.lock();
try {
VectorMap<PlatformBlockState> d = new VectorMap<>();
Set<IrisBlockVector> omitted = new HashSet<>();
for (var entry : self.blocks) {
d.put(r.rotate(entry.getKey(), spinx, spiny, spinz), r.rotate(entry.getValue(), spinx, spiny, spinz));
for (Map.Entry<IrisBlockVector, PlatformBlockState> entry : self.blocks) {
PlatformBlockState rotated = r.rotate(entry.getValue(), spinx, spiny, spinz);
if (rotated == null) {
omitted.add(entry.getKey());
continue;
}
d.put(r.rotate(entry.getKey(), spinx, spiny, spinz), rotated);
}
VectorMap<TileData> dx = new VectorMap<>();
for (var entry : self.states) {
dx.put(r.rotate(entry.getKey(), spinx, spiny, spinz), entry.getValue());
for (Map.Entry<IrisBlockVector, TileData> entry : self.states) {
if (!omitted.contains(entry.getKey())) {
dx.put(r.rotate(entry.getKey(), spinx, spiny, spinz), entry.getValue());
}
}
self.blocks = d;
@@ -100,26 +110,27 @@ final class IrisObjectTransforms {
VectorMap<TileData> placeTile = new VectorMap<>();
VectorMap<IrisBlockVector> placeMax = savedOrigin && scale > 1 ? new VectorMap<>() : null;
IrisVector center = new IrisVector(self.getCenter().getX(), self.getCenter().getY(), self.getCenter().getZ());
if (self.getH() == 2) {
center = center.setY(center.getBlockY() + 0.5);
}
if (self.getW() == 2) {
center = center.setX(center.getBlockX() + 0.5);
}
if (self.getD() == 2) {
center = center.setZ(center.getBlockZ() + 0.5);
}
IrisObject oo = savedOrigin ? createOriginScaledObject(self, scale)
: new IrisObject((int) Math.ceil((self.w * scale) + (scale * 2)), (int) Math.ceil((self.h * scale) + (scale * 2)), (int) Math.ceil((self.d * scale) + (scale * 2)));
oo.setLoadKey(self.getLoadKey());
oo.setLoader(self.getLoader());
oo.setLoadFile(self.getLoadFile());
IrisVector center;
IrisObject oo;
boolean hasTiles;
self.readLock.lock();
try {
center = new IrisVector(self.getCenter().getX(), self.getCenter().getY(), self.getCenter().getZ());
if (self.getH() == 2) {
center = center.setY(center.getBlockY() + 0.5);
}
if (self.getW() == 2) {
center = center.setX(center.getBlockX() + 0.5);
}
if (self.getD() == 2) {
center = center.setZ(center.getBlockZ() + 0.5);
}
oo = savedOrigin ? createOriginScaledObject(self, scale)
: new IrisObject((int) Math.ceil((self.w * scale) + (scale * 2)), (int) Math.ceil((self.h * scale) + (scale * 2)), (int) Math.ceil((self.d * scale) + (scale * 2)));
oo.setLoadKey(self.getLoadKey());
oo.setLoader(self.getLoader());
oo.setLoadFile(self.getLoadFile());
hasTiles = !self.states.isEmpty();
for (Map.Entry<IrisBlockVector, PlatformBlockState> entry : self.blocks) {
PlatformBlockState bd = entry.getValue();
@@ -10,6 +10,9 @@ import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.math.RNG;
import org.bukkit.Material;
import org.bukkit.block.BlockFace;
import org.bukkit.block.data.Directional;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -19,6 +22,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -69,6 +73,36 @@ public class IrisObjectPlacementRunnerRegressionTest {
IrisPlatforms.unbind();
}
@Test
public void unsupportedRotatedOrientationOmitsBlockTileAndListener() {
PlatformBlockState torch = state("minecraft:wall_torch", false);
Directional directional = mock(Directional.class);
Material material = mock(Material.class);
when(torch.nativeHandle()).thenReturn(directional);
when(directional.clone()).thenReturn(directional);
when(directional.getFacing()).thenReturn(BlockFace.NORTH);
when(directional.getFaces()).thenReturn(Set.of(BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST, BlockFace.WEST));
when(directional.getMaterial()).thenReturn(material);
IrisObjectPlacement placement = placement();
placement.setRotation(IrisObjectRotation.of(90, 0, 0));
assertNull(placement.getRotation().rotate(torch, 0, 0, 0));
IrisObject object = lineObject(3);
object.setUnsigned(1, 0, 0, torch);
object.setUnsignedTile(1, 0, 0, new TileData("minecraft:wall_torch", new KMap<>()));
RecordingPlacer placer = new RecordingPlacer(null);
List<PlatformBlockState> placed = new ArrayList<>();
int result = object.place(0, ANCHOR_Y, 0, placer, placement, new RNG(2L),
(position, state) -> placed.add(state), null, data);
assertEquals(ANCHOR_Y, result);
assertEquals(2, placer.writes().size());
assertEquals(List.of(solid, solid), placed);
assertNull(placer.get(0, ANCHOR_Y, 0));
assertNull(placer.getData(0, ANCHOR_Y, 0, TileData.class));
assertEquals(1, object.getStates().size());
}
@Test
public void placementFailurePropagatesToTheCaller() {
RecordingPlacer placer = new RecordingPlacer(null);
@@ -0,0 +1,127 @@
package art.arcane.iris.engine.object;
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.IrisBlockVector;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisObjectShapingTest {
@Before
public void bindPlatform() {
IrisPlatforms.unbind();
PlatformRegistries registries = mock(PlatformRegistries.class);
Map<String, PlatformBlockState> states = new HashMap<>();
when(registries.block(anyString())).thenAnswer(invocation -> states.computeIfAbsent(
invocation.getArgument(0), key -> mock(PlatformBlockState.class)));
IrisPlatform platform = mock(IrisPlatform.class);
when(platform.registries()).thenReturn(registries);
IrisPlatforms.bind(platform);
}
@After
public void unbindPlatform() {
IrisPlatforms.unbind();
}
@Test
public void smartBoreMatchesOrderedAxisClosureAndPreservesOriginalBlocks() {
Random random = new Random(617431L);
PlatformBlockState stone = mock(PlatformBlockState.class);
for (int fixture = 0; fixture < 24; fixture++) {
int offset = switch (fixture % 3) {
case 0 -> -1050;
case 1 -> 1020;
default -> -3;
};
IrisObject object = new IrisObject(6, 6, 6);
Map<Cell, PlatformBlockState> expected = new HashMap<>();
for (int index = 0; index < 36; index++) {
Cell cell = new Cell(offset + random.nextInt(6), offset + random.nextInt(6), offset + random.nextInt(6));
PlatformBlockState state = switch (index % 3) {
case 0 -> IrisObject.States.AIR;
case 1 -> IrisObject.States.VAIR;
default -> stone;
};
object.getBlocks().put(cell.vector(), state);
expected.put(cell, state);
}
for (int axis = 0; axis < 3; axis++) {
closeAxis(expected, axis);
}
IrisObjectShaping.ensureSmartBored(object);
assertTrue(object.isSmartBored());
assertEquals("fixture " + fixture, expected.size(), object.getBlocks().size());
for (Map.Entry<Cell, PlatformBlockState> entry : expected.entrySet()) {
assertSame(entry.getValue(), object.getBlocks().get(entry.getKey().vector()));
}
long revision = object.getBlocks().modificationRevision();
IrisObjectShaping.ensureSmartBored(object);
assertEquals(revision, object.getBlocks().modificationRevision());
}
}
@Test
public void emptySmartBoreCompletesWithoutAddingBlocks() {
IrisObject object = new IrisObject(1, 1, 1);
IrisObjectShaping.ensureSmartBored(object);
assertTrue(object.isSmartBored());
assertTrue(object.getBlocks().isEmpty());
}
private static void closeAxis(Map<Cell, PlatformBlockState> blocks, int axis) {
List<Cell> occupied = new ArrayList<>(blocks.keySet());
for (Cell start : occupied) {
for (Cell end : occupied) {
if (!start.with(axis, 0).equals(end.with(axis, 0))) {
continue;
}
for (int position = start.coordinate(axis); position <= end.coordinate(axis); position++) {
blocks.putIfAbsent(start.with(axis, position), IrisObject.States.VAIR);
}
}
}
}
private record Cell(int x, int y, int z) {
private int coordinate(int axis) {
return switch (axis) {
case 0 -> x;
case 1 -> y;
default -> z;
};
}
private Cell with(int axis, int coordinate) {
return switch (axis) {
case 0 -> new Cell(coordinate, y, z);
case 1 -> new Cell(x, coordinate, z);
default -> new Cell(x, y, coordinate);
};
}
private IrisBlockVector vector() {
return new IrisBlockVector(x, y, z);
}
}
}
@@ -16,6 +16,9 @@ import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -46,6 +49,40 @@ public class IrisObjectTransformsTileTest {
IrisPlatforms.unbind();
}
@Test
public void rotationOmitsUnsupportedBlocksAndTheirTiles() {
IrisObject source = new IrisObject(3, 1, 1);
PlatformBlockState unsupported = state("minecraft:wall_torch");
source.setUnsigned(0, 0, 0, unsupported);
source.setUnsignedTile(0, 0, 0, tile("minecraft:wall_torch", "omitted"));
source.setUnsigned(1, 0, 0, state("minecraft:chest"));
source.setUnsignedTile(1, 0, 0, tile("minecraft:chest", "kept"));
source.setUnsigned(2, 0, 0, state("minecraft:stone"));
IrisObjectRotation.StateRotator previous = IrisObjectRotation.bindPlatformRotator(
(rotation, block, x, y, z) -> block == unsupported ? null : block);
IrisObject rotated;
try {
rotated = source.rotateCopy(IrisObjectRotation.of(0, 90, 0));
} finally {
IrisObjectRotation.restorePlatformRotator(previous);
}
assertEquals(2, rotated.getBlocks().size());
assertEquals(1, rotated.getStates().size());
for (Map.Entry<IrisBlockVector, TileData> entry : rotated.getStates()) {
assertEquals("minecraft:chest", rotated.getBlocks().get(entry.getKey()).materialKey());
assertEquals("kept", entry.getValue().getProperties().get("name"));
}
assertEquals(3, source.getBlocks().size());
assertEquals(2, source.getStates().size());
}
@Test
public void scalingWaitsForMatchingGeometryAndVolume() throws Exception {
assertConsistentScaling(false);
assertConsistentScaling(true);
}
@Test
public void enlargedTilesFollowEveryExpandedBlockAndCloneProperties() {
IrisObject source = new IrisObject(1, 1, 1);
@@ -186,6 +223,52 @@ public class IrisObjectTransformsTileTest {
assertEquals(8, scaled.getStates().size());
}
private static void assertConsistentScaling(boolean savedOrigin) throws Exception {
IrisObject source = new IrisObject(9, 5, 3);
source.setUnsigned(0, 0, 0, state("minecraft:stone"));
FutureTask<IrisObject> scaling = new FutureTask<>(() -> savedOrigin
? source.scaledAroundOrigin(2, IrisObjectPlacementScaleInterpolator.NONE)
: source.scaled(2, IrisObjectPlacementScaleInterpolator.NONE));
Thread worker = new Thread(scaling, "object-scaling-snapshot");
source.writeLock.lock();
try {
worker.start();
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
while (worker.getState() != Thread.State.WAITING && System.nanoTime() < deadline) {
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1));
}
assertEquals(Thread.State.WAITING, worker.getState());
source.setW(2);
source.setH(2);
source.setD(2);
source.setCenter(new Vector3i(1, 1, 1));
source.getBlocks().clear();
source.setUnsigned(0, 0, 0, state("minecraft:chest"));
source.setUnsignedTile(0, 0, 0, tile("minecraft:chest", "new volume"));
} finally {
source.writeLock.unlock();
}
try {
IrisObject actual = scaling.get(5, TimeUnit.SECONDS);
IrisObject expected = savedOrigin
? source.scaledAroundOrigin(2, IrisObjectPlacementScaleInterpolator.NONE)
: source.scaled(2, IrisObjectPlacementScaleInterpolator.NONE);
assertEquals(expected.getCenter(), actual.getCenter());
assertEquals(expected.getW(), actual.getW());
assertEquals(expected.getH(), actual.getH());
assertEquals(expected.getD(), actual.getD());
assertEquals(expected.getBlocks().size(), actual.getBlocks().size());
assertEquals(expected.getStates().size(), actual.getStates().size());
for (Map.Entry<IrisBlockVector, PlatformBlockState> entry : expected.getBlocks()) {
assertEquals(entry.getValue(), actual.getBlocks().get(entry.getKey()));
assertEquals(expected.getStates().get(entry.getKey()), actual.getStates().get(entry.getKey()));
}
} finally {
worker.interrupt();
worker.join(TimeUnit.SECONDS.toMillis(5));
}
}
private static IrisObject asymmetricObject() {
IrisObject source = new IrisObject(7, 5, 3);
source.setCenter(new Vector3i(5, 1, 2));