mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-28 06:50:02 +00:00
Grounded procedural trees
- Compact connected roots extend bounded gaps into supporting terrain - Unsupported cliff placements leave no partial trees - Root and canopy roles follow object scaling and rotation - Canopies preserve solid trunk cells across chunk replay and saved continuations - Branch wood replaces leaf clusters within each tree - Regression tests cover slopes and overlapping tree placements - Gameplay checks compare grounded spindle geometry across both packs
This commit is contained in:
1 parent
51c626c410
commit
a6aeceff86
34 files changed
+2082
-216
No files matched your search
+62
@@ -0,0 +1,62 @@
|
||||
package art.arcane.iris.generation.decoration.tree;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class TreeCanopyBuilderTest {
|
||||
@Test
|
||||
public void laterBranchesGrowThroughEarlierLeafClusters() {
|
||||
IrisProceduralTree tree = branchedTree();
|
||||
TreeBlockCanvas canvas = new TreeBlockCanvas();
|
||||
double[][] offsets = TreeTrunkBuilder.build(canvas, tree, 6).getFirst().offsets();
|
||||
|
||||
TreeCanopyBuilder.build(canvas, tree, 6, offsets, 0, 42L, null);
|
||||
|
||||
for (int y = 0; y < 6; y++) {
|
||||
for (int z = 0; z <= 4; z++) {
|
||||
assertTrue(canvas.getTrunk().contains(new TreeBlockCanvas.Vec(0, y, z)));
|
||||
assertFalse(canvas.getLeaf().contains(new TreeBlockCanvas.Vec(0, y, z)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void branchWoodReplacesBothLeafRolesButKeepsWoodAndAccents() {
|
||||
IrisProceduralTree tree = branchedTree();
|
||||
TreeBlockCanvas canvas = new TreeBlockCanvas();
|
||||
double[][] offsets = TreeTrunkBuilder.build(canvas, tree, 6).getFirst().offsets();
|
||||
canvas.setLeaf(0, 1, 1, TreeBlockCanvas.Role.LEAF);
|
||||
canvas.setLeaf(0, 1, 2, TreeBlockCanvas.Role.SECONDARY_LEAF);
|
||||
canvas.setTrunk(0, 1, 3, TreeBlockCanvas.Role.SECONDARY_TRUNK, TreeBlockCanvas.Axis.X);
|
||||
canvas.setDecor(0, 1, 4, 0, "north");
|
||||
|
||||
TreeCanopyBuilder.build(canvas, tree, 6, offsets, 0, 42L, null);
|
||||
|
||||
assertEquals(TreeBlockCanvas.Role.TRUNK, canvas.get(0, 1, 1).role());
|
||||
assertEquals(TreeBlockCanvas.Role.TRUNK, canvas.get(0, 1, 2).role());
|
||||
assertEquals(TreeBlockCanvas.Role.SECONDARY_TRUNK, canvas.get(0, 1, 3).role());
|
||||
assertEquals(TreeBlockCanvas.Axis.X, canvas.get(0, 1, 3).axis());
|
||||
assertEquals(TreeBlockCanvas.Role.DECORATOR, canvas.get(0, 1, 4).role());
|
||||
assertFalse(canvas.getLeaf().contains(new TreeBlockCanvas.Vec(0, 1, 1)));
|
||||
assertFalse(canvas.getLeaf().contains(new TreeBlockCanvas.Vec(0, 1, 2)));
|
||||
}
|
||||
|
||||
private static IrisProceduralTree branchedTree() {
|
||||
IrisProceduralTree tree = new IrisProceduralTree();
|
||||
IrisTreeBranches branches = new IrisTreeBranches();
|
||||
branches.setProbabilityFunction(IrisTreeBranchProbability.CONSTANT);
|
||||
branches.setProbabilityConstant(1);
|
||||
branches.setLengthFunction(IrisTreeFunction.CONSTANT);
|
||||
branches.setLengthConstant(4);
|
||||
branches.setAzimuthMode(IrisTreeAzimuthMode.CONSTANT);
|
||||
branches.setAzimuth(0);
|
||||
branches.setElevation(0);
|
||||
branches.setClusterRadius(2);
|
||||
branches.setClusterMode(IrisTreeLeafMode.FILLED);
|
||||
tree.getCanopy().setBranches(branches);
|
||||
return tree;
|
||||
}
|
||||
}
|
||||
+218
@@ -0,0 +1,218 @@
|
||||
package art.arcane.iris.generation.decoration.tree;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class TreeRootBuilderTest {
|
||||
@Test
|
||||
public void everyRootHasAFaceConnectedPathToExistingWood() {
|
||||
for (IrisTreeRootStyle style : IrisTreeRootStyle.values()) {
|
||||
for (int width : new int[]{1, 2, 5}) {
|
||||
for (long seed : new long[]{1L, 42L, 5923L}) {
|
||||
IrisProceduralTree tree = new IrisProceduralTree();
|
||||
tree.setRootStyle(style);
|
||||
tree.setTrunkWidth(width);
|
||||
tree.setRootDepth(8);
|
||||
tree.setRootFlare(7);
|
||||
tree.setLeanAngle(40);
|
||||
tree.setLeanAzimuth(45);
|
||||
TreeBlockCanvas canvas = trunk(tree, 24);
|
||||
Set<TreeBlockCanvas.Vec> original = new HashSet<>(canvas.getTrunk());
|
||||
|
||||
TreeRootBuilder.build(canvas, tree, 24, seed);
|
||||
|
||||
assertAllRootsConnected(canvas, original);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void automaticFlareRemainsCompactForTallThinTrees() {
|
||||
IrisProceduralTree tree = new IrisProceduralTree();
|
||||
for (int height : new int[]{8, 30, 80}) {
|
||||
TreeBlockCanvas canvas = trunk(tree, height);
|
||||
|
||||
TreeRootBuilder.build(canvas, tree, height, 42L);
|
||||
|
||||
for (TreeBlockCanvas.Vec cell : canvas.getTrunk()) {
|
||||
if (cell.y() < 0) {
|
||||
assertTrue(Math.abs(cell.x()) <= 2);
|
||||
assertTrue(Math.abs(cell.z()) <= 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void taprootNarrowsToOneCellWithoutExpandingTheBase() {
|
||||
IrisProceduralTree tree = new IrisProceduralTree();
|
||||
tree.setTrunkWidth(5);
|
||||
tree.setRootDepth(7);
|
||||
tree.setRootStyle(IrisTreeRootStyle.TAPROOT);
|
||||
TreeBlockCanvas canvas = trunk(tree, 20);
|
||||
Set<TreeBlockCanvas.Vec> original = new HashSet<>(canvas.getTrunk());
|
||||
|
||||
TreeRootBuilder.build(canvas, tree, 20, 42L);
|
||||
|
||||
int previousSize = 25;
|
||||
for (int depth = 1; depth <= 7; depth++) {
|
||||
int size = 0;
|
||||
for (TreeBlockCanvas.Vec cell : canvas.getTrunk()) {
|
||||
if (cell.y() == -depth) {
|
||||
size++;
|
||||
assertTrue(original.contains(new TreeBlockCanvas.Vec(cell.x(), 0, cell.z())));
|
||||
}
|
||||
}
|
||||
assertTrue(size > 0);
|
||||
assertTrue(size <= previousSize);
|
||||
previousSize = size;
|
||||
}
|
||||
assertEquals(1, previousSize);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buttressTipsEndAtDifferentDepths() {
|
||||
IrisProceduralTree tree = new IrisProceduralTree();
|
||||
tree.setRootDepth(8);
|
||||
tree.setRootFlare(7);
|
||||
TreeBlockCanvas canvas = trunk(tree, 20);
|
||||
|
||||
TreeRootBuilder.build(canvas, tree, 20, 42L);
|
||||
|
||||
Map<TreeBlockCanvas.Vec, Integer> outerDepths = new HashMap<>();
|
||||
for (TreeBlockCanvas.Vec cell : canvas.getTrunk()) {
|
||||
if (cell.y() < 0 && Math.hypot(cell.x(), cell.z()) >= 5) {
|
||||
TreeBlockCanvas.Vec column = new TreeBlockCanvas.Vec(cell.x(), 0, cell.z());
|
||||
outerDepths.merge(column, cell.y(), Math::min);
|
||||
}
|
||||
}
|
||||
assertTrue(new HashSet<>(outerDepths.values()).size() > 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void explicitFlareKeepsWideStiltRoots() {
|
||||
IrisProceduralTree tree = new IrisProceduralTree();
|
||||
tree.setRootStyle(IrisTreeRootStyle.STILT);
|
||||
tree.setRootFlare(8);
|
||||
tree.setRootDepth(5);
|
||||
TreeBlockCanvas canvas = trunk(tree, 24);
|
||||
Set<TreeBlockCanvas.Vec> original = new HashSet<>(canvas.getTrunk());
|
||||
|
||||
TreeRootBuilder.build(canvas, tree, 24, 42L);
|
||||
|
||||
double reach = 0;
|
||||
boolean aboveGround = false;
|
||||
for (TreeBlockCanvas.Vec cell : canvas.getTrunk()) {
|
||||
if (!original.contains(cell)) {
|
||||
reach = Math.max(reach, Math.hypot(cell.x(), cell.z()));
|
||||
aboveGround |= cell.y() > 0;
|
||||
}
|
||||
}
|
||||
assertTrue(reach >= 6);
|
||||
assertTrue(aboveGround);
|
||||
assertAllRootsConnected(canvas, original);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stiltRootsAttachToTheActualLeaningTrunk() {
|
||||
IrisProceduralTree tree = new IrisProceduralTree();
|
||||
tree.setRootStyle(IrisTreeRootStyle.STILT);
|
||||
tree.setLeanAngle(55);
|
||||
tree.setLeanAzimuth(90);
|
||||
tree.setRootFlare(3);
|
||||
TreeBlockCanvas canvas = trunk(tree, 24);
|
||||
Set<TreeBlockCanvas.Vec> original = new HashSet<>(canvas.getTrunk());
|
||||
|
||||
TreeRootBuilder.build(canvas, tree, 24, 42L);
|
||||
|
||||
boolean attachedAboveGround = false;
|
||||
for (TreeBlockCanvas.Vec cell : canvas.getTrunk()) {
|
||||
if (!original.contains(cell) && cell.y() >= 4) {
|
||||
for (TreeBlockCanvas.Vec neighbor : neighbors(cell)) {
|
||||
attachedAboveGround |= original.contains(neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
assertTrue(attachedAboveGround);
|
||||
assertAllRootsConnected(canvas, original);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void intersectingFoliageDoesNotSeverRoots() {
|
||||
IrisProceduralTree tree = new IrisProceduralTree();
|
||||
tree.setRootDepth(5);
|
||||
tree.setRootFlare(5);
|
||||
TreeBlockCanvas canvas = trunk(tree, 20);
|
||||
Set<TreeBlockCanvas.Vec> original = new HashSet<>(canvas.getTrunk());
|
||||
for (int x = -5; x <= 5; x++) {
|
||||
for (int z = -5; z <= 5; z++) {
|
||||
canvas.setLeaf(x, -1, z, TreeBlockCanvas.Role.LEAF);
|
||||
}
|
||||
}
|
||||
|
||||
TreeRootBuilder.build(canvas, tree, 20, 42L);
|
||||
|
||||
assertAllRootsConnected(canvas, original);
|
||||
assertEquals(TreeBlockCanvas.Role.TRUNK, canvas.get(0, -1, 0).role());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rootGeometryIsDeterministic() {
|
||||
for (IrisTreeRootStyle style : IrisTreeRootStyle.values()) {
|
||||
IrisProceduralTree tree = new IrisProceduralTree();
|
||||
tree.setRootStyle(style);
|
||||
tree.setTrunkWidth(2);
|
||||
TreeBlockCanvas first = trunk(tree, 30);
|
||||
TreeBlockCanvas second = trunk(tree, 30);
|
||||
|
||||
TreeRootBuilder.build(first, tree, 30, 5923L);
|
||||
TreeRootBuilder.build(second, tree, 30, 5923L);
|
||||
|
||||
assertEquals(first.getCells(), second.getCells());
|
||||
}
|
||||
}
|
||||
|
||||
private static TreeBlockCanvas trunk(IrisProceduralTree tree, int height) {
|
||||
TreeBlockCanvas canvas = new TreeBlockCanvas();
|
||||
TreeTrunkBuilder.build(canvas, tree, height);
|
||||
return canvas;
|
||||
}
|
||||
|
||||
private static void assertAllRootsConnected(TreeBlockCanvas canvas, Set<TreeBlockCanvas.Vec> original) {
|
||||
Set<TreeBlockCanvas.Vec> reached = new HashSet<>(original);
|
||||
ArrayDeque<TreeBlockCanvas.Vec> pending = new ArrayDeque<>(original);
|
||||
while (!pending.isEmpty()) {
|
||||
TreeBlockCanvas.Vec cell = pending.removeFirst();
|
||||
for (TreeBlockCanvas.Vec neighbor : neighbors(cell)) {
|
||||
if (canvas.getTrunk().contains(neighbor) && reached.add(neighbor)) {
|
||||
pending.addLast(neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
Set<TreeBlockCanvas.Vec> roots = new HashSet<>(canvas.getTrunk());
|
||||
roots.removeAll(original);
|
||||
assertFalse(roots.isEmpty());
|
||||
assertEquals(canvas.getTrunk(), reached);
|
||||
}
|
||||
|
||||
private static TreeBlockCanvas.Vec[] neighbors(TreeBlockCanvas.Vec cell) {
|
||||
return new TreeBlockCanvas.Vec[]{
|
||||
new TreeBlockCanvas.Vec(cell.x() - 1, cell.y(), cell.z()),
|
||||
new TreeBlockCanvas.Vec(cell.x() + 1, cell.y(), cell.z()),
|
||||
new TreeBlockCanvas.Vec(cell.x(), cell.y() - 1, cell.z()),
|
||||
new TreeBlockCanvas.Vec(cell.x(), cell.y() + 1, cell.z()),
|
||||
new TreeBlockCanvas.Vec(cell.x(), cell.y(), cell.z() - 1),
|
||||
new TreeBlockCanvas.Vec(cell.x(), cell.y(), cell.z() + 1)
|
||||
};
|
||||
}
|
||||
}
|
||||
+22
@@ -28,9 +28,11 @@ import art.arcane.iris.generation.block.TileData;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveAction;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.math.BlockPosition;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
@@ -42,6 +44,26 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class CaveObjectPlacementTransactionTest {
|
||||
@Test
|
||||
public void canopyCommitRechecksSolidsAndSkipsAssociatedMetadata() {
|
||||
IObjectPlacer delegate = createPlacer(128, 80, 20, 60);
|
||||
CaveObjectPlacementTransaction transaction = new CaveObjectPlacementTransaction(delegate, 20, 10);
|
||||
NativeBlockState canopy = mock(NativeBlockState.class);
|
||||
NativeBlockState trunk = mock(NativeBlockState.class);
|
||||
when(trunk.isSolid()).thenReturn(true);
|
||||
when(delegate.beginTreeCanopy(any())).thenCallRealMethod();
|
||||
assertTrue(transaction.beginTreeCanopy(new BlockPosition(4, 30, 7)));
|
||||
transaction.set(4, 30, 7, canopy);
|
||||
transaction.setData(4, 30, 7, "canopy");
|
||||
transaction.endTreeCanopy();
|
||||
when(delegate.get(4, 30, 7)).thenReturn(trunk);
|
||||
|
||||
assertEquals(CaveObjectPlacementTransaction.CommitResult.COMMITTED, transaction.commit());
|
||||
|
||||
verify(delegate, never()).set(4, 30, 7, canopy);
|
||||
verify(delegate, never()).setData(4, 30, 7, "canopy");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validPlacementCommitsBlocksTilesAndMetadataTogether() {
|
||||
IObjectPlacer delegate = createPlacer(128, 80, 20, 60);
|
||||
|
||||
+24
@@ -21,6 +21,7 @@ package art.arcane.iris.generation.mantle;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.block.TileData;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.math.BlockPosition;
|
||||
import org.junit.Test;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
@@ -31,6 +32,7 @@ import art.arcane.iris.structure.object.IObjectPlacer;
|
||||
import art.arcane.iris.world.storage.matter.IrisMatterSupport;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
@@ -42,6 +44,28 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class FloatingObjectPlacementTransactionTest {
|
||||
@Test
|
||||
public void canopyCommitRechecksSolidsAndSkipsAssociatedMetadata() {
|
||||
IslandObjectPlacer delegate = createPlacer(true);
|
||||
FloatingObjectPlacementTransaction transaction = new FloatingObjectPlacementTransaction(delegate,
|
||||
new ObjectContinuationBundle.PlacementKey(ObjectContinuationBundle.Kind.FLOATING, 0, 0, 0));
|
||||
NativeBlockState canopy = mock(NativeBlockState.class);
|
||||
NativeBlockState trunk = mock(NativeBlockState.class);
|
||||
when(trunk.isSolid()).thenReturn(true);
|
||||
when(delegate.beginTreeCanopy(any())).thenCallRealMethod();
|
||||
when(delegate.canWriteObjectBlock(4, 101, 7)).thenReturn(true);
|
||||
assertTrue(transaction.beginTreeCanopy(new BlockPosition(4, 101, 7)));
|
||||
transaction.set(4, 101, 7, canopy);
|
||||
transaction.setData(4, 101, 7, "canopy");
|
||||
transaction.endTreeCanopy();
|
||||
when(delegate.get(4, 101, 7)).thenReturn(trunk);
|
||||
|
||||
assertEquals(FloatingObjectPlacementTransaction.CommitResult.COMMITTED, transaction.commit());
|
||||
|
||||
verify(delegate, never()).set(4, 101, 7, canopy);
|
||||
verify(delegate, never()).setData(4, 101, 7, "canopy");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unsupportedFinalTerrainRejectsBlocksTilesAndMarkersTogether() {
|
||||
IslandObjectPlacer delegate = createPlacer(false);
|
||||
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
package art.arcane.iris.generation.mantle;
|
||||
|
||||
import art.arcane.iris.generation.block.TileData;
|
||||
import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.integration.Identifier;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import art.arcane.iris.structure.object.IObjectPlacer;
|
||||
import art.arcane.iris.testsupport.PlatformLeakGuard;
|
||||
import art.arcane.iris.world.storage.matter.IrisMatterSupport;
|
||||
import art.arcane.iris.world.storage.matter.TileWrapper;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.hunk.Hunk;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.math.BlockPosition;
|
||||
import art.arcane.volmlib.util.matter.IrisMatter;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.matter.MatterSlice;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ObjectContinuationCanopyTest {
|
||||
@ClassRule
|
||||
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
|
||||
|
||||
private IrisPlatform previous;
|
||||
private NativeBlockState trunk;
|
||||
private NativeBlockState canopy;
|
||||
private NativeBlockState custom;
|
||||
|
||||
@Before
|
||||
public void bindPalette() {
|
||||
IrisMatterSupport.ensureRegistered();
|
||||
previous = IrisPlatforms.isBound() ? IrisPlatforms.get() : null;
|
||||
IrisPlatform platform = mock(IrisPlatform.class);
|
||||
PlatformRegistries registries = mock(PlatformRegistries.class);
|
||||
trunk = mock(NativeBlockState.class);
|
||||
canopy = mock(NativeBlockState.class);
|
||||
custom = mock(NativeBlockState.class);
|
||||
when(trunk.key()).thenReturn("minecraft:oak_log");
|
||||
when(trunk.isSolid()).thenReturn(true);
|
||||
when(canopy.key()).thenReturn("minecraft:red_stained_glass");
|
||||
when(canopy.isSolid()).thenReturn(true);
|
||||
when(custom.isCustom()).thenReturn(true);
|
||||
when(custom.placementBaseState()).thenReturn(canopy);
|
||||
when(custom.deferredPlacementKey()).thenReturn("test:custom_canopy");
|
||||
when(platform.registries()).thenReturn(registries);
|
||||
when(registries.decodeBlockState(trunk.key())).thenReturn(trunk);
|
||||
when(registries.decodeBlockState(canopy.key())).thenReturn(canopy);
|
||||
IrisPlatforms.unbind();
|
||||
IrisPlatforms.bind(platform);
|
||||
}
|
||||
|
||||
@After
|
||||
public void restorePalette() {
|
||||
IrisPlatforms.unbind();
|
||||
if (previous != null) {
|
||||
IrisPlatforms.bind(previous);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void encodingKeepsTrunkProvenanceWhenLaterCustomCanopyOverlaps() throws Exception {
|
||||
Matter payload = decode(List.of(
|
||||
new ObjectDestinationTransaction.SetMutation(key(16, NativeBlockState.class), trunk, false),
|
||||
new ObjectDestinationTransaction.SetMutation(key(16, String.class), "original-trunk", false),
|
||||
new ObjectDestinationTransaction.CustomBlockMutation(key(16, NativeBlockState.class), custom, true),
|
||||
new ObjectDestinationTransaction.SetMutation(key(16, String.class), "rejected-canopy", true),
|
||||
new ObjectDestinationTransaction.CustomBlockMutation(key(17, NativeBlockState.class), custom, true)));
|
||||
|
||||
assertSame(trunk, payload.getSlice(NativeBlockState.class).get(0, 5, 1));
|
||||
assertEquals("original-trunk", payload.getSlice(String.class).get(0, 5, 1));
|
||||
assertNull(payload.getSlice(Identifier.class).get(0, 5, 1));
|
||||
assertNull(payload.getSlice(TreeCanopy.class).get(0, 5, 1));
|
||||
assertSame(canopy, payload.getSlice(NativeBlockState.class).get(1, 5, 1));
|
||||
assertEquals(Identifier.fromString("test:custom_canopy"), payload.getSlice(Identifier.class).get(1, 5, 1));
|
||||
assertSame(TreeCanopy.INSTANCE, payload.getSlice(TreeCanopy.class).get(1, 5, 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unconditionalBlockClearsPreviousCanopyCondition() throws Exception {
|
||||
Matter payload = decode(List.of(
|
||||
new ObjectDestinationTransaction.CustomBlockMutation(key(16, NativeBlockState.class), custom, true),
|
||||
new ObjectDestinationTransaction.SetMutation(key(16, NativeBlockState.class), trunk, false)));
|
||||
|
||||
assertSame(trunk, payload.getSlice(NativeBlockState.class).get(0, 5, 1));
|
||||
MatterSlice<TreeCanopy> conditions = payload.getSlice(TreeCanopy.class);
|
||||
assertTrue(conditions == null || conditions.get(0, 5, 1) == null);
|
||||
MatterSlice<Identifier> identifiers = payload.getSlice(Identifier.class);
|
||||
assertTrue(identifiers == null || identifiers.get(0, 5, 1) == null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void replayDefersCanopyAndPreservesLaterStructureMetadata() throws Exception {
|
||||
ReplayFixture fixture = new ReplayFixture();
|
||||
Matter payload = customPayload();
|
||||
ObjectContinuationPersistence.restore(fixture.mantle, 1, 0, payload, fixture.blocks, false);
|
||||
assertFalse(fixture.section.hasSlice(Identifier.class));
|
||||
assertNull(fixture.blocks.get(0, 5, 1));
|
||||
fixture.blocks.set(0, 5, 1, trunk);
|
||||
Identifier existingId = Identifier.fromString("test:existing_structure");
|
||||
TreeBlockMaterial provenance = TreeBlockMaterial.of("minecraft:oak_log");
|
||||
TileWrapper tile = new TileWrapper(mock(TileData.class));
|
||||
fixture.section.slice(NativeBlockState.class).set(0, 5, 1, trunk);
|
||||
fixture.section.slice(Identifier.class).set(0, 5, 1, existingId);
|
||||
fixture.section.slice(String.class).set(0, 5, 1, "original-trunk");
|
||||
fixture.section.slice(TreeBlockMaterial.class).set(0, 5, 1, provenance);
|
||||
fixture.section.slice(TileWrapper.class).set(0, 5, 1, tile);
|
||||
|
||||
ObjectContinuationPersistence.restore(fixture.mantle, 1, 0, payload, fixture.blocks, true);
|
||||
|
||||
assertSame(trunk, fixture.blocks.get(0, 5, 1));
|
||||
assertSame(trunk, fixture.section.getSlice(NativeBlockState.class).get(0, 5, 1));
|
||||
assertEquals(existingId, fixture.section.getSlice(Identifier.class).get(0, 5, 1));
|
||||
assertEquals("original-trunk", fixture.section.getSlice(String.class).get(0, 5, 1));
|
||||
assertEquals(provenance, fixture.section.getSlice(TreeBlockMaterial.class).get(0, 5, 1));
|
||||
assertSame(tile, fixture.section.getSlice(TileWrapper.class).get(0, 5, 1));
|
||||
assertFalse(fixture.section.hasSlice(TreeCanopy.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void replayPlacesCustomCanopyIntoAirAndClearsOldTileMetadata() throws Exception {
|
||||
ReplayFixture fixture = new ReplayFixture();
|
||||
fixture.section.slice(TileWrapper.class).set(0, 5, 1, new TileWrapper(mock(TileData.class)));
|
||||
fixture.section.slice(TreeBlockMaterial.class).set(0, 5, 1, TreeBlockMaterial.of("minecraft:oak_log"));
|
||||
|
||||
ObjectContinuationPersistence.restore(fixture.mantle, 1, 0, customPayload(), fixture.blocks, true);
|
||||
|
||||
assertSame(canopy, fixture.blocks.get(0, 5, 1));
|
||||
assertEquals(Identifier.fromString("test:custom_canopy"), fixture.section.getSlice(Identifier.class).get(0, 5, 1));
|
||||
assertEquals("incoming-canopy", fixture.section.getSlice(String.class).get(0, 5, 1));
|
||||
assertNull(fixture.section.getSlice(TileWrapper.class).get(0, 5, 1));
|
||||
assertNull(fixture.section.getSlice(TreeBlockMaterial.class).get(0, 5, 1));
|
||||
assertFalse(fixture.section.hasSlice(TreeCanopy.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void floatingContinuationRetainsCustomCanopyCondition() throws Exception {
|
||||
IObjectPlacer delegate = mock(IObjectPlacer.class);
|
||||
Engine engine = mock(Engine.class);
|
||||
EngineMantle engineMantle = mock(EngineMantle.class);
|
||||
Mantle<Matter> mantle = mock(Mantle.class);
|
||||
MantleChunk<Matter> firstChunk = mock(MantleChunk.class);
|
||||
MantleChunk<Matter> secondChunk = mock(MantleChunk.class);
|
||||
Matter first = new IrisMatter(16, 16, 16);
|
||||
Matter second = new IrisMatter(16, 16, 16);
|
||||
when(delegate.getEngine()).thenReturn(engine);
|
||||
when(delegate.beginTreeCanopy(any())).thenCallRealMethod();
|
||||
when(engine.getHeight()).thenReturn(64);
|
||||
when(engine.getMantle()).thenReturn(engineMantle);
|
||||
when(engineMantle.getMantle()).thenReturn(mantle);
|
||||
when(mantle.useChunk(0, 0)).thenReturn(firstChunk);
|
||||
when(mantle.useChunk(1, 0)).thenReturn(secondChunk);
|
||||
when(firstChunk.getOrCreate(0)).thenReturn(first);
|
||||
when(secondChunk.getOrCreate(0)).thenReturn(second);
|
||||
FloatingObjectPlacementTransaction transaction = new FloatingObjectPlacementTransaction(delegate,
|
||||
new ObjectContinuationBundle.PlacementKey(ObjectContinuationBundle.Kind.FLOATING, 0, 0, 0));
|
||||
transaction.set(15, 5, 1, trunk);
|
||||
assertTrue(transaction.beginTreeCanopy(new BlockPosition(16, 5, 1)));
|
||||
transaction.set(16, 5, 1, custom);
|
||||
transaction.setData(16, 5, 1, "floating-canopy");
|
||||
transaction.endTreeCanopy();
|
||||
|
||||
assertEquals(FloatingObjectPlacementTransaction.CommitResult.COMMITTED, transaction.commit());
|
||||
|
||||
Matter payload = second.<ObjectContinuationBundle>getSlice(ObjectContinuationBundle.class)
|
||||
.get(0, 0, 0).fragments().getFirst().decode();
|
||||
assertSame(TreeCanopy.INSTANCE, payload.getSlice(TreeCanopy.class).get(0, 5, 1));
|
||||
assertSame(canopy, payload.getSlice(NativeBlockState.class).get(0, 5, 1));
|
||||
assertEquals(Identifier.fromString("test:custom_canopy"), payload.getSlice(Identifier.class).get(0, 5, 1));
|
||||
assertEquals("floating-canopy", payload.getSlice(String.class).get(0, 5, 1));
|
||||
}
|
||||
|
||||
private Matter customPayload() throws IOException {
|
||||
return decode(List.of(
|
||||
new ObjectDestinationTransaction.CustomBlockMutation(key(16, NativeBlockState.class), custom, true),
|
||||
new ObjectDestinationTransaction.SetMutation(key(16, String.class), "incoming-canopy", true)));
|
||||
}
|
||||
|
||||
private static Matter decode(List<ObjectDestinationTransaction.Mutation> mutations) throws IOException {
|
||||
return Matter.read(new ByteArrayInputStream(ObjectSourcePlan.encode(mutations, 64)));
|
||||
}
|
||||
|
||||
private static ObjectDestinationTransaction.DataKey key(int x, Class<?> type) {
|
||||
return new ObjectDestinationTransaction.DataKey(x, 5, 1, type);
|
||||
}
|
||||
|
||||
private static final class ReplayFixture {
|
||||
private final Mantle<Matter> mantle;
|
||||
private final Matter section = new IrisMatter(16, 16, 16);
|
||||
private final Hunk<NativeBlockState> blocks = Hunk.newArrayHunk(16, 64, 16);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private ReplayFixture() {
|
||||
mantle = mock(Mantle.class);
|
||||
MantleChunk<Matter> chunk = mock(MantleChunk.class);
|
||||
when(mantle.useChunk(1, 0)).thenReturn(chunk);
|
||||
when(chunk.get(0)).thenReturn(section);
|
||||
when(chunk.getOrCreate(0)).thenReturn(section);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -166,11 +166,11 @@ public class ObjectContinuationTest {
|
||||
properties.put("CustomName", "continued chest");
|
||||
TileWrapper tile = new TileWrapper(new TileData("minecraft:chest", properties));
|
||||
List<ObjectDestinationTransaction.Mutation> mutations = List.of(
|
||||
new ObjectDestinationTransaction.CustomBlockMutation(key(NativeBlockState.class), custom),
|
||||
new ObjectDestinationTransaction.SetMutation(key(String.class), "tree@42"),
|
||||
new ObjectDestinationTransaction.SetMutation(key(TreeBlockMaterial.class), TreeBlockMaterial.of("minecraft:oak_log")),
|
||||
new ObjectDestinationTransaction.SetMutation(key(TileWrapper.class), tile),
|
||||
new ObjectDestinationTransaction.SetMutation(new ObjectDestinationTransaction.DataKey(17, 5, 1, NativeBlockState.class), air));
|
||||
new ObjectDestinationTransaction.CustomBlockMutation(key(NativeBlockState.class), custom, false),
|
||||
new ObjectDestinationTransaction.SetMutation(key(String.class), "tree@42", false),
|
||||
new ObjectDestinationTransaction.SetMutation(key(TreeBlockMaterial.class), TreeBlockMaterial.of("minecraft:oak_log"), false),
|
||||
new ObjectDestinationTransaction.SetMutation(key(TileWrapper.class), tile, false),
|
||||
new ObjectDestinationTransaction.SetMutation(new ObjectDestinationTransaction.DataKey(17, 5, 1, NativeBlockState.class), air, false));
|
||||
ObjectContinuationBundle.Fragment fragment = new ObjectContinuationBundle.Fragment(
|
||||
new ObjectContinuationBundle.PlacementKey(ObjectContinuationBundle.Kind.BIOME, 0, 0, 2), new ObjectContinuationBundle.Bounds(15, 1, 17, 1),
|
||||
List.of(new ObjectContinuationBundle.ChunkPosition(0, 0), new ObjectContinuationBundle.ChunkPosition(1, 0)),
|
||||
@@ -215,7 +215,7 @@ public class ObjectContinuationTest {
|
||||
ObjectContinuationBundle.Fragment fragment = new ObjectContinuationBundle.Fragment(
|
||||
new ObjectContinuationBundle.PlacementKey(ObjectContinuationBundle.Kind.BIOME, 0, 0, 0), new ObjectContinuationBundle.Bounds(15, 1, 16, 1),
|
||||
List.of(new ObjectContinuationBundle.ChunkPosition(0, 0), new ObjectContinuationBundle.ChunkPosition(1, 0)),
|
||||
ObjectSourcePlan.encode(List.of(new ObjectDestinationTransaction.SetMutation(key(IrisSpawner.class), spawner)), 64));
|
||||
ObjectSourcePlan.encode(List.of(new ObjectDestinationTransaction.SetMutation(key(IrisSpawner.class), spawner, false)), 64));
|
||||
|
||||
try (IrisMatterContext.Scope outer = IrisMatterContext.open(current)) {
|
||||
assertThrows(IOException.class, fragment::decode);
|
||||
@@ -278,7 +278,7 @@ public class ObjectContinuationTest {
|
||||
}
|
||||
|
||||
private static ObjectDestinationTransaction.Mutation marker(int x, String marker) {
|
||||
return new ObjectDestinationTransaction.SetMutation(new ObjectDestinationTransaction.DataKey(x, 5, 1, String.class), marker);
|
||||
return new ObjectDestinationTransaction.SetMutation(new ObjectDestinationTransaction.DataKey(x, 5, 1, String.class), marker, false);
|
||||
}
|
||||
|
||||
private static final class Fixture {
|
||||
|
||||
+85
@@ -14,6 +14,8 @@ import art.arcane.iris.generation.terrain.Terrain3DColumnFixtures;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.math.BlockPosition;
|
||||
import art.arcane.iris.structure.object.DecayControlPlacer;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import art.arcane.iris.testsupport.BukkitTestServer;
|
||||
import art.arcane.iris.world.storage.matter.IrisMatterSupport;
|
||||
@@ -51,6 +53,89 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ObjectDestinationTransactionTest {
|
||||
@Test
|
||||
public void separateSourceCanopiesPreservePriorSolidsAndTheirMetadata() {
|
||||
for (boolean custom : new boolean[]{false, true}) {
|
||||
MantleWriter writer = writer();
|
||||
NativeBlockState trunk = mock(NativeBlockState.class);
|
||||
NativeBlockState canopy = mock(NativeBlockState.class);
|
||||
NativeBlockState base = mock(NativeBlockState.class);
|
||||
when(trunk.isSolid()).thenReturn(true);
|
||||
if (custom) {
|
||||
when(canopy.isCustom()).thenReturn(true);
|
||||
when(canopy.deferredPlacementKey()).thenReturn("test:canopy");
|
||||
when(canopy.placementBaseState()).thenReturn(base);
|
||||
}
|
||||
TreeBlockMaterial trunkMaterial = new TreeBlockMaterial("test:trunk");
|
||||
TreeBlockMaterial canopyMaterial = new TreeBlockMaterial("test:canopy");
|
||||
ObjectDestinationTransaction source = new ObjectDestinationTransaction(writer, 0, 0);
|
||||
DecayControlPlacer placer = new DecayControlPlacer(source);
|
||||
for (int x : new int[]{17, 18}) {
|
||||
assertTrue(placer.beginTreeCanopy(new BlockPosition(x, 7, 1)));
|
||||
placer.set(x, 7, 1, canopy);
|
||||
placer.setData(x, 7, 1, "canopy");
|
||||
placer.setData(x, 7, 1, canopyMaterial);
|
||||
placer.endTreeCanopy();
|
||||
}
|
||||
ObjectSourcePlan plan = source.sourcePlanSince(0);
|
||||
for (ObjectDestinationTransaction.Mutation mutation : plan.mutationsFor(1, 0)) {
|
||||
assertTrue(mutation.canopy());
|
||||
}
|
||||
ObjectDestinationTransaction destination = new ObjectDestinationTransaction(writer, 1, 0);
|
||||
destination.set(17, 7, 1, trunk);
|
||||
destination.setData(17, 7, 1, "trunk");
|
||||
destination.setData(17, 7, 1, trunkMaterial);
|
||||
destination.apply(plan);
|
||||
assertSame(trunk, destination.get(17, 7, 1));
|
||||
assertEquals("trunk", destination.getDataIfPresent(17, 7, 1, String.class));
|
||||
assertEquals(trunkMaterial, destination.getDataIfPresent(17, 7, 1, TreeBlockMaterial.class));
|
||||
assertNull(destination.getDataIfPresent(17, 7, 1, Identifier.class));
|
||||
assertSame(custom ? base : canopy, destination.get(18, 7, 1));
|
||||
assertEquals("canopy", destination.getDataIfPresent(18, 7, 1, String.class));
|
||||
destination.commit();
|
||||
verify(writer, never()).set(17, 7, 1, canopy);
|
||||
verify(writer, never()).setData(17, 7, 1, canopy);
|
||||
verify(writer, never()).setData(17, 7, 1, "canopy");
|
||||
verify(writer, never()).setData(17, 7, 1, canopyMaterial);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void skippedCanopyDoesNotSuppressLaterWoodAtTheSameCell() {
|
||||
MantleWriter writer = writer();
|
||||
NativeBlockState existing = mock(NativeBlockState.class);
|
||||
NativeBlockState canopy = mock(NativeBlockState.class);
|
||||
NativeBlockState replacement = mock(NativeBlockState.class);
|
||||
when(existing.isSolid()).thenReturn(true);
|
||||
ObjectDestinationTransaction source = new ObjectDestinationTransaction(writer, 0, 0);
|
||||
assertTrue(source.beginTreeCanopy(new BlockPosition(17, 7, 1)));
|
||||
source.set(17, 7, 1, canopy);
|
||||
source.setData(17, 7, 1, "canopy");
|
||||
source.endTreeCanopy();
|
||||
source.set(17, 7, 1, replacement);
|
||||
source.setData(17, 7, 1, "replacement");
|
||||
ObjectDestinationTransaction destination = new ObjectDestinationTransaction(writer, 1, 0);
|
||||
destination.set(17, 7, 1, existing);
|
||||
destination.apply(source.sourcePlanSince(0));
|
||||
assertSame(replacement, destination.get(17, 7, 1));
|
||||
assertEquals("replacement", destination.getDataIfPresent(17, 7, 1, String.class));
|
||||
destination.commit();
|
||||
verify(writer, never()).setData(17, 7, 1, canopy);
|
||||
verify(writer, never()).setData(17, 7, 1, "canopy");
|
||||
verify(writer).setData(17, 7, 1, replacement);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canopyCaptureRejectsExistingSolidsWithoutMutations() {
|
||||
MantleWriter writer = writer();
|
||||
NativeBlockState solid = mock(NativeBlockState.class);
|
||||
when(solid.isSolid()).thenReturn(true);
|
||||
when(writer.getPrerequisiteBlock(4, 7, 1)).thenReturn(solid);
|
||||
ObjectDestinationTransaction source = new ObjectDestinationTransaction(writer, 0, 0);
|
||||
assertFalse(source.beginTreeCanopy(new BlockPosition(4, 7, 1)));
|
||||
assertEquals(0, source.mutationCheckpoint());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sourceCaptureCannotReplaceAuthoredRoomSolids() {
|
||||
MantleWriter writer = writer();
|
||||
|
||||
@@ -326,7 +326,7 @@ public class ObjectSourcePlanCacheTest {
|
||||
List<ObjectDestinationTransaction.Mutation> mutations = new ArrayList<>();
|
||||
for (int[] chunk : chunks) {
|
||||
mutations.add(new ObjectDestinationTransaction.SetMutation(
|
||||
new ObjectDestinationTransaction.DataKey(chunk[0] << 4, 4, chunk[1] << 4, String.class), "marker"));
|
||||
new ObjectDestinationTransaction.DataKey(chunk[0] << 4, 4, chunk[1] << 4, String.class), "marker", false));
|
||||
}
|
||||
return new ObjectSourcePlan(mutations);
|
||||
}
|
||||
|
||||
@@ -74,14 +74,14 @@ public class ObjectSourcePlanTest {
|
||||
int z = centerZ + random.nextInt(2 * reach + 1) - reach;
|
||||
int y = random.nextInt(768);
|
||||
mutations.add(switch (random.nextInt(6)) {
|
||||
case 0 -> new ObjectDestinationTransaction.CustomBlockMutation(key(x, y, z, NativeBlockState.class), custom);
|
||||
case 1 -> new ObjectDestinationTransaction.SetMutation(key(x, y, z, String.class), placementMarker);
|
||||
case 0 -> new ObjectDestinationTransaction.CustomBlockMutation(key(x, y, z, NativeBlockState.class), custom, random.nextBoolean());
|
||||
case 1 -> new ObjectDestinationTransaction.SetMutation(key(x, y, z, String.class), placementMarker, random.nextBoolean());
|
||||
case 2 -> new ObjectDestinationTransaction.SetMutation(key(x, y, z, TreeBlockMaterial.class),
|
||||
new TreeBlockMaterial("minecraft:oak_log"));
|
||||
case 3 -> new ObjectDestinationTransaction.SetMutation(key(x, y, z, MatterMarker.class), marker);
|
||||
new TreeBlockMaterial("minecraft:oak_log"), random.nextBoolean());
|
||||
case 3 -> new ObjectDestinationTransaction.SetMutation(key(x, y, z, MatterMarker.class), marker, random.nextBoolean());
|
||||
default -> {
|
||||
NativeBlockState state = states[random.nextInt(states.length)];
|
||||
yield new ObjectDestinationTransaction.SetMutation(key(x, y, z, NativeBlockState.class), state);
|
||||
yield new ObjectDestinationTransaction.SetMutation(key(x, y, z, NativeBlockState.class), state, random.nextBoolean());
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -109,11 +109,11 @@ public class ObjectSourcePlanTest {
|
||||
@Test
|
||||
public void heightsOutsideThePackedRangeAreRejected() {
|
||||
assertThrows(IllegalArgumentException.class, () -> new ObjectSourcePlan(List.of(
|
||||
new ObjectDestinationTransaction.SetMutation(key(0, -1, 0, String.class), "marker"))));
|
||||
new ObjectDestinationTransaction.SetMutation(key(0, -1, 0, String.class), "marker", false))));
|
||||
assertThrows(IllegalArgumentException.class, () -> new ObjectSourcePlan(List.of(
|
||||
new ObjectDestinationTransaction.SetMutation(key(0, 1 << 22, 0, String.class), "marker"))));
|
||||
new ObjectDestinationTransaction.SetMutation(key(0, 1 << 22, 0, String.class), "marker", false))));
|
||||
assertThrows(IllegalArgumentException.class, () -> new ObjectSourcePlan(List.of(
|
||||
new ObjectDestinationTransaction.SetMutation(key(0, 4, 0, Object.class), "marker"))));
|
||||
new ObjectDestinationTransaction.SetMutation(key(0, 4, 0, Object.class), "marker", false))));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -139,7 +139,7 @@ public class ObjectSourcePlanTest {
|
||||
List<ObjectDestinationTransaction.Mutation> trees = new ArrayList<>();
|
||||
for (int index = 0; index < 100; index++) {
|
||||
trees.add(new ObjectDestinationTransaction.SetMutation(key(index & 15, 4, 0, TreeBlockMaterial.class),
|
||||
new TreeBlockMaterial("minecraft:oak_log")));
|
||||
new TreeBlockMaterial("minecraft:oak_log"), false));
|
||||
}
|
||||
int single = new ObjectSourcePlan(one).estimatedRetainedBytes();
|
||||
|
||||
@@ -150,15 +150,15 @@ public class ObjectSourcePlanTest {
|
||||
@Test
|
||||
public void retainedBytesIncludeVariableLengthMarkers() {
|
||||
ObjectDestinationTransaction.DataKey key = new ObjectDestinationTransaction.DataKey(0, 4, 0, String.class);
|
||||
ObjectSourcePlan shortMarker = new ObjectSourcePlan(List.of(new ObjectDestinationTransaction.SetMutation(key, "a")));
|
||||
ObjectSourcePlan longMarker = new ObjectSourcePlan(List.of(new ObjectDestinationTransaction.SetMutation(key, "a".repeat(4096))));
|
||||
ObjectSourcePlan shortMarker = new ObjectSourcePlan(List.of(new ObjectDestinationTransaction.SetMutation(key, "a", false)));
|
||||
ObjectSourcePlan longMarker = new ObjectSourcePlan(List.of(new ObjectDestinationTransaction.SetMutation(key, "a".repeat(4096), false)));
|
||||
assertTrue(longMarker.estimatedRetainedBytes() - shortMarker.estimatedRetainedBytes() >= 8190);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknownPayloadsAreWeighedInsteadOfDisablingRetention() {
|
||||
ObjectSourcePlan unknown = new ObjectSourcePlan(List.of(new ObjectDestinationTransaction.SetMutation(
|
||||
key(0, 4, 0, Object.class), new Object())));
|
||||
key(0, 4, 0, Object.class), new Object(), false)));
|
||||
|
||||
assertTrue(unknown.estimatedRetainedBytes() > new ObjectSourcePlan(List.of(mutationAt(0, 0))).estimatedRetainedBytes());
|
||||
assertTrue(unknown.estimatedRetainedBytes() < 4096);
|
||||
@@ -169,9 +169,9 @@ public class ObjectSourcePlanTest {
|
||||
List<ObjectDestinationTransaction.Mutation> shortMarkers = List.of(mutationAt(0, 0), mutationAt(16, 0));
|
||||
List<ObjectDestinationTransaction.Mutation> longMarkers = List.of(
|
||||
new ObjectDestinationTransaction.SetMutation(
|
||||
new ObjectDestinationTransaction.DataKey(0, 4, 0, String.class), "a".repeat(4096)),
|
||||
new ObjectDestinationTransaction.DataKey(0, 4, 0, String.class), "a".repeat(4096), false),
|
||||
new ObjectDestinationTransaction.SetMutation(
|
||||
new ObjectDestinationTransaction.DataKey(16, 4, 0, String.class), "b".repeat(4096)));
|
||||
new ObjectDestinationTransaction.DataKey(16, 4, 0, String.class), "b".repeat(4096), false));
|
||||
ObjectDestinationTransaction.PlacementRange whole = new ObjectDestinationTransaction.PlacementRange(0, 2);
|
||||
ObjectSourcePlan shortPlain = new ObjectSourcePlan(shortMarkers);
|
||||
ObjectSourcePlan shortCrossing = new ObjectSourcePlan(shortMarkers, List.of(whole));
|
||||
@@ -224,6 +224,6 @@ public class ObjectSourcePlanTest {
|
||||
|
||||
private static ObjectDestinationTransaction.Mutation mutationAt(int x, int z) {
|
||||
return new ObjectDestinationTransaction.SetMutation(
|
||||
new ObjectDestinationTransaction.DataKey(x, 4, z, String.class), "marker");
|
||||
new ObjectDestinationTransaction.DataKey(x, 4, z, String.class), "marker", false);
|
||||
}
|
||||
}
|
||||
@@ -83,7 +83,7 @@ public class SchemaBuilderIdentityTest {
|
||||
@ClassRule
|
||||
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
|
||||
|
||||
private static final String EXPECTED_SCHEMA_DIGEST = "c99b0db94bfd6f316738beb87401edde5b52eed11bfa419148bb2e8c08b0d7a9";
|
||||
private static final String EXPECTED_SCHEMA_DIGEST = "f0be31c11f7b03061a88387c66578058f447e5803cabe536a34721066d2bc636";
|
||||
|
||||
private static final List<Class<?>> SCHEMA_ROOTS = List.of(
|
||||
IrisDimension.class,
|
||||
|
||||
@@ -0,0 +1,498 @@
|
||||
package art.arcane.iris.structure.object;
|
||||
|
||||
import art.arcane.iris.generation.decoration.tree.IrisProceduralTree;
|
||||
import art.arcane.iris.generation.decoration.tree.ProceduralTreeGenerator;
|
||||
import art.arcane.iris.generation.decoration.tree.TreeBlockCanvas;
|
||||
import art.arcane.iris.generation.decoration.tree.TreeGrounding;
|
||||
import art.arcane.iris.generation.geometry.IrisBlockVector;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.testsupport.PlatformBinding;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class TreeGroundingPlacementTest {
|
||||
private static final int SURFACE_Y = 80;
|
||||
|
||||
@Rule
|
||||
public final PlatformBinding platform = PlatformBinding.mockPlatform();
|
||||
|
||||
private IrisData data;
|
||||
private NativeBlockState wood;
|
||||
private NativeBlockState leaves;
|
||||
|
||||
@Before
|
||||
public void bindStates() {
|
||||
wood = state("minecraft:oak_log");
|
||||
leaves = state("minecraft:oak_leaves");
|
||||
when(leaves.isFoliage()).thenReturn(true);
|
||||
Engine engine = mock(Engine.class);
|
||||
when(engine.getHeight()).thenReturn(256);
|
||||
data = mock(IrisData.class);
|
||||
when(data.getEngine()).thenReturn(engine);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flatGroundKeepsTheAuthoredBase() {
|
||||
IrisObject tree = tree(true);
|
||||
Terrain terrain = new Terrain();
|
||||
|
||||
assertTrue(place(tree, terrain, placement()) >= 0);
|
||||
|
||||
assertEquals(tree.getBlocks().size(), terrain.written.size());
|
||||
assertSame(wood, terrain.written.get(new Position(0, 80, 0)));
|
||||
assertSame(wood, terrain.written.get(new Position(1, 81, 0)));
|
||||
assertFalse(terrain.written.containsKey(new Position(1, 79, 0)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void gentleSlopeExtendsOnlyUnsupportedRootColumns() {
|
||||
Terrain terrain = new Terrain();
|
||||
terrain.heights.put(new Column(1, 0), 77);
|
||||
|
||||
assertTrue(place(tree(true), terrain, placement()) >= 0);
|
||||
|
||||
for (int y = 77; y <= 81; y++) {
|
||||
assertSame(wood, terrain.written.get(new Position(1, y, 0)));
|
||||
}
|
||||
assertFalse(terrain.written.containsKey(new Position(0, 79, 0)));
|
||||
assertFalse(terrain.written.containsKey(new Position(2, 79, 0)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cliffRejectionWritesNoPartOfTheTreeOrListener() {
|
||||
Terrain terrain = new Terrain();
|
||||
terrain.heights.put(new Column(1, 0), 70);
|
||||
Map<Position, NativeBlockState> observed = new HashMap<>();
|
||||
|
||||
int result = tree(true).place(0, -1, 0, terrain.placer, placement(), new RNG(42L),
|
||||
(position, state) -> observed.put(new Position(position.getX(), position.getY(), position.getZ()), state),
|
||||
null, data);
|
||||
|
||||
assertEquals(-1, result);
|
||||
assertTrue(terrain.written.isEmpty());
|
||||
assertTrue(observed.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void carvedRootContactRejectsBeforeWriting() {
|
||||
Terrain terrain = new Terrain();
|
||||
terrain.carved.add(new Position(1, 79, 0));
|
||||
|
||||
assertEquals(-1, place(tree(true), terrain, placement()));
|
||||
|
||||
assertTrue(terrain.written.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rotatedTranslatedRootsUseTheirFinalWorldCoordinates() {
|
||||
Terrain terrain = new Terrain();
|
||||
terrain.heights.put(new Column(13, 17), 78);
|
||||
IrisObjectPlacement placement = placement();
|
||||
placement.setRotation(IrisObjectRotation.of(0, 90, 0));
|
||||
placement.setTranslate(new IrisObjectTranslate().setX(2).setY(1).setZ(3));
|
||||
|
||||
int result = tree(true).place(10, -1, 20, terrain.placer, placement, new RNG(42L), data);
|
||||
|
||||
assertTrue(result >= 0);
|
||||
assertSame(wood, terrain.written.get(new Position(13, 81, 18)));
|
||||
for (int y = 78; y <= 82; y++) {
|
||||
assertSame(wood, terrain.written.get(new Position(13, y, 17)));
|
||||
}
|
||||
assertFalse(terrain.written.containsKey(new Position(11, 80, 20)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rootlessTreesRequireContactWithoutAddingRoots() {
|
||||
Terrain flat = new Terrain();
|
||||
IrisObject rootless = tree(false);
|
||||
assertTrue(place(rootless, flat, placement()) >= 0);
|
||||
assertEquals(rootless.getBlocks().size(), flat.written.size());
|
||||
|
||||
Terrain slope = new Terrain();
|
||||
slope.heights.put(new Column(1, 0), 79);
|
||||
|
||||
assertEquals(-1, place(rootless, slope, placement()));
|
||||
assertTrue(slope.written.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void placementListenerReceivesEveryGroundingBlock() {
|
||||
Terrain terrain = new Terrain();
|
||||
terrain.heights.put(new Column(1, 0), 77);
|
||||
Map<Position, NativeBlockState> observed = new HashMap<>();
|
||||
|
||||
int result = tree(true).place(0, -1, 0, terrain.placer, placement(), new RNG(42L),
|
||||
(position, state) -> observed.put(new Position(position.getX(), position.getY(), position.getZ()), state),
|
||||
null, data);
|
||||
|
||||
assertTrue(result >= 0);
|
||||
assertEquals(terrain.written, observed);
|
||||
assertSame(wood, observed.get(new Position(1, 77, 0)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void repeatedPlacementDoesNotMutateCachedVariants() {
|
||||
IrisObject tree = tree(true);
|
||||
Map<Position, NativeBlockState> original = snapshot(tree);
|
||||
TreeGrounding originalGrounding = tree.getTreeGrounding();
|
||||
Terrain first = new Terrain();
|
||||
first.heights.put(new Column(1, 0), 77);
|
||||
Terrain cliff = new Terrain();
|
||||
cliff.heights.put(new Column(1, 0), 60);
|
||||
Terrain second = new Terrain();
|
||||
second.heights.put(new Column(1, 0), 77);
|
||||
|
||||
assertTrue(place(tree, first, placement()) >= 0);
|
||||
assertEquals(-1, place(tree, cliff, placement()));
|
||||
assertTrue(place(tree, second, placement()) >= 0);
|
||||
|
||||
assertEquals(first.written, second.written);
|
||||
assertEquals(original, snapshot(tree));
|
||||
assertSame(originalGrounding, tree.getTreeGrounding());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canopyOverhangDoesNotBecomeAGroundSupportFootprint() {
|
||||
Terrain terrain = new Terrain();
|
||||
terrain.heights.put(new Column(4, 0), 20);
|
||||
terrain.carved.add(new Position(4, 20, 0));
|
||||
|
||||
assertTrue(place(tree(true), terrain, placement()) >= 0);
|
||||
|
||||
assertSame(leaves, terrain.written.get(new Position(4, 84, 0)));
|
||||
assertFalse(terrain.written.containsKey(new Position(4, 80, 0)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void genericStiltingDoesNotRepeatTheRootBase() {
|
||||
IrisObject tree = tree(true);
|
||||
Terrain ordinary = new Terrain();
|
||||
ordinary.heights.put(new Column(1, 0), 77);
|
||||
Terrain stilted = new Terrain();
|
||||
stilted.heights.put(new Column(1, 0), 77);
|
||||
|
||||
assertTrue(place(tree, ordinary, placement()) >= 0);
|
||||
assertTrue(place(tree, stilted, placement().setMode(ObjectPlaceMode.CENTER_STILT)) >= 0);
|
||||
|
||||
assertEquals(ordinary.written, stilted.written);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generatedLeaningTreesKeepTheirTrunkAtThePlacementOrigin() {
|
||||
NativeBlockState stone = state("minecraft:stone");
|
||||
when(platform.registries().block(anyString())).thenReturn(stone);
|
||||
when(platform.registries().blockOrNull(anyString(), anyBoolean())).thenReturn(stone);
|
||||
IrisProceduralTree definition = new IrisProceduralTree();
|
||||
definition.setTrunk("minecraft:stone");
|
||||
definition.setLeaves("minecraft:stone");
|
||||
definition.setLeanAngle(40);
|
||||
definition.setLeanAzimuth(90);
|
||||
definition.setRoots(false);
|
||||
definition.setPlausible(false);
|
||||
|
||||
IrisObject generated = ProceduralTreeGenerator.generate(definition, 20, new RNG(42L), data);
|
||||
|
||||
assertNotNull(generated);
|
||||
assertNotNull(generated.getTreeGrounding());
|
||||
List<TreeGrounding.Root> roots = generated.getTreeGrounding().roots();
|
||||
assertEquals(1, roots.size());
|
||||
assertEquals(0, roots.getFirst().x());
|
||||
assertEquals(0, roots.getFirst().z());
|
||||
assertEquals(1 - generated.getCenter().getBlockY(), roots.getFirst().y());
|
||||
Terrain terrain = new Terrain();
|
||||
assertTrue(place(generated, terrain, placement()) >= 0);
|
||||
assertSame(stone, terrain.written.get(new Position(0, 81, 0)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void scaledVariantsRetainGroundingAtCliffs() {
|
||||
IrisObject source = tree(true);
|
||||
Map<Position, NativeBlockState> original = snapshot(source);
|
||||
for (double scale : new double[]{1.5, 2, 3}) {
|
||||
IrisObjectPlacement placement = placement();
|
||||
IrisObject scaled = placement.scaleObject(new RNG(42L), source,
|
||||
new IrisDimension().setAllObjectScaleFactor(scale));
|
||||
assertNotSame(source, scaled);
|
||||
assertGroundingMatchesBlocks(scaled);
|
||||
Terrain terrain = new Terrain();
|
||||
for (TreeGrounding.Root root : scaled.getTreeGrounding().roots()) {
|
||||
if (root.x() != 0 || root.z() != 0) {
|
||||
terrain.heights.put(new Column(root.x(), root.z()), 40);
|
||||
}
|
||||
}
|
||||
assertFalse(terrain.heights.isEmpty());
|
||||
|
||||
assertEquals(-1, place(scaled, terrain, placement));
|
||||
|
||||
assertTrue(terrain.written.isEmpty());
|
||||
assertEquals(original, snapshot(source));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void scaledRootMetadataMatchesBlocksForBothOrigins() {
|
||||
IrisObject source = tree(true);
|
||||
TreeGrounding grounding = source.getTreeGrounding();
|
||||
for (double scale : new double[]{0.5, 1.5, 2, 3}) {
|
||||
IrisObject centered = source.scaled(scale, IrisObjectPlacementScaleInterpolator.NONE);
|
||||
IrisObject savedOrigin = source.scaledAroundOrigin(scale, IrisObjectPlacementScaleInterpolator.NONE);
|
||||
|
||||
assertGroundingMatchesBlocks(centered);
|
||||
assertGroundingMatchesBlocks(savedOrigin);
|
||||
assertEquals((int) Math.ceil(3 * scale), centered.getTreeGrounding().maxExtension());
|
||||
assertEquals((int) Math.ceil(3 * scale), savedOrigin.getTreeGrounding().maxExtension());
|
||||
assertSame(grounding, source.getTreeGrounding());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rotatedCopiesKeepMetadataOnTheirActualRootBlocks() {
|
||||
IrisObject source = tree(true);
|
||||
Map<Position, NativeBlockState> original = snapshot(source);
|
||||
TreeGrounding grounding = source.getTreeGrounding();
|
||||
for (int angle : new int[]{45, 90, 180, 270}) {
|
||||
IrisObject rotated = source.rotateCopy(IrisObjectRotation.of(0, angle, 0));
|
||||
|
||||
assertGroundingMatchesBlocks(rotated);
|
||||
assertEquals(3, rotated.getTreeGrounding().maxExtension());
|
||||
assertEquals(original, snapshot(source));
|
||||
assertSame(grounding, source.getTreeGrounding());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shrinkwrapMovesRootMetadataAndInvalidatesItsFootprint() {
|
||||
IrisObject source = tree(true);
|
||||
Map<Position, NativeBlockState> original = snapshot(source);
|
||||
IrisObject shrunk = source.copy();
|
||||
List<IrisBlockVector> oldFootprint = List.copyOf(shrunk.getSurfaceSupportOffsets());
|
||||
|
||||
shrunk.shrinkwrap();
|
||||
|
||||
assertGroundingMatchesBlocks(shrunk);
|
||||
assertFalse(oldFootprint.equals(shrunk.getSurfaceSupportOffsets()));
|
||||
assertEquals(shrunk.getTreeGrounding().supportOffsets(), shrunk.getSurfaceSupportOffsets());
|
||||
assertEquals(original, snapshot(source));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void overlappingCanopiesPreserveWoodAndSkipListenerWrites() {
|
||||
for (Palette palette : List.of(new Palette("minecraft:oak_wood", "minecraft:birch_leaves"),
|
||||
new Palette("minecraft:warped_hyphae", "minecraft:warped_wart_block"),
|
||||
new Palette("test:trunk", "test:canopy"))) {
|
||||
wood = state(palette.trunk());
|
||||
leaves = state(palette.canopy());
|
||||
Terrain terrain = new Terrain();
|
||||
IrisObject earlier = tree(true);
|
||||
earlier.getBlocks().put(new IrisBlockVector(0, 1, 0), wood);
|
||||
assertTrue(earlier.place(4, -1, 0, terrain.placer, placement(), new RNG(42L), data) >= 0);
|
||||
IrisObject later = tree(true);
|
||||
later.getBlocks().put(new IrisBlockVector(4, 2, 0), leaves);
|
||||
later.setTreeGrounding(TreeGrounding.create(later.getTreeGrounding().roots(), 3,
|
||||
Set.of(new TreeBlockCanvas.Vec(4, 1, 0), new TreeBlockCanvas.Vec(4, 2, 0))));
|
||||
Map<Position, NativeBlockState> observed = new HashMap<>();
|
||||
|
||||
int result = later.place(0, -1, 0, terrain.placer, placement(), new RNG(42L),
|
||||
(position, state) -> observed.put(new Position(position.getX(), position.getY(), position.getZ()), state),
|
||||
null, data);
|
||||
|
||||
assertTrue(result >= 0);
|
||||
assertSame(wood, terrain.written.get(new Position(4, 84, 0)));
|
||||
assertFalse(observed.containsKey(new Position(4, 84, 0)));
|
||||
assertSame(leaves, observed.get(new Position(4, 85, 0)));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canopyRolesRemainDistinctWhenWoodAndLeavesShareAMaterial() {
|
||||
leaves = wood;
|
||||
NativeBlockState existing = state("test:older_trunk");
|
||||
Terrain terrain = new Terrain();
|
||||
terrain.written.put(new Position(4, 84, 0), existing);
|
||||
terrain.written.put(new Position(0, 83, 0), existing);
|
||||
|
||||
assertTrue(place(tree(true), terrain, placement()) >= 0);
|
||||
|
||||
assertSame(existing, terrain.written.get(new Position(4, 84, 0)));
|
||||
assertSame(wood, terrain.written.get(new Position(0, 83, 0)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void treeWoodStillReplacesOverlappingCanopy() {
|
||||
Terrain terrain = new Terrain();
|
||||
terrain.written.put(new Position(0, 83, 0), leaves);
|
||||
|
||||
assertTrue(place(tree(true), terrain, placement()) >= 0);
|
||||
|
||||
assertSame(wood, terrain.written.get(new Position(0, 83, 0)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ordinaryObjectCanopiesKeepTheirPlacementSemantics() {
|
||||
Terrain terrain = new Terrain();
|
||||
terrain.written.put(new Position(4, 84, 0), wood);
|
||||
IrisObject ordinary = tree(true);
|
||||
ordinary.setTreeGrounding(null);
|
||||
|
||||
assertTrue(place(ordinary, terrain, placement()) >= 0);
|
||||
|
||||
assertSame(leaves, terrain.written.get(new Position(4, 84, 0)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transformedCanopyRolesStillProtectExistingSolids() {
|
||||
IrisObject source = tree(true);
|
||||
IrisObject shrunk = source.copy();
|
||||
shrunk.shrinkwrap();
|
||||
for (IrisObject transformed : List.of(source.copy(), shrunk,
|
||||
source.rotateCopy(IrisObjectRotation.of(0, 90, 0)),
|
||||
source.scaled(2, IrisObjectPlacementScaleInterpolator.NONE),
|
||||
source.scaledAroundOrigin(2, IrisObjectPlacementScaleInterpolator.NONE))) {
|
||||
assertGroundingMatchesBlocks(transformed);
|
||||
TreeBlockCanvas.Vec canopy = transformed.getTreeGrounding().canopy().iterator().next();
|
||||
Position target = new Position(canopy.x(), 100 + canopy.y(), canopy.z());
|
||||
NativeBlockState existing = state("test:older_trunk");
|
||||
Terrain terrain = new Terrain();
|
||||
terrain.written.put(target, existing);
|
||||
|
||||
assertTrue(transformed.place(0, 100, 0, terrain.placer, placement(), new RNG(42L), data) >= 0);
|
||||
|
||||
assertSame(existing, terrain.written.get(target));
|
||||
}
|
||||
}
|
||||
|
||||
private int place(IrisObject tree, Terrain terrain, IrisObjectPlacement placement) {
|
||||
return tree.place(0, -1, 0, terrain.placer, placement, new RNG(42L), data);
|
||||
}
|
||||
|
||||
private IrisObjectPlacement placement() {
|
||||
return new IrisObjectPlacement().setMode(ObjectPlaceMode.CENTER_HEIGHT)
|
||||
.setRotation(IrisObjectRotation.of(0, 0, 0)).setSurfaceSupportBuffer(0);
|
||||
}
|
||||
|
||||
private IrisObject tree(boolean rooted) {
|
||||
IrisObject tree = new IrisObject(9, 7, 3);
|
||||
tree.setLoader(data);
|
||||
tree.getBlocks().put(new IrisBlockVector(0, -2, 0), wood);
|
||||
tree.getBlocks().put(new IrisBlockVector(0, -1, 0), wood);
|
||||
tree.getBlocks().put(new IrisBlockVector(0, 0, 0), wood);
|
||||
tree.getBlocks().put(new IrisBlockVector(1, -2, 0), wood);
|
||||
tree.getBlocks().put(new IrisBlockVector(4, 0, 0), wood);
|
||||
tree.getBlocks().put(new IrisBlockVector(4, 1, 0), leaves);
|
||||
List<TreeGrounding.Root> roots = new ArrayList<>();
|
||||
if (rooted) {
|
||||
tree.getBlocks().put(new IrisBlockVector(0, -3, 0), wood);
|
||||
roots.add(new TreeGrounding.Root(0, -3, 0, wood));
|
||||
} else {
|
||||
roots.add(new TreeGrounding.Root(0, -2, 0, wood));
|
||||
}
|
||||
roots.add(new TreeGrounding.Root(1, -2, 0, wood));
|
||||
tree.setTreeGrounding(TreeGrounding.create(roots, rooted ? 3 : 0,
|
||||
Set.of(new TreeBlockCanvas.Vec(4, 1, 0))));
|
||||
return tree;
|
||||
}
|
||||
|
||||
private static Map<Position, NativeBlockState> snapshot(IrisObject tree) {
|
||||
Map<Position, NativeBlockState> snapshot = new HashMap<>();
|
||||
for (Map.Entry<IrisBlockVector, NativeBlockState> entry : tree.getBlocks()) {
|
||||
IrisBlockVector position = entry.getKey();
|
||||
snapshot.put(new Position(position.getBlockX(), position.getBlockY(), position.getBlockZ()), entry.getValue());
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private void assertGroundingMatchesBlocks(IrisObject tree) {
|
||||
TreeGrounding grounding = tree.getTreeGrounding();
|
||||
assertNotNull(grounding);
|
||||
assertFalse(grounding.roots().isEmpty());
|
||||
Set<Column> columns = new HashSet<>();
|
||||
for (TreeGrounding.Root root : grounding.roots()) {
|
||||
assertTrue(columns.add(new Column(root.x(), root.z())));
|
||||
assertSame(root.state(), tree.getBlocks().get(new IrisBlockVector(root.x(), root.y(), root.z())));
|
||||
assertFalse(root.state().isFoliage());
|
||||
}
|
||||
for (Map.Entry<IrisBlockVector, NativeBlockState> entry : tree.getBlocks()) {
|
||||
IrisBlockVector cell = entry.getKey();
|
||||
assertEquals(entry.getValue() == leaves, grounding.isCanopy(cell.getBlockX(), cell.getBlockY(), cell.getBlockZ()));
|
||||
}
|
||||
}
|
||||
|
||||
private static NativeBlockState state(String key) {
|
||||
NativeBlockState state = mock(NativeBlockState.class);
|
||||
when(state.key()).thenReturn(key);
|
||||
when(state.materialKey()).thenReturn(key);
|
||||
when(state.isSolid()).thenReturn(true);
|
||||
when(state.isOccluding()).thenReturn(true);
|
||||
return state;
|
||||
}
|
||||
|
||||
private static final class Terrain {
|
||||
private final IObjectPlacer placer = mock(IObjectPlacer.class);
|
||||
private final Map<Column, Integer> heights = new HashMap<>();
|
||||
private final Map<Position, NativeBlockState> written = new HashMap<>();
|
||||
private final Set<Position> carved = new HashSet<>();
|
||||
|
||||
private Terrain() {
|
||||
when(placer.beginTreeCanopy(any())).thenCallRealMethod();
|
||||
when(placer.getHighest(anyInt(), anyInt(), any())).thenAnswer(invocation ->
|
||||
surface(invocation.getArgument(0), invocation.getArgument(1)));
|
||||
when(placer.getHighest(anyInt(), anyInt(), any(), anyBoolean())).thenAnswer(invocation ->
|
||||
surface(invocation.getArgument(0), invocation.getArgument(1)));
|
||||
when(placer.isSurfaceSolid(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> {
|
||||
int x = invocation.getArgument(0);
|
||||
int y = invocation.getArgument(1);
|
||||
int z = invocation.getArgument(2);
|
||||
return y >= 0 && y <= surface(x, z) && !carved.contains(new Position(x, y, z));
|
||||
});
|
||||
when(placer.isCarved(anyInt(), anyInt(), anyInt())).thenAnswer(invocation ->
|
||||
carved.contains(new Position(invocation.getArgument(0), invocation.getArgument(1), invocation.getArgument(2))));
|
||||
when(placer.get(anyInt(), anyInt(), anyInt())).thenAnswer(invocation ->
|
||||
written.get(new Position(invocation.getArgument(0), invocation.getArgument(1), invocation.getArgument(2))));
|
||||
doAnswer(invocation -> {
|
||||
Position position = new Position(invocation.getArgument(0), invocation.getArgument(1), invocation.getArgument(2));
|
||||
NativeBlockState state = invocation.getArgument(3);
|
||||
written.put(position, state);
|
||||
return null;
|
||||
}).when(placer).set(anyInt(), anyInt(), anyInt(), any());
|
||||
}
|
||||
|
||||
private int surface(int x, int z) {
|
||||
return heights.getOrDefault(new Column(x, z), SURFACE_Y);
|
||||
}
|
||||
}
|
||||
|
||||
private record Column(int x, int z) {
|
||||
}
|
||||
|
||||
private record Position(int x, int y, int z) {
|
||||
}
|
||||
|
||||
private record Palette(String trunk, String canopy) {
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user