mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 14:00:26 +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
@@ -0,0 +1,143 @@
|
|||||||
|
import { readFile, realpath } from 'node:fs/promises'
|
||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
|
const nonGround = new Set(['air', 'cave_air', 'void_air', 'water', 'lava', 'nether_wart_block', 'warped_wart_block'])
|
||||||
|
const chunkOffsets = [-56, -40, -24, -8, 8, 24, 40, 56]
|
||||||
|
|
||||||
|
function isWood(block) {
|
||||||
|
return block !== null && /_(wood|hyphae|log|stem)$/.test(block.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isGround(block) {
|
||||||
|
return block !== null && block.boundingBox === 'block' && !nonGround.has(block.name)
|
||||||
|
&& !block.name.endsWith('_leaves') && !isWood(block)
|
||||||
|
}
|
||||||
|
|
||||||
|
function positionKey(position) {
|
||||||
|
return `${position.x},${position.y},${position.z}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function groundSpread(bot, base) {
|
||||||
|
const heights = []
|
||||||
|
for (const x of [-3, 0, 3]) {
|
||||||
|
for (const z of [-3, 0, 3]) {
|
||||||
|
for (let y = base.y + 6; y >= base.y - 8; y--) {
|
||||||
|
if (!isGround(bot.blockAt(base.offset(x, y - base.y, z)))) continue
|
||||||
|
heights.push(y)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return heights.length === 9 ? Math.max(...heights) - Math.min(...heights) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function inspectTrees(context) {
|
||||||
|
const bot = context.bot
|
||||||
|
const center = bot.entity.position.floored()
|
||||||
|
const woodIds = Object.values(bot.registry.blocksByName).filter(isWood).map(block => block.id)
|
||||||
|
const positions = bot.findBlocks({ matching: woodIds, point: center, maxDistance: 64, count: 20000 })
|
||||||
|
const columns = new Map()
|
||||||
|
for (const position of positions) {
|
||||||
|
const key = `${position.x},${position.z}`
|
||||||
|
if (!columns.has(key)) columns.set(key, [])
|
||||||
|
columns.get(key).push(position)
|
||||||
|
}
|
||||||
|
const trees = []
|
||||||
|
for (const column of columns.values()) {
|
||||||
|
column.sort((first, second) => first.y - second.y)
|
||||||
|
const base = column[0]
|
||||||
|
const height = column[column.length - 1].y - base.y + 1
|
||||||
|
if (column.length < 8 || height < 8 || column.length / height < 0.65) continue
|
||||||
|
if (column.some(position => position.distanceTo(center) >= 60)) continue
|
||||||
|
if (![0, 1, 2, 3].every(y => isWood(bot.blockAt(base.offset(0, y, 0))))) continue
|
||||||
|
const upperGaps = []
|
||||||
|
for (let y = base.y; y < base.y + height; y++) {
|
||||||
|
const block = bot.blockAt(base.offset(0, y - base.y, 0))
|
||||||
|
if (block === null || !isWood(block)) upperGaps.push({ y, block: block?.name ?? null })
|
||||||
|
}
|
||||||
|
const grounded = isGround(bot.blockAt(base.offset(0, -1, 0)))
|
||||||
|
const spread = groundSpread(bot, base)
|
||||||
|
trees.push({ base: { x: base.x, y: base.y, z: base.z }, woodBlocks: column.length,
|
||||||
|
height, grounded, terrainSpread: spread, upperGaps })
|
||||||
|
}
|
||||||
|
return trees
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'iris-tree-grounding',
|
||||||
|
description: 'Verify grounded spindle bases, continuous overlapping trunks, and paired pack geometry.',
|
||||||
|
async run(context) {
|
||||||
|
const directory = await realpath(context.server.directory)
|
||||||
|
const source = await readFile(path.join(directory, '.server-source'), 'utf8')
|
||||||
|
context.expect(/^isolated=true\s*$/m.test(source), 'Tree generation requires an isolated instance')
|
||||||
|
const packs = await realpath(path.join(directory, 'plugins', 'Iris', 'packs'))
|
||||||
|
context.expect(packs.startsWith(`${directory}${path.sep}`), 'Tree tests require local pack copies')
|
||||||
|
context.report.treeWorlds = []
|
||||||
|
context.report.treeGrounding = []
|
||||||
|
context.report.treeContinuity = []
|
||||||
|
for (const pack of ['overworld', 'underworld']) {
|
||||||
|
const dimension = JSON.parse(await readFile(path.join(packs, pack, 'dimensions', `${pack}.json`), 'utf8'))
|
||||||
|
context.expect(dimension.focus === 'estranged/spindlewood', 'Pack must focus on spindlewood', { pack })
|
||||||
|
await context.step(`create ${pack} spindle world`, async () => {
|
||||||
|
const world = `tree_${pack}_${Date.now().toString(36)}`
|
||||||
|
const response = await context.command(`/iris create name=${world} type=${pack} seed=78264193`,
|
||||||
|
/Successfully created your world|Failed to create/, 240000)
|
||||||
|
context.expect(response.includes('Successfully created your world'), response)
|
||||||
|
context.report.treeWorlds.push({ pack, name: world, seed: 78264193, biome: dimension.focus })
|
||||||
|
if (context.bot.game.gameMode !== 'spectator') {
|
||||||
|
await context.command('/minecraft:gamemode spectator', /spectator/i, 30000)
|
||||||
|
}
|
||||||
|
await context.waitUntil(() => context.bot.game.gameMode === 'spectator',
|
||||||
|
{ label: 'Spectator mode for terrain inspection', timeoutMs: 30000, intervalMs: 100 })
|
||||||
|
})
|
||||||
|
await context.step(`${pack} trees connect to flat and sloped terrain`, async () => {
|
||||||
|
const origin = context.bot.entity.position.floored()
|
||||||
|
const inspected = new Map()
|
||||||
|
for (const offset of [0, 80, 160]) {
|
||||||
|
await context.command(`/minecraft:tp @s ${origin.x + offset + 0.5} ${origin.y + 12} ${origin.z + 0.5}`,
|
||||||
|
/Teleported/i, 30000)
|
||||||
|
await context.waitUntil(() => {
|
||||||
|
const center = context.bot.entity.position.floored()
|
||||||
|
if (Math.abs(center.x - origin.x - offset) > 1) return false
|
||||||
|
return chunkOffsets.every(x => chunkOffsets.every(z => context.bot.blockAt(center.offset(x, 0, z)) !== null))
|
||||||
|
}, { label: 'Complete terrain neighborhood', timeoutMs: 90000, intervalMs: 250 })
|
||||||
|
const trees = inspectTrees(context)
|
||||||
|
for (const tree of trees) {
|
||||||
|
context.expect(tree.grounded, 'Generated spindle trunk base has no terrain connection', { pack, ...tree })
|
||||||
|
inspected.set(positionKey(tree.base), tree)
|
||||||
|
}
|
||||||
|
if (inspected.size >= 8 && [...inspected.values()].some(tree => tree.terrainSpread >= 2)) break
|
||||||
|
}
|
||||||
|
const trees = [...inspected.values()]
|
||||||
|
const sloped = trees.filter(tree => tree.terrainSpread >= 2)
|
||||||
|
context.expect(trees.length >= 8, 'Expected at least eight generated spindle trunk bases', { pack, trees })
|
||||||
|
context.expect(sloped.length > 0, 'Expected a grounded tree on uneven terrain', { pack, trees })
|
||||||
|
context.report.treeGrounding.push({ pack, trees: trees.length, sloped: sloped.length, samples: trees })
|
||||||
|
})
|
||||||
|
await context.step(`${pack} overlapping canopy preserves the seeded main trunk`, async () => {
|
||||||
|
await context.command('/minecraft:tp @s 0.5 88 0.5', /Teleported/i, 30000)
|
||||||
|
await context.waitUntil(() => {
|
||||||
|
const center = context.bot.entity.position.floored()
|
||||||
|
return Math.abs(center.x) < 2 && Math.abs(center.z) < 2
|
||||||
|
&& context.bot.blockAt(center.offset(-7 - center.x, 69 - center.y, 6 - center.z)) !== null
|
||||||
|
}, { label: 'Seeded trunk neighborhood', timeoutMs: 90000, intervalMs: 250 })
|
||||||
|
const center = context.bot.entity.position.floored()
|
||||||
|
const blocks = []
|
||||||
|
for (let y = 69; y <= 83; y++) {
|
||||||
|
const block = context.bot.blockAt(center.offset(-7 - center.x, y - center.y, 6 - center.z))
|
||||||
|
context.expect(isWood(block), 'Overlapping canopy interrupted the seeded main trunk',
|
||||||
|
{ pack, x: -7, y, z: 6, block: block?.name ?? null })
|
||||||
|
blocks.push({ y, block: block.name })
|
||||||
|
}
|
||||||
|
context.report.treeContinuity.push({ pack, x: -7, z: 6, blocks })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
await context.step('paired packs retain matching sampled tree geometry', async () => {
|
||||||
|
const geometry = context.report.treeGrounding.map(result => result.samples.map(tree => ({
|
||||||
|
base: tree.base, woodBlocks: tree.woodBlocks, height: tree.height, terrainSpread: tree.terrainSpread
|
||||||
|
})).sort((first, second) => positionKey(first.base).localeCompare(positionKey(second.base))))
|
||||||
|
context.expect(JSON.stringify(geometry[0]) === JSON.stringify(geometry[1]),
|
||||||
|
'Paired spindle packs produced different sampled geometry', { geometry })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -242,11 +242,11 @@ public class IrisProceduralTree implements IrisProceduralPlacement {
|
|||||||
private IrisTreeRootStyle rootStyle = IrisTreeRootStyle.BUTTRESS;
|
private IrisTreeRootStyle rootStyle = IrisTreeRootStyle.BUTTRESS;
|
||||||
|
|
||||||
@MinNumber(0)
|
@MinNumber(0)
|
||||||
@Description("Override the root depth/reach in blocks. 0 means auto (scales with tree height).")
|
@Description("Root depth in blocks, also used for ground-contact extension up to 16 blocks. 0 scales with tree height.")
|
||||||
private int rootDepth = 0;
|
private int rootDepth = 0;
|
||||||
|
|
||||||
@MinNumber(0)
|
@MinNumber(0)
|
||||||
@Description("Override the root flare radius in blocks. 0 means auto.")
|
@Description("Horizontal reach of buttress and stilt roots. 0 uses a compact radius based on trunk width. Taproots ignore this field.")
|
||||||
private double rootFlare = 0;
|
private double rootFlare = 0;
|
||||||
|
|
||||||
@MinNumber(1)
|
@MinNumber(1)
|
||||||
|
|||||||
+30
-9
@@ -40,6 +40,12 @@ public final class ProceduralTreeGenerator {
|
|||||||
long baseSeed = rng.getSeed();
|
long baseSeed = rng.getSeed();
|
||||||
|
|
||||||
List<TreeTrunkBuilder.Limb> limbs = TreeTrunkBuilder.build(canvas, tree, height);
|
List<TreeTrunkBuilder.Limb> limbs = TreeTrunkBuilder.build(canvas, tree, height);
|
||||||
|
Set<TreeBlockCanvas.Vec> supportPositions = new HashSet<>();
|
||||||
|
for (TreeBlockCanvas.Vec position : canvas.getTrunk()) {
|
||||||
|
if (position.y() == 0) {
|
||||||
|
supportPositions.add(position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
boolean wantEndpoints = tree.getDecorators() != null && !tree.getDecorators().isEmpty();
|
boolean wantEndpoints = tree.getDecorators() != null && !tree.getDecorators().isEmpty();
|
||||||
List<int[]> branchEndpoints = wantEndpoints ? new ArrayList<>() : null;
|
List<int[]> branchEndpoints = wantEndpoints ? new ArrayList<>() : null;
|
||||||
@@ -55,7 +61,13 @@ public final class ProceduralTreeGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (tree.isRoots()) {
|
if (tree.isRoots()) {
|
||||||
|
Set<TreeBlockCanvas.Vec> existingWood = new HashSet<>(canvas.getTrunk());
|
||||||
TreeRootBuilder.build(canvas, tree, height, baseSeed);
|
TreeRootBuilder.build(canvas, tree, height, baseSeed);
|
||||||
|
for (TreeBlockCanvas.Vec position : canvas.getTrunk()) {
|
||||||
|
if (position.y() <= 0 && !existingWood.contains(position)) {
|
||||||
|
supportPositions.add(position);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tree.isPlausible()) {
|
if (tree.isPlausible()) {
|
||||||
@@ -82,10 +94,14 @@ public final class ProceduralTreeGenerator {
|
|||||||
|
|
||||||
TreePlausibility.apply(resolved, trunkPositions, leafPositions, tree);
|
TreePlausibility.apply(resolved, trunkPositions, leafPositions, tree);
|
||||||
|
|
||||||
return assemble(resolved);
|
int rootReach = tree.isRoots() ? tree.getRootDepth() > 0 ? tree.getRootDepth()
|
||||||
|
: Math.max(2, (int) Math.round(height * 0.18)) : 0;
|
||||||
|
return assemble(resolved, supportPositions, leafPositions, rootReach);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IrisObject assemble(Map<TreeBlockCanvas.Vec, NativeBlockState> resolved) {
|
private static IrisObject assemble(Map<TreeBlockCanvas.Vec, NativeBlockState> resolved,
|
||||||
|
Set<TreeBlockCanvas.Vec> supportPositions,
|
||||||
|
Set<TreeBlockCanvas.Vec> leafPositions, int rootReach) {
|
||||||
if (resolved.isEmpty()) {
|
if (resolved.isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -105,21 +121,26 @@ public final class ProceduralTreeGenerator {
|
|||||||
maxZ = Math.max(maxZ, v.z());
|
maxZ = Math.max(maxZ, v.z());
|
||||||
}
|
}
|
||||||
|
|
||||||
int w = maxX - minX + 1;
|
int w = 2 * Math.max(Math.abs(minX), Math.abs(maxX)) + 1;
|
||||||
int h = maxY - minY + 1;
|
int h = maxY - minY + 1;
|
||||||
int d = maxZ - minZ + 1;
|
int d = 2 * Math.max(Math.abs(minZ), Math.abs(maxZ)) + 1;
|
||||||
int cx = w / 2;
|
|
||||||
int cy = h / 2;
|
int cy = h / 2;
|
||||||
int cz = d / 2;
|
|
||||||
|
|
||||||
IrisObject object = new IrisObject(w, h, d);
|
IrisObject object = new IrisObject(w, h, d);
|
||||||
|
List<TreeGrounding.Root> roots = new ArrayList<>(supportPositions.size());
|
||||||
|
Set<TreeBlockCanvas.Vec> canopy = new HashSet<>(leafPositions.size());
|
||||||
for (Map.Entry<TreeBlockCanvas.Vec, NativeBlockState> entry : resolved.entrySet()) {
|
for (Map.Entry<TreeBlockCanvas.Vec, NativeBlockState> entry : resolved.entrySet()) {
|
||||||
TreeBlockCanvas.Vec v = entry.getKey();
|
TreeBlockCanvas.Vec v = entry.getKey();
|
||||||
int nx = v.x() - minX - cx;
|
|
||||||
int ny = v.y() - cy + 1;
|
int ny = v.y() - cy + 1;
|
||||||
int nz = v.z() - minZ - cz;
|
object.getBlocks().put(new IrisBlockVector(v.x(), ny, v.z()), entry.getValue());
|
||||||
object.getBlocks().put(new IrisBlockVector(nx, ny, nz), entry.getValue());
|
if (supportPositions.contains(v)) {
|
||||||
|
roots.add(new TreeGrounding.Root(v.x(), ny, v.z(), entry.getValue()));
|
||||||
|
}
|
||||||
|
if (leafPositions.contains(v)) {
|
||||||
|
canopy.add(new TreeBlockCanvas.Vec(v.x(), ny, v.z()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
object.setTreeGrounding(TreeGrounding.create(roots, rootReach, canopy));
|
||||||
|
|
||||||
return object;
|
return object;
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -335,7 +335,9 @@ public final class TreeCanopyBuilder {
|
|||||||
int x = (int) Math.round(ox + dx * t);
|
int x = (int) Math.round(ox + dx * t);
|
||||||
int y = (int) Math.round(oy + dy * t - sagBlocks * t * t);
|
int y = (int) Math.round(oy + dy * t - sagBlocks * t * t);
|
||||||
int z = (int) Math.round(oz + dz * t);
|
int z = (int) Math.round(oz + dz * t);
|
||||||
if (!canvas.has(x, y, z)) {
|
TreeBlockCanvas.Cell existing = canvas.get(x, y, z);
|
||||||
|
if (existing == null || existing.role() == TreeBlockCanvas.Role.LEAF
|
||||||
|
|| existing.role() == TreeBlockCanvas.Role.SECONDARY_LEAF) {
|
||||||
canvas.setTrunk(x, y, z, TreeBlockCanvas.Role.TRUNK, axis);
|
canvas.setTrunk(x, y, z, TreeBlockCanvas.Role.TRUNK, axis);
|
||||||
}
|
}
|
||||||
tip = new int[]{x, y, z};
|
tip = new int[]{x, y, z};
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package art.arcane.iris.generation.decoration.tree;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.geometry.IrisBlockVector;
|
||||||
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.pack.loading.IrisData;
|
||||||
|
import art.arcane.iris.structure.object.IObjectPlacer;
|
||||||
|
import art.arcane.iris.structure.object.IrisObjectPlacement;
|
||||||
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||||
|
import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume;
|
||||||
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
|
import art.arcane.volmlib.util.math.BlockPosition;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public record TreeGrounding(List<Root> roots, int maxExtension, Set<TreeBlockCanvas.Vec> canopy) {
|
||||||
|
public TreeGrounding {
|
||||||
|
roots = List.copyOf(roots);
|
||||||
|
maxExtension = Math.max(0, Math.min(16, maxExtension));
|
||||||
|
canopy = Set.copyOf(canopy);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TreeGrounding create(List<Root> roots, int maxExtension, Set<TreeBlockCanvas.Vec> canopy) {
|
||||||
|
Map<Long, Root> columns = new HashMap<>();
|
||||||
|
for (Root root : roots) {
|
||||||
|
long key = ((long) root.x() << 32) | (root.z() & 0xffffffffL);
|
||||||
|
Root previous = columns.get(key);
|
||||||
|
if (previous == null || root.y() < previous.y()) {
|
||||||
|
columns.put(key, root);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<Root> lowest = new ArrayList<>(columns.values());
|
||||||
|
lowest.sort(Comparator.comparingInt(Root::x).thenComparingInt(Root::z));
|
||||||
|
return new TreeGrounding(lowest, maxExtension, canopy);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isCanopy(int x, int y, int z) {
|
||||||
|
return canopy.contains(new TreeBlockCanvas.Vec(x, y, z));
|
||||||
|
}
|
||||||
|
|
||||||
|
public KList<IrisBlockVector> supportOffsets() {
|
||||||
|
KList<IrisBlockVector> offsets = new KList<>();
|
||||||
|
for (Root root : roots) {
|
||||||
|
offsets.add(new IrisBlockVector(root.x(), root.y(), root.z()));
|
||||||
|
}
|
||||||
|
return offsets;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Plan plan(IObjectPlacer placer, Placement placement) {
|
||||||
|
List<Extension> extensions = new ArrayList<>();
|
||||||
|
IrisObjectPlacement config = placement.config();
|
||||||
|
BlockPosition origin = placement.origin();
|
||||||
|
BlockPosition spin = placement.spin();
|
||||||
|
int solidDepth = Math.max(1, config.getSurfaceSupportDepth());
|
||||||
|
for (Root root : roots) {
|
||||||
|
IrisBlockVector offset = config.getRotation().rotate(
|
||||||
|
new IrisBlockVector(root.x(), root.y(), root.z()), spin.getX(), spin.getY(), spin.getZ());
|
||||||
|
offset = config.getTranslate().translate(offset, config.getRotation(), spin.getX(), spin.getY(), spin.getZ());
|
||||||
|
int x = origin.getX() + (int) Math.round(offset.getX());
|
||||||
|
int y = origin.getY() + (int) Math.round(offset.getY());
|
||||||
|
int z = origin.getZ() + (int) Math.round(offset.getZ());
|
||||||
|
int surface = placer.getHighest(x, z, placement.data(), true);
|
||||||
|
int contact = Math.min(surface, y - 1);
|
||||||
|
if (y - contact - 1 > maxExtension) {
|
||||||
|
return Plan.UNSUPPORTED;
|
||||||
|
}
|
||||||
|
for (int below = 0; below < solidDepth; below++) {
|
||||||
|
if (placer.isCarved(x, contact - below, z) || !placer.isSurfaceSolid(x, contact - below, z)) {
|
||||||
|
return Plan.UNSUPPORTED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (contact < y - 1) {
|
||||||
|
NativeBlockState state = config.getRotation().rotate(root.state(), spin.getX(), spin.getY(), spin.getZ());
|
||||||
|
if (state == null) {
|
||||||
|
return Plan.UNSUPPORTED;
|
||||||
|
}
|
||||||
|
Extension extension = new Extension(x, y - 1, contact, z, state);
|
||||||
|
if (intersectsStructure(placer, extension)) {
|
||||||
|
return Plan.UNSUPPORTED;
|
||||||
|
}
|
||||||
|
extensions.add(extension);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Plan(!roots.isEmpty(), List.copyOf(extensions));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean intersectsStructure(IObjectPlacer placer, Extension extension) {
|
||||||
|
Engine engine = placer.getEngine();
|
||||||
|
if (engine == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
KList<NativeStructureVolume> volumes = engine.getNativeStructureVolumes(
|
||||||
|
extension.x(), extension.z(), extension.x(), extension.z());
|
||||||
|
if (volumes == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (NativeStructureVolume volume : volumes) {
|
||||||
|
if (volume.intersects(extension.x(), extension.bottomY() + engine.getMinHeight(), extension.z(),
|
||||||
|
extension.x(), extension.topY() + engine.getMinHeight(), extension.z())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Root(int x, int y, int z, NativeBlockState state) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Placement(IrisData data, IrisObjectPlacement config, BlockPosition origin, BlockPosition spin) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Extension(int x, int topY, int bottomY, int z, NativeBlockState state) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Plan(boolean supported, List<Extension> extensions) {
|
||||||
|
private static final Plan UNSUPPORTED = new Plan(false, List.of());
|
||||||
|
}
|
||||||
|
}
|
||||||
+104
-68
@@ -28,120 +28,152 @@ public final class TreeRootBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void build(TreeBlockCanvas canvas, IrisProceduralTree tree, int height, long baseSeed) {
|
public static void build(TreeBlockCanvas canvas, IrisProceduralTree tree, int height, long baseSeed) {
|
||||||
List<int[]> cells = new ArrayList<>();
|
List<TreeBlockCanvas.Vec> cells = new ArrayList<>();
|
||||||
for (TreeBlockCanvas.Vec v : canvas.getTrunk()) {
|
for (TreeBlockCanvas.Vec v : canvas.getTrunk()) {
|
||||||
if (v.y() == 0) {
|
if (v.y() == 0) {
|
||||||
cells.add(new int[]{v.x(), v.z()});
|
cells.add(v);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (cells.isEmpty()) {
|
if (cells.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
cells.sort(TreeBlockCanvas.Vec::compareTo);
|
||||||
|
|
||||||
int depth = tree.getRootDepth() > 0 ? tree.getRootDepth() : autoDepth(height);
|
int depth = tree.getRootDepth() > 0 ? tree.getRootDepth() : autoDepth(height);
|
||||||
RNG rng = new RNG(baseSeed ^ 0x5009L);
|
RNG rng = new RNG(baseSeed ^ 0x5009L);
|
||||||
|
|
||||||
double cx = 0;
|
double cx = 0;
|
||||||
double cz = 0;
|
double cz = 0;
|
||||||
for (int[] c : cells) {
|
for (TreeBlockCanvas.Vec cell : cells) {
|
||||||
cx += c[0];
|
cx += cell.x();
|
||||||
cz += c[1];
|
cz += cell.z();
|
||||||
}
|
}
|
||||||
cx /= cells.size();
|
cx /= cells.size();
|
||||||
cz /= cells.size();
|
cz /= cells.size();
|
||||||
|
|
||||||
double baseRadius = 1.0;
|
double baseRadius = 1.0;
|
||||||
for (int[] c : cells) {
|
for (TreeBlockCanvas.Vec cell : cells) {
|
||||||
baseRadius = Math.max(baseRadius, Math.hypot(c[0] - cx, c[1] - cz) + 0.5);
|
baseRadius = Math.max(baseRadius, Math.hypot(cell.x() - cx, cell.z() - cz) + 0.5);
|
||||||
}
|
}
|
||||||
double flare = tree.getRootFlare() > 0 ? tree.getRootFlare() : baseRadius + Math.max(2.0, depth * 0.6);
|
double flare = tree.getRootFlare() > 0 ? tree.getRootFlare() : baseRadius + Math.max(0.75, baseRadius * 0.35);
|
||||||
|
RootShape shape = new RootShape(cells, cx, cz, baseRadius, depth, flare);
|
||||||
|
|
||||||
IrisTreeRootStyle style = tree.getRootStyle();
|
IrisTreeRootStyle style = tree.getRootStyle();
|
||||||
switch (style) {
|
switch (style) {
|
||||||
case TAPROOT -> taproot(canvas, cells, cx, cz, baseRadius, depth);
|
case TAPROOT -> taproot(canvas, shape);
|
||||||
case BUTTRESS -> {
|
case BUTTRESS -> {
|
||||||
taproot(canvas, cells, cx, cz, baseRadius, depth);
|
taproot(canvas, shape);
|
||||||
buttress(canvas, cells, cx, cz, baseRadius, depth, flare, rng);
|
buttress(canvas, shape, rng);
|
||||||
}
|
}
|
||||||
case STILT -> {
|
case STILT -> {
|
||||||
taproot(canvas, cells, cx, cz, baseRadius, Math.min(depth, 3));
|
stilts(canvas, shape, height, rng);
|
||||||
stilts(canvas, cx, cz, baseRadius, flare, height, cells.size(), rng);
|
taproot(canvas, new RootShape(cells, cx, cz, baseRadius, Math.min(depth, 3), flare));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void taproot(TreeBlockCanvas canvas, List<int[]> cells, double cx, double cz, double baseRadius, int depth) {
|
private static void taproot(TreeBlockCanvas canvas, RootShape shape) {
|
||||||
int icx = (int) Math.round(cx);
|
TreeBlockCanvas.Vec center = nearestCell(shape.cells(), shape.centerX(), shape.centerZ());
|
||||||
int icz = (int) Math.round(cz);
|
for (int k = 1; k <= shape.depth(); k++) {
|
||||||
for (int k = 1; k <= depth; k++) {
|
double fraction = k / (double) shape.depth();
|
||||||
double frac = k / (double) depth;
|
double keepRadius = shape.radius() * (1.0 - fraction);
|
||||||
double keepR = baseRadius * (1.0 - 0.6 * frac);
|
for (TreeBlockCanvas.Vec cell : shape.cells()) {
|
||||||
for (int[] c : cells) {
|
if (Math.hypot(cell.x() - shape.centerX(), cell.z() - shape.centerZ()) <= keepRadius) {
|
||||||
if (Math.hypot(c[0] - cx, c[1] - cz) <= keepR + 1e-6) {
|
placeRoot(canvas, cell.x(), -k, cell.z(), TreeBlockCanvas.Axis.Y);
|
||||||
placeRoot(canvas, c[0], -k, c[1], TreeBlockCanvas.Axis.Y);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
placeRoot(canvas, icx, -k, icz, TreeBlockCanvas.Axis.Y);
|
placeRoot(canvas, center.x(), -k, center.z(), TreeBlockCanvas.Axis.Y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void buttress(TreeBlockCanvas canvas, List<int[]> cells, double cx, double cz, double baseRadius,
|
private static void buttress(TreeBlockCanvas canvas, RootShape shape, RNG rng) {
|
||||||
int depth, double flare, RNG rng) {
|
int legs = Math.max(3, Math.min(8, shape.cells().size() + 2));
|
||||||
int nLegs = Math.max(4, Math.min(12, cells.size() + 2));
|
for (int leg = 0; leg < legs; leg++) {
|
||||||
for (int li = 0; li < nLegs; li++) {
|
double angle = 2.0 * Math.PI * leg / legs + rng.d(-0.25, 0.25);
|
||||||
double ang = 2.0 * Math.PI * li / nLegs + rng.d(-0.25, 0.25);
|
double reach = shape.flare() * rng.d(0.8, 1.0);
|
||||||
double ca = Math.cos(ang);
|
int x = (int) Math.round(shape.centerX() + reach * Math.cos(angle));
|
||||||
double sa = Math.sin(ang);
|
int z = (int) Math.round(shape.centerZ() + reach * Math.sin(angle));
|
||||||
int prevX = (int) Math.round(cx + baseRadius * ca);
|
int depth = legDepth(shape.depth(), leg, legs);
|
||||||
int prevZ = (int) Math.round(cz + baseRadius * sa);
|
TreeBlockCanvas.Vec start = nearestCell(shape.cells(), x, z);
|
||||||
for (int k = 1; k <= depth; k++) {
|
rasterizeRoot(canvas, start, new TreeBlockCanvas.Vec(x, -depth, z));
|
||||||
double frac = k / (double) depth;
|
}
|
||||||
double r = baseRadius + (flare - baseRadius) * frac;
|
}
|
||||||
int x = (int) Math.round(cx + r * ca);
|
|
||||||
int z = (int) Math.round(cz + r * sa);
|
private static void stilts(TreeBlockCanvas canvas, RootShape shape, int height, RNG rng) {
|
||||||
placeRoot(canvas, x, -k, z, TreeBlockCanvas.Axis.Y);
|
int stiltTop = Math.min(height - 1, Math.max(2, Math.min(7, (int) Math.round(0.25 * height))));
|
||||||
if (x != prevX || z != prevZ) {
|
List<TreeBlockCanvas.Vec> attachments = new ArrayList<>();
|
||||||
placeRoot(canvas, prevX, -k, prevZ, TreeBlockCanvas.Axis.Y);
|
for (TreeBlockCanvas.Vec cell : canvas.getTrunk()) {
|
||||||
}
|
if (cell.y() == stiltTop) {
|
||||||
prevX = x;
|
attachments.add(cell);
|
||||||
prevZ = z;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if (attachments.isEmpty()) {
|
||||||
|
attachments.addAll(shape.cells());
|
||||||
private static void stilts(TreeBlockCanvas canvas, double cx, double cz, double baseRadius, double flare,
|
}
|
||||||
int height, int cellCount, RNG rng) {
|
attachments.sort(TreeBlockCanvas.Vec::compareTo);
|
||||||
int stiltTop = Math.max(2, Math.min(7, (int) Math.round(0.25 * height)));
|
int props = Math.max(3, Math.min(8, shape.cells().size() + 2));
|
||||||
int nProps = Math.max(4, Math.min(10, cellCount + 2));
|
for (int prop = 0; prop < props; prop++) {
|
||||||
for (int p = 0; p < nProps; p++) {
|
double angle = 2.0 * Math.PI * prop / props + rng.d(-0.25, 0.25);
|
||||||
double ang = 2.0 * Math.PI * p / nProps + rng.d(-0.25, 0.25);
|
double reach = shape.flare() * rng.d(0.8, 1.0);
|
||||||
double ca = Math.cos(ang);
|
int x = (int) Math.round(shape.centerX() + reach * Math.cos(angle));
|
||||||
double sa = Math.sin(ang);
|
int z = (int) Math.round(shape.centerZ() + reach * Math.sin(angle));
|
||||||
int sx = (int) Math.round(cx + baseRadius * ca);
|
int depth = legDepth(Math.min(shape.depth(), 3), prop, props);
|
||||||
int sz = (int) Math.round(cz + baseRadius * sa);
|
TreeBlockCanvas.Vec start = nearestCell(attachments, x, z);
|
||||||
int fx = (int) Math.round(cx + flare * ca);
|
rasterizeRoot(canvas, start, new TreeBlockCanvas.Vec(x, -depth, z));
|
||||||
int fz = (int) Math.round(cz + flare * sa);
|
|
||||||
rasterizeRoot(canvas, sx, stiltTop, sz, fx, -2, fz);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void rasterizeRoot(TreeBlockCanvas canvas, int ox, int oy, int oz, int ex, int ey, int ez) {
|
private static int legDepth(int depth, int leg, int count) {
|
||||||
int dx = ex - ox;
|
return Math.max(1, (int) Math.round(depth * (0.55 + 0.45 * leg / Math.max(1, count - 1))));
|
||||||
int dy = ey - oy;
|
}
|
||||||
int dz = ez - oz;
|
|
||||||
|
private static TreeBlockCanvas.Vec nearestCell(List<TreeBlockCanvas.Vec> cells, double x, double z) {
|
||||||
|
TreeBlockCanvas.Vec nearest = cells.getFirst();
|
||||||
|
double distance = Double.POSITIVE_INFINITY;
|
||||||
|
for (TreeBlockCanvas.Vec cell : cells) {
|
||||||
|
double dx = cell.x() - x;
|
||||||
|
double dz = cell.z() - z;
|
||||||
|
double candidateDistance = dx * dx + dz * dz;
|
||||||
|
if (candidateDistance < distance) {
|
||||||
|
nearest = cell;
|
||||||
|
distance = candidateDistance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nearest;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void rasterizeRoot(TreeBlockCanvas canvas, TreeBlockCanvas.Vec start, TreeBlockCanvas.Vec end) {
|
||||||
|
int dx = end.x() - start.x();
|
||||||
|
int dy = end.y() - start.y();
|
||||||
|
int dz = end.z() - start.z();
|
||||||
int steps = Math.max(Math.max(Math.abs(dx), Math.abs(dy)), Math.max(Math.abs(dz), 1));
|
int steps = Math.max(Math.max(Math.abs(dx), Math.abs(dy)), Math.max(Math.abs(dz), 1));
|
||||||
TreeBlockCanvas.Axis axis = TreeFunctions.logAxis(dx, dy, dz);
|
TreeBlockCanvas.Axis axis = TreeFunctions.logAxis(dx, dy, dz);
|
||||||
for (int i = 0; i <= steps; i++) {
|
int x = start.x();
|
||||||
|
int y = start.y();
|
||||||
|
int z = start.z();
|
||||||
|
for (int i = 1; i <= steps; i++) {
|
||||||
double t = i / (double) steps;
|
double t = i / (double) steps;
|
||||||
int x = (int) Math.round(ox + dx * t);
|
int nextX = (int) Math.round(start.x() + dx * t);
|
||||||
int y = (int) Math.round(oy + dy * t);
|
int nextY = (int) Math.round(start.y() + dy * t);
|
||||||
int z = (int) Math.round(oz + dz * t);
|
int nextZ = (int) Math.round(start.z() + dz * t);
|
||||||
placeRoot(canvas, x, y, z, axis);
|
while (y != nextY) {
|
||||||
|
y += Integer.signum(nextY - y);
|
||||||
|
placeRoot(canvas, x, y, z, axis);
|
||||||
|
}
|
||||||
|
while (x != nextX) {
|
||||||
|
x += Integer.signum(nextX - x);
|
||||||
|
placeRoot(canvas, x, y, z, axis);
|
||||||
|
}
|
||||||
|
while (z != nextZ) {
|
||||||
|
z += Integer.signum(nextZ - z);
|
||||||
|
placeRoot(canvas, x, y, z, axis);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void placeRoot(TreeBlockCanvas canvas, int x, int y, int z, TreeBlockCanvas.Axis axis) {
|
private static void placeRoot(TreeBlockCanvas canvas, int x, int y, int z, TreeBlockCanvas.Axis axis) {
|
||||||
if (!canvas.has(x, y, z)) {
|
TreeBlockCanvas.Cell cell = canvas.get(x, y, z);
|
||||||
|
if (cell == null || cell.role() != TreeBlockCanvas.Role.TRUNK && cell.role() != TreeBlockCanvas.Role.SECONDARY_TRUNK) {
|
||||||
canvas.setTrunk(x, y, z, TreeBlockCanvas.Role.TRUNK, axis);
|
canvas.setTrunk(x, y, z, TreeBlockCanvas.Role.TRUNK, axis);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,4 +181,8 @@ public final class TreeRootBuilder {
|
|||||||
private static int autoDepth(int height) {
|
private static int autoDepth(int height) {
|
||||||
return Math.max(2, Math.min(16, (int) Math.round(0.18 * height)));
|
return Math.max(2, Math.min(16, (int) Math.round(0.18 * height)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record RootShape(List<TreeBlockCanvas.Vec> cells, double centerX, double centerZ,
|
||||||
|
double radius, int depth, double flare) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+55
-9
@@ -28,6 +28,7 @@ import art.arcane.iris.structure.object.IObjectPlacer;
|
|||||||
import art.arcane.iris.generation.block.TileData;
|
import art.arcane.iris.generation.block.TileData;
|
||||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
|
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
|
||||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||||
|
import art.arcane.volmlib.util.math.BlockPosition;
|
||||||
import art.arcane.iris.generation.block.B;
|
import art.arcane.iris.generation.block.B;
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
|
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
|
||||||
@@ -49,6 +50,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
|||||||
private final Long2IntOpenHashMap surfaceHeights;
|
private final Long2IntOpenHashMap surfaceHeights;
|
||||||
private final Long2IntOpenHashMap caveCeilings;
|
private final Long2IntOpenHashMap caveCeilings;
|
||||||
private int blockWrites;
|
private int blockWrites;
|
||||||
|
private BlockPosition canopyPosition;
|
||||||
|
|
||||||
CaveObjectPlacementTransaction(IObjectPlacer delegate, int anchorY, int minDepthBelowSurface) {
|
CaveObjectPlacementTransaction(IObjectPlacer delegate, int anchorY, int minDepthBelowSurface) {
|
||||||
this.delegate = delegate;
|
this.delegate = delegate;
|
||||||
@@ -95,13 +97,36 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (BufferedMutation mutation : mutations) {
|
applyMutations();
|
||||||
mutation.apply(delegate);
|
|
||||||
}
|
|
||||||
discard();
|
discard();
|
||||||
return CommitResult.COMMITTED;
|
return CommitResult.COMMITTED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void applyMutations() {
|
||||||
|
boolean canopyOpen = false;
|
||||||
|
boolean skipCanopy = false;
|
||||||
|
try {
|
||||||
|
for (BufferedMutation mutation : mutations) {
|
||||||
|
if (mutation.value() instanceof NativeBlockState) {
|
||||||
|
if (canopyOpen) {
|
||||||
|
delegate.endTreeCanopy();
|
||||||
|
canopyOpen = false;
|
||||||
|
}
|
||||||
|
skipCanopy = mutation.canopy()
|
||||||
|
&& !delegate.beginTreeCanopy(new BlockPosition(mutation.x(), mutation.y(), mutation.z()));
|
||||||
|
canopyOpen = mutation.canopy() && !skipCanopy;
|
||||||
|
}
|
||||||
|
if (!mutation.canopy() || !skipCanopy) {
|
||||||
|
mutation.apply(delegate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (canopyOpen) {
|
||||||
|
delegate.endTreeCanopy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void discard() {
|
void discard() {
|
||||||
mutations.clear();
|
mutations.clear();
|
||||||
bufferedBlocks.clear();
|
bufferedBlocks.clear();
|
||||||
@@ -139,11 +164,30 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
|||||||
if (state == null) {
|
if (state == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
mutations.add(new BlockMutation(x, y, z, state));
|
mutations.add(new BlockMutation(x, y, z, state, isCanopy(x, y, z)));
|
||||||
bufferedBlocks.put(new PositionKey(x, y, z), state);
|
bufferedBlocks.put(new PositionKey(x, y, z), state);
|
||||||
blockWrites++;
|
blockWrites++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean beginTreeCanopy(BlockPosition position) {
|
||||||
|
if (!IObjectPlacer.super.beginTreeCanopy(position)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
canopyPosition = position;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void endTreeCanopy() {
|
||||||
|
canopyPosition = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isCanopy(int x, int y, int z) {
|
||||||
|
return canopyPosition != null && canopyPosition.getX() == x
|
||||||
|
&& canopyPosition.getY() == y && canopyPosition.getZ() == z;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public NativeBlockState get(int x, int y, int z) {
|
public NativeBlockState get(int x, int y, int z) {
|
||||||
NativeBlockState buffered = bufferedBlocks.get(new PositionKey(x, y, z));
|
NativeBlockState buffered = bufferedBlocks.get(new PositionKey(x, y, z));
|
||||||
@@ -198,7 +242,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
|||||||
@Override
|
@Override
|
||||||
public void setTile(int x, int y, int z, TileData tile) {
|
public void setTile(int x, int y, int z, TileData tile) {
|
||||||
if (tile != null) {
|
if (tile != null) {
|
||||||
mutations.add(new TileMutation(x, y, z, tile));
|
mutations.add(new TileMutation(x, y, z, tile, isCanopy(x, y, z)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,7 +251,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
|||||||
if (data == null) {
|
if (data == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
mutations.add(new DataMutation(x, y, z, data));
|
mutations.add(new DataMutation(x, y, z, data, isCanopy(x, y, z)));
|
||||||
if (data instanceof NativeBlockState state) {
|
if (data instanceof NativeBlockState state) {
|
||||||
bufferedBlocks.put(new PositionKey(x, y, z), state);
|
bufferedBlocks.put(new PositionKey(x, y, z), state);
|
||||||
blockWrites++;
|
blockWrites++;
|
||||||
@@ -291,24 +335,26 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
|||||||
|
|
||||||
Object value();
|
Object value();
|
||||||
|
|
||||||
|
boolean canopy();
|
||||||
|
|
||||||
void apply(IObjectPlacer placer);
|
void apply(IObjectPlacer placer);
|
||||||
}
|
}
|
||||||
|
|
||||||
private record BlockMutation(int x, int y, int z, NativeBlockState value) implements BufferedMutation {
|
private record BlockMutation(int x, int y, int z, NativeBlockState value, boolean canopy) implements BufferedMutation {
|
||||||
@Override
|
@Override
|
||||||
public void apply(IObjectPlacer placer) {
|
public void apply(IObjectPlacer placer) {
|
||||||
placer.set(x, y, z, value);
|
placer.set(x, y, z, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private record TileMutation(int x, int y, int z, TileData value) implements BufferedMutation {
|
private record TileMutation(int x, int y, int z, TileData value, boolean canopy) implements BufferedMutation {
|
||||||
@Override
|
@Override
|
||||||
public void apply(IObjectPlacer placer) {
|
public void apply(IObjectPlacer placer) {
|
||||||
placer.setTile(x, y, z, value);
|
placer.setTile(x, y, z, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private record DataMutation(int x, int y, int z, Object value) implements BufferedMutation {
|
private record DataMutation(int x, int y, int z, Object value, boolean canopy) implements BufferedMutation {
|
||||||
@Override
|
@Override
|
||||||
public void apply(IObjectPlacer placer) {
|
public void apply(IObjectPlacer placer) {
|
||||||
placer.setData(x, y, z, value);
|
placer.setData(x, y, z, value);
|
||||||
|
|||||||
+63
-9
@@ -24,6 +24,7 @@ import art.arcane.iris.generation.runtime.IrisComplex;
|
|||||||
import art.arcane.iris.structure.object.IObjectPlacer;
|
import art.arcane.iris.structure.object.IObjectPlacer;
|
||||||
import art.arcane.iris.generation.block.TileData;
|
import art.arcane.iris.generation.block.TileData;
|
||||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||||
|
import art.arcane.volmlib.util.math.BlockPosition;
|
||||||
import art.arcane.iris.generation.block.B;
|
import art.arcane.iris.generation.block.B;
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
@@ -43,6 +44,7 @@ final class FloatingObjectPlacementTransaction implements IObjectPlacer {
|
|||||||
private final KList<BufferedMutation> mutations;
|
private final KList<BufferedMutation> mutations;
|
||||||
private final Map<PositionKey, NativeBlockState> bufferedBlocks;
|
private final Map<PositionKey, NativeBlockState> bufferedBlocks;
|
||||||
private int blockWrites;
|
private int blockWrites;
|
||||||
|
private BlockPosition canopyPosition;
|
||||||
|
|
||||||
FloatingObjectPlacementTransaction(IObjectPlacer delegate, ObjectContinuationBundle.PlacementKey placementKey) {
|
FloatingObjectPlacementTransaction(IObjectPlacer delegate, ObjectContinuationBundle.PlacementKey placementKey) {
|
||||||
this.delegate = Objects.requireNonNull(delegate);
|
this.delegate = Objects.requireNonNull(delegate);
|
||||||
@@ -70,9 +72,7 @@ final class FloatingObjectPlacementTransaction implements IObjectPlacer {
|
|||||||
return CommitResult.REJECTED_SUPPORT;
|
return CommitResult.REJECTED_SUPPORT;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (BufferedMutation mutation : mutations) {
|
applyMutations();
|
||||||
mutation.apply(delegate);
|
|
||||||
}
|
|
||||||
persistContinuation(engine);
|
persistContinuation(engine);
|
||||||
discard();
|
discard();
|
||||||
return CommitResult.COMMITTED;
|
return CommitResult.COMMITTED;
|
||||||
@@ -94,9 +94,17 @@ final class FloatingObjectPlacementTransaction implements IObjectPlacer {
|
|||||||
}
|
}
|
||||||
IrisMatterSupport.ensureRegistered();
|
IrisMatterSupport.ensureRegistered();
|
||||||
LinkedHashMap<ObjectContinuationBundle.ChunkPosition, Matter> payloads = new LinkedHashMap<>();
|
LinkedHashMap<ObjectContinuationBundle.ChunkPosition, Matter> payloads = new LinkedHashMap<>();
|
||||||
|
boolean skipCanopy = false;
|
||||||
for (BufferedMutation mutation : mutations) {
|
for (BufferedMutation mutation : mutations) {
|
||||||
ObjectContinuationBundle.ChunkPosition chunk = new ObjectContinuationBundle.ChunkPosition(mutation.x() >> 4, mutation.z() >> 4);
|
ObjectContinuationBundle.ChunkPosition chunk = new ObjectContinuationBundle.ChunkPosition(mutation.x() >> 4, mutation.z() >> 4);
|
||||||
Matter payload = payloads.computeIfAbsent(chunk, ignored -> new IrisMatter(16, engine.getHeight(), 16));
|
Matter payload = payloads.computeIfAbsent(chunk, ignored -> new IrisMatter(16, engine.getHeight(), 16));
|
||||||
|
if (mutation.value() instanceof NativeBlockState) {
|
||||||
|
skipCanopy = !ObjectContinuationPersistence.beginBlock(payload, mutation.x() & 15,
|
||||||
|
mutation.y(), mutation.z() & 15, mutation.canopy());
|
||||||
|
}
|
||||||
|
if (mutation.canopy() && skipCanopy) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
ObjectContinuationPersistence.put(payload, mutation.x() & 15, mutation.y(), mutation.z() & 15, mutation.value());
|
ObjectContinuationPersistence.put(payload, mutation.x() & 15, mutation.y(), mutation.z() & 15, mutation.value());
|
||||||
}
|
}
|
||||||
LinkedHashMap<ObjectContinuationBundle.ChunkPosition, Supplier<Matter>> destinations = new LinkedHashMap<>();
|
LinkedHashMap<ObjectContinuationBundle.ChunkPosition, Supplier<Matter>> destinations = new LinkedHashMap<>();
|
||||||
@@ -105,6 +113,31 @@ final class FloatingObjectPlacementTransaction implements IObjectPlacer {
|
|||||||
new ObjectContinuationBundle.Bounds(minimumX, minimumZ, maximumX, maximumZ), destinations);
|
new ObjectContinuationBundle.Bounds(minimumX, minimumZ, maximumX, maximumZ), destinations);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void applyMutations() {
|
||||||
|
boolean canopyOpen = false;
|
||||||
|
boolean skipCanopy = false;
|
||||||
|
try {
|
||||||
|
for (BufferedMutation mutation : mutations) {
|
||||||
|
if (mutation.value() instanceof NativeBlockState) {
|
||||||
|
if (canopyOpen) {
|
||||||
|
delegate.endTreeCanopy();
|
||||||
|
canopyOpen = false;
|
||||||
|
}
|
||||||
|
skipCanopy = mutation.canopy()
|
||||||
|
&& !delegate.beginTreeCanopy(new BlockPosition(mutation.x(), mutation.y(), mutation.z()));
|
||||||
|
canopyOpen = mutation.canopy() && !skipCanopy;
|
||||||
|
}
|
||||||
|
if (!mutation.canopy() || !skipCanopy) {
|
||||||
|
mutation.apply(delegate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (canopyOpen) {
|
||||||
|
delegate.endTreeCanopy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void discard() {
|
void discard() {
|
||||||
mutations.clear();
|
mutations.clear();
|
||||||
bufferedBlocks.clear();
|
bufferedBlocks.clear();
|
||||||
@@ -126,11 +159,30 @@ final class FloatingObjectPlacementTransaction implements IObjectPlacer {
|
|||||||
if (state == null) {
|
if (state == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
mutations.add(new BlockMutation(x, y, z, state));
|
mutations.add(new BlockMutation(x, y, z, state, isCanopy(x, y, z)));
|
||||||
bufferedBlocks.put(new PositionKey(x, y, z), state);
|
bufferedBlocks.put(new PositionKey(x, y, z), state);
|
||||||
blockWrites++;
|
blockWrites++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean beginTreeCanopy(BlockPosition position) {
|
||||||
|
if (!IObjectPlacer.super.beginTreeCanopy(position)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
canopyPosition = position;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void endTreeCanopy() {
|
||||||
|
canopyPosition = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isCanopy(int x, int y, int z) {
|
||||||
|
return canopyPosition != null && canopyPosition.getX() == x
|
||||||
|
&& canopyPosition.getY() == y && canopyPosition.getZ() == z;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public NativeBlockState get(int x, int y, int z) {
|
public NativeBlockState get(int x, int y, int z) {
|
||||||
NativeBlockState state = bufferedBlocks.get(new PositionKey(x, y, z));
|
NativeBlockState state = bufferedBlocks.get(new PositionKey(x, y, z));
|
||||||
@@ -176,7 +228,7 @@ final class FloatingObjectPlacementTransaction implements IObjectPlacer {
|
|||||||
@Override
|
@Override
|
||||||
public void setTile(int x, int y, int z, TileData tile) {
|
public void setTile(int x, int y, int z, TileData tile) {
|
||||||
if (tile != null) {
|
if (tile != null) {
|
||||||
mutations.add(new TileMutation(x, y, z, tile));
|
mutations.add(new TileMutation(x, y, z, tile, isCanopy(x, y, z)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +237,7 @@ final class FloatingObjectPlacementTransaction implements IObjectPlacer {
|
|||||||
if (data == null) {
|
if (data == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
mutations.add(new DataMutation(x, y, z, data));
|
mutations.add(new DataMutation(x, y, z, data, isCanopy(x, y, z)));
|
||||||
if (data instanceof NativeBlockState state) {
|
if (data instanceof NativeBlockState state) {
|
||||||
bufferedBlocks.put(new PositionKey(x, y, z), state);
|
bufferedBlocks.put(new PositionKey(x, y, z), state);
|
||||||
blockWrites++;
|
blockWrites++;
|
||||||
@@ -228,24 +280,26 @@ final class FloatingObjectPlacementTransaction implements IObjectPlacer {
|
|||||||
|
|
||||||
Object value();
|
Object value();
|
||||||
|
|
||||||
|
boolean canopy();
|
||||||
|
|
||||||
void apply(IObjectPlacer placer);
|
void apply(IObjectPlacer placer);
|
||||||
}
|
}
|
||||||
|
|
||||||
private record BlockMutation(int x, int y, int z, NativeBlockState value) implements BufferedMutation {
|
private record BlockMutation(int x, int y, int z, NativeBlockState value, boolean canopy) implements BufferedMutation {
|
||||||
@Override
|
@Override
|
||||||
public void apply(IObjectPlacer placer) {
|
public void apply(IObjectPlacer placer) {
|
||||||
placer.set(x, y, z, value);
|
placer.set(x, y, z, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private record TileMutation(int x, int y, int z, TileData value) implements BufferedMutation {
|
private record TileMutation(int x, int y, int z, TileData value, boolean canopy) implements BufferedMutation {
|
||||||
@Override
|
@Override
|
||||||
public void apply(IObjectPlacer placer) {
|
public void apply(IObjectPlacer placer) {
|
||||||
placer.setTile(x, y, z, value);
|
placer.setTile(x, y, z, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private record DataMutation(int x, int y, int z, Object value) implements BufferedMutation {
|
private record DataMutation(int x, int y, int z, Object value, boolean canopy) implements BufferedMutation {
|
||||||
@Override
|
@Override
|
||||||
public void apply(IObjectPlacer placer) {
|
public void apply(IObjectPlacer placer) {
|
||||||
placer.setData(x, y, z, value);
|
placer.setData(x, y, z, value);
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import art.arcane.iris.structure.object.IObjectPlacer;
|
|||||||
import art.arcane.iris.generation.block.TileData;
|
import art.arcane.iris.generation.block.TileData;
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||||
|
import art.arcane.volmlib.util.math.BlockPosition;
|
||||||
|
|
||||||
final class GoldenDebugObjectPlacer implements IObjectPlacer {
|
final class GoldenDebugObjectPlacer implements IObjectPlacer {
|
||||||
private static final int[] GOLDEN_DEBUG_TARGET = parseGoldenDebugTarget(resolveGoldenDebugSpec());
|
private static final int[] GOLDEN_DEBUG_TARGET = parseGoldenDebugTarget(resolveGoldenDebugSpec());
|
||||||
@@ -93,6 +94,16 @@ final class GoldenDebugObjectPlacer implements IObjectPlacer {
|
|||||||
delegate.set(x, y, z, d);
|
delegate.set(x, y, z, d);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean beginTreeCanopy(BlockPosition position) {
|
||||||
|
return delegate.beginTreeCanopy(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void endTreeCanopy() {
|
||||||
|
delegate.endTreeCanopy();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public NativeBlockState get(int x, int y, int z) {
|
public NativeBlockState get(int x, int y, int z) {
|
||||||
return delegate.get(x, y, z);
|
return delegate.get(x, y, z);
|
||||||
|
|||||||
+92
@@ -1,9 +1,11 @@
|
|||||||
package art.arcane.iris.generation.mantle;
|
package art.arcane.iris.generation.mantle;
|
||||||
|
|
||||||
import art.arcane.iris.generation.block.TileData;
|
import art.arcane.iris.generation.block.TileData;
|
||||||
|
import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial;
|
||||||
import art.arcane.iris.integration.Identifier;
|
import art.arcane.iris.integration.Identifier;
|
||||||
import art.arcane.iris.world.storage.matter.TileWrapper;
|
import art.arcane.iris.world.storage.matter.TileWrapper;
|
||||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||||
|
import art.arcane.volmlib.util.hunk.Hunk;
|
||||||
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||||
@@ -12,6 +14,7 @@ import art.arcane.volmlib.util.matter.MatterSlice;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.BitSet;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
public final class ObjectContinuationPersistence {
|
public final class ObjectContinuationPersistence {
|
||||||
@@ -68,6 +71,95 @@ public final class ObjectContinuationPersistence {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean beginBlock(Matter payload, int x, int y, int z, boolean canopy) {
|
||||||
|
if (canopy) {
|
||||||
|
MatterSlice<NativeBlockState> blocks = payload.getSlice(NativeBlockState.class);
|
||||||
|
NativeBlockState existing = blocks == null ? null : blocks.get(x, y, z);
|
||||||
|
if (existing != null && existing.isSolid()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
payload.slice(TreeCanopy.class).set(x, y, z, TreeCanopy.INSTANCE);
|
||||||
|
} else {
|
||||||
|
MatterSlice<TreeCanopy> conditions = payload.getSlice(TreeCanopy.class);
|
||||||
|
if (conditions != null) {
|
||||||
|
conditions.set(x, y, z, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void restore(Mantle<Matter> mantle, int chunkX, int chunkZ, Matter continuation,
|
||||||
|
Hunk<NativeBlockState> blocks, boolean applyBlocks) {
|
||||||
|
BitSet blocked = blockedCanopy(continuation, blocks, applyBlocks);
|
||||||
|
MatterSlice<NativeBlockState> states = continuation.getSlice(NativeBlockState.class);
|
||||||
|
MantleChunk<Matter> chunk = mantle.useChunk(chunkX, chunkZ);
|
||||||
|
try {
|
||||||
|
synchronized (chunk) {
|
||||||
|
if (states != null) {
|
||||||
|
states.iterateSync((x, y, z, state) -> {
|
||||||
|
if (blocked.get(cellIndex(x, y, z))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Matter section = chunk.get(y >> 4);
|
||||||
|
if (section != null) {
|
||||||
|
clearMetadata(section, x, y & 15, z);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (Map.Entry<Class<?>, MatterSlice<?>> entry : continuation.getSliceMap().entrySet()) {
|
||||||
|
if (entry.getKey() == TreeCanopy.class) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
entry.getValue().iterateSync((x, y, z, value) -> {
|
||||||
|
if (!blocked.get(cellIndex(x, y, z))) {
|
||||||
|
chunk.getOrCreate(y >> 4).slice(entry.getKey()).set(x, y & 15, z, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
chunk.release();
|
||||||
|
}
|
||||||
|
if (applyBlocks && states != null) {
|
||||||
|
states.iterateSync((x, y, z, state) -> {
|
||||||
|
if (!blocked.get(cellIndex(x, y, z))) {
|
||||||
|
blocks.set(x, y, z, state);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BitSet blockedCanopy(Matter continuation, Hunk<NativeBlockState> blocks, boolean applyBlocks) {
|
||||||
|
BitSet blocked = new BitSet();
|
||||||
|
MatterSlice<TreeCanopy> conditions = continuation.getSlice(TreeCanopy.class);
|
||||||
|
if (conditions != null) {
|
||||||
|
conditions.iterateSync((x, y, z, condition) -> {
|
||||||
|
NativeBlockState existing = blocks.get(x, y, z);
|
||||||
|
if (!applyBlocks || existing != null && existing.isSolid()) {
|
||||||
|
blocked.set(cellIndex(x, y, z));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return blocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int cellIndex(int x, int y, int z) {
|
||||||
|
return (y << 8) | (z << 4) | x;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void clearMetadata(Matter section, int x, int y, int z) {
|
||||||
|
clearMetadata(section.getSlice(Identifier.class), x, y, z);
|
||||||
|
clearMetadata(section.getSlice(String.class), x, y, z);
|
||||||
|
clearMetadata(section.getSlice(TreeBlockMaterial.class), x, y, z);
|
||||||
|
clearMetadata(section.getSlice(TileWrapper.class), x, y, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void clearMetadata(MatterSlice<?> slice, int x, int y, int z) {
|
||||||
|
if (slice != null) {
|
||||||
|
slice.set(x, y, z, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static boolean contains(ObjectContinuationBundle bundle, ObjectContinuationBundle.PlacementKey key) {
|
private static boolean contains(ObjectContinuationBundle bundle, ObjectContinuationBundle.PlacementKey key) {
|
||||||
if (bundle == null) {
|
if (bundle == null) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
+35
-4
@@ -10,6 +10,7 @@ import art.arcane.iris.generation.subterrain.SubterrainRasterizer;
|
|||||||
import art.arcane.iris.generation.block.TileData;
|
import art.arcane.iris.generation.block.TileData;
|
||||||
import art.arcane.iris.structure.object.IObjectPlacer;
|
import art.arcane.iris.structure.object.IObjectPlacer;
|
||||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||||
|
import art.arcane.volmlib.util.math.BlockPosition;
|
||||||
import art.arcane.iris.generation.block.B;
|
import art.arcane.iris.generation.block.B;
|
||||||
import art.arcane.iris.world.storage.matter.TileWrapper;
|
import art.arcane.iris.world.storage.matter.TileWrapper;
|
||||||
import art.arcane.volmlib.util.matter.MatterCavern;
|
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||||
@@ -34,6 +35,7 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
|||||||
private final List<Mutation> mutations;
|
private final List<Mutation> mutations;
|
||||||
private final List<PlacementRange> placements = new ArrayList<>();
|
private final List<PlacementRange> placements = new ArrayList<>();
|
||||||
private final Long2ObjectOpenHashMap<Int2ObjectOpenHashMap<OverlayCell>> overlay;
|
private final Long2ObjectOpenHashMap<Int2ObjectOpenHashMap<OverlayCell>> overlay;
|
||||||
|
private BlockPosition canopyPosition;
|
||||||
private boolean hydrologyGateKnown;
|
private boolean hydrologyGateKnown;
|
||||||
private int hydrologyGateChunkX;
|
private int hydrologyGateChunkX;
|
||||||
private int hydrologyGateChunkZ;
|
private int hydrologyGateChunkZ;
|
||||||
@@ -82,7 +84,15 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void apply(ObjectSourcePlan plan) {
|
void apply(ObjectSourcePlan plan) {
|
||||||
|
boolean skipCanopy = false;
|
||||||
for (Mutation mutation : plan.mutationsFor(destinationChunkX, destinationChunkZ)) {
|
for (Mutation mutation : plan.mutationsFor(destinationChunkX, destinationChunkZ)) {
|
||||||
|
if (mutation.key().type() == NativeBlockState.class) {
|
||||||
|
NativeBlockState existing = mutation.canopy() ? get(mutation.key().x(), mutation.key().y(), mutation.key().z()) : null;
|
||||||
|
skipCanopy = existing != null && existing.isSolid();
|
||||||
|
}
|
||||||
|
if (mutation.canopy() && skipCanopy) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
switch (mutation) {
|
switch (mutation) {
|
||||||
case SetMutation set -> setData(set.key().x(), set.key().y(), set.key().z(), set.value(), set);
|
case SetMutation set -> setData(set.key().x(), set.key().y(), set.key().z(), set.value(), set);
|
||||||
case CustomBlockMutation custom -> set(custom.key().x(), custom.key().y(), custom.key().z(), custom.state(), custom);
|
case CustomBlockMutation custom -> set(custom.key().x(), custom.key().y(), custom.key().z(), custom.state(), custom);
|
||||||
@@ -158,12 +168,31 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
|||||||
cell.tile = CLEARED;
|
cell.tile = CLEARED;
|
||||||
cell.identifier = identifier;
|
cell.identifier = identifier;
|
||||||
mutations.add(replayed != null
|
mutations.add(replayed != null
|
||||||
? replayed : new CustomBlockMutation(new DataKey(x, y, z, NativeBlockState.class), state));
|
? replayed : new CustomBlockMutation(new DataKey(x, y, z, NativeBlockState.class), state, isCanopy(x, y, z)));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setData(x, y, z, state);
|
setData(x, y, z, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean beginTreeCanopy(BlockPosition position) {
|
||||||
|
if (!ObjectPassPlacer.super.beginTreeCanopy(position)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
canopyPosition = position;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void endTreeCanopy() {
|
||||||
|
canopyPosition = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isCanopy(int x, int y, int z) {
|
||||||
|
return canopyPosition != null && canopyPosition.getX() == x
|
||||||
|
&& canopyPosition.getY() == y && canopyPosition.getZ() == z;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public NativeBlockState get(int x, int y, int z) {
|
public NativeBlockState get(int x, int y, int z) {
|
||||||
OverlayCell cell = overlayCell(x, y, z);
|
OverlayCell cell = overlayCell(x, y, z);
|
||||||
@@ -257,7 +286,7 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
|||||||
cell.put(type, data);
|
cell.put(type, data);
|
||||||
}
|
}
|
||||||
mutations.add(replayed != null
|
mutations.add(replayed != null
|
||||||
? replayed : new SetMutation(new DataKey(x, y, z, type), data));
|
? replayed : new SetMutation(new DataKey(x, y, z, type), data, isCanopy(x, y, z)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -476,10 +505,12 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
|||||||
|
|
||||||
DataKey key();
|
DataKey key();
|
||||||
|
|
||||||
|
boolean canopy();
|
||||||
|
|
||||||
void apply(IObjectPlacer placer);
|
void apply(IObjectPlacer placer);
|
||||||
}
|
}
|
||||||
|
|
||||||
record SetMutation(DataKey key, Object value) implements Mutation {
|
record SetMutation(DataKey key, Object value, boolean canopy) implements Mutation {
|
||||||
@Override
|
@Override
|
||||||
public int x() {
|
public int x() {
|
||||||
return key.x();
|
return key.x();
|
||||||
@@ -496,7 +527,7 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
record CustomBlockMutation(DataKey key, NativeBlockState state) implements Mutation {
|
record CustomBlockMutation(DataKey key, NativeBlockState state, boolean canopy) implements Mutation {
|
||||||
@Override
|
@Override
|
||||||
public int x() {
|
public int x() {
|
||||||
return key.x();
|
return key.x();
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ final class ObjectSourcePlan {
|
|||||||
private static final int PLACEMENT_DESTINATION_BYTES = 40;
|
private static final int PLACEMENT_DESTINATION_BYTES = 40;
|
||||||
private static final int UNKNOWN_PAYLOAD_BYTES = 256;
|
private static final int UNKNOWN_PAYLOAD_BYTES = 256;
|
||||||
private static final int CUSTOM_BIT = 1 << 8;
|
private static final int CUSTOM_BIT = 1 << 8;
|
||||||
private static final int Y_SHIFT = 9;
|
private static final int CANOPY_BIT = 1 << 9;
|
||||||
|
private static final int Y_SHIFT = 10;
|
||||||
private static final int MAXIMUM_Y = (1 << (31 - Y_SHIFT)) - 1;
|
private static final int MAXIMUM_Y = (1 << (31 - Y_SHIFT)) - 1;
|
||||||
|
|
||||||
private final long[] destinations;
|
private final long[] destinations;
|
||||||
@@ -102,7 +103,7 @@ final class ObjectSourcePlan {
|
|||||||
custom = CUSTOM_BIT;
|
custom = CUSTOM_BIT;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cells[position] = (key.y() << Y_SHIFT) | custom | ((key.z() & 15) << 4) | (key.x() & 15);
|
cells[position] = (key.y() << Y_SHIFT) | custom | (mutation.canopy() ? CANOPY_BIT : 0) | ((key.z() & 15) << 4) | (key.x() & 15);
|
||||||
valueIndexes[position] = table.indexOf(value);
|
valueIndexes[position] = table.indexOf(value);
|
||||||
}
|
}
|
||||||
values = table.values.toArray();
|
values = table.values.toArray();
|
||||||
@@ -275,9 +276,9 @@ final class ObjectSourcePlan {
|
|||||||
Object value = values[valueIndexes[start + index]];
|
Object value = values[valueIndexes[start + index]];
|
||||||
decoded[index] = (cell & CUSTOM_BIT) != 0
|
decoded[index] = (cell & CUSTOM_BIT) != 0
|
||||||
? new ObjectDestinationTransaction.CustomBlockMutation(
|
? new ObjectDestinationTransaction.CustomBlockMutation(
|
||||||
new ObjectDestinationTransaction.DataKey(x, y, z, NativeBlockState.class), (NativeBlockState) value)
|
new ObjectDestinationTransaction.DataKey(x, y, z, NativeBlockState.class), (NativeBlockState) value, (cell & CANOPY_BIT) != 0)
|
||||||
: new ObjectDestinationTransaction.SetMutation(
|
: new ObjectDestinationTransaction.SetMutation(
|
||||||
new ObjectDestinationTransaction.DataKey(x, y, z, typeOf(value)), value);
|
new ObjectDestinationTransaction.DataKey(x, y, z, typeOf(value)), value, (cell & CANOPY_BIT) != 0);
|
||||||
}
|
}
|
||||||
return List.of(decoded);
|
return List.of(decoded);
|
||||||
}
|
}
|
||||||
@@ -285,10 +286,17 @@ final class ObjectSourcePlan {
|
|||||||
static byte[] encode(List<ObjectDestinationTransaction.Mutation> mutations, int height) {
|
static byte[] encode(List<ObjectDestinationTransaction.Mutation> mutations, int height) {
|
||||||
IrisMatterSupport.ensureRegistered();
|
IrisMatterSupport.ensureRegistered();
|
||||||
Matter payload = new IrisMatter(16, height, 16);
|
Matter payload = new IrisMatter(16, height, 16);
|
||||||
|
boolean skipCanopy = false;
|
||||||
for (ObjectDestinationTransaction.Mutation mutation : mutations) {
|
for (ObjectDestinationTransaction.Mutation mutation : mutations) {
|
||||||
ObjectDestinationTransaction.DataKey key = mutation.key();
|
ObjectDestinationTransaction.DataKey key = mutation.key();
|
||||||
int x = key.x() & 15;
|
int x = key.x() & 15;
|
||||||
int z = key.z() & 15;
|
int z = key.z() & 15;
|
||||||
|
if (key.type() == NativeBlockState.class) {
|
||||||
|
skipCanopy = !ObjectContinuationPersistence.beginBlock(payload, x, key.y(), z, mutation.canopy());
|
||||||
|
}
|
||||||
|
if (mutation.canopy() && skipCanopy) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (mutation instanceof ObjectDestinationTransaction.CustomBlockMutation custom) {
|
if (mutation instanceof ObjectDestinationTransaction.CustomBlockMutation custom) {
|
||||||
payload.slice(NativeBlockState.class).set(x, key.y(), z, custom.state().placementBaseState());
|
payload.slice(NativeBlockState.class).set(x, key.y(), z, custom.state().placementBaseState());
|
||||||
payload.slice(Identifier.class).set(x, key.y(), z, Identifier.fromString(custom.state().deferredPlacementKey()));
|
payload.slice(Identifier.class).set(x, key.y(), z, Identifier.fromString(custom.state().deferredPlacementKey()));
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package art.arcane.iris.generation.mantle;
|
||||||
|
|
||||||
|
public enum TreeCanopy {
|
||||||
|
INSTANCE
|
||||||
|
}
|
||||||
@@ -23,11 +23,9 @@ import art.arcane.iris.generation.concurrent.MultiBurst;
|
|||||||
import art.arcane.iris.generation.context.ChunkContext;
|
import art.arcane.iris.generation.context.ChunkContext;
|
||||||
import art.arcane.iris.generation.context.IrisContext;
|
import art.arcane.iris.generation.context.IrisContext;
|
||||||
import art.arcane.iris.generation.mantle.EngineMantle;
|
import art.arcane.iris.generation.mantle.EngineMantle;
|
||||||
|
import art.arcane.iris.generation.mantle.ObjectContinuationPersistence;
|
||||||
import art.arcane.iris.generation.stage.IrisDimensionStackActuator;
|
import art.arcane.iris.generation.stage.IrisDimensionStackActuator;
|
||||||
import art.arcane.iris.generation.stage.IrisTransitionGeometryActuator;
|
import art.arcane.iris.generation.stage.IrisTransitionGeometryActuator;
|
||||||
import art.arcane.iris.integration.Identifier;
|
|
||||||
import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial;
|
|
||||||
import art.arcane.iris.world.storage.matter.TileWrapper;
|
|
||||||
import art.arcane.iris.world.history.FloatingBiomeOverlay;
|
import art.arcane.iris.world.history.FloatingBiomeOverlay;
|
||||||
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
|
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
|
||||||
import art.arcane.iris.world.history.SavedTerrainChunk;
|
import art.arcane.iris.world.history.SavedTerrainChunk;
|
||||||
@@ -36,13 +34,10 @@ import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
|||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
import art.arcane.volmlib.util.documentation.BlockCoordinates;
|
import art.arcane.volmlib.util.documentation.BlockCoordinates;
|
||||||
import art.arcane.volmlib.util.hunk.Hunk;
|
import art.arcane.volmlib.util.hunk.Hunk;
|
||||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
|
||||||
import art.arcane.volmlib.util.matter.Matter;
|
import art.arcane.volmlib.util.matter.Matter;
|
||||||
import art.arcane.volmlib.util.matter.MatterSlice;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public interface EngineMode extends Staged {
|
public interface EngineMode extends Staged {
|
||||||
KList<EngineStage> getTerrainStages();
|
KList<EngineStage> getTerrainStages();
|
||||||
@@ -221,7 +216,7 @@ public interface EngineMode extends Staged {
|
|||||||
recordNaturalTerrain(x, z, blocks, biomes, ctx);
|
recordNaturalTerrain(x, z, blocks, biomes, ctx);
|
||||||
IrisDimensionStackActuator.clearHostNaturalMetadata(getEngine(), x, z, biomes.getHeight(), ctx);
|
IrisDimensionStackActuator.clearHostNaturalMetadata(getEngine(), x, z, biomes.getHeight(), ctx);
|
||||||
ctx.beginContent();
|
ctx.beginContent();
|
||||||
List<Matter> continuations = restoreObjectContinuations(x, z);
|
List<Matter> continuations = restoreObjectContinuations(x, z, blocks);
|
||||||
if (!continuations.isEmpty()) {
|
if (!continuations.isEmpty()) {
|
||||||
ctx.setCaveTerrain(null);
|
ctx.setCaveTerrain(null);
|
||||||
}
|
}
|
||||||
@@ -229,16 +224,13 @@ public interface EngineMode extends Staged {
|
|||||||
i.generate(x, z, blocks, biomes, multicore, ctx);
|
i.generate(x, z, blocks, biomes, multicore, ctx);
|
||||||
}
|
}
|
||||||
for (Matter continuation : continuations) {
|
for (Matter continuation : continuations) {
|
||||||
restoreObjectMetadata(x, z, continuation);
|
ObjectContinuationPersistence.restore(getMantle().getMantle(), x >> 4, z >> 4,
|
||||||
MatterSlice<NativeBlockState> states = continuation.getSlice(NativeBlockState.class);
|
continuation, blocks, true);
|
||||||
if (states != null) {
|
|
||||||
states.iterateSync((localX, y, localZ, state) -> blocks.set(localX, y, localZ, state));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Matter> restoreObjectContinuations(int x, int z) {
|
private List<Matter> restoreObjectContinuations(int x, int z, Hunk<NativeBlockState> blocks) {
|
||||||
if (!(getEngine() instanceof IrisEngine irisEngine)) {
|
if (!(getEngine() instanceof IrisEngine irisEngine)) {
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
@@ -257,7 +249,8 @@ public interface EngineMode extends Staged {
|
|||||||
if (continuation.getHeight() != getEngine().getHeight()) {
|
if (continuation.getHeight() != getEngine().getHeight()) {
|
||||||
throw new IOException("Object continuation height does not match the world");
|
throw new IOException("Object continuation height does not match the world");
|
||||||
}
|
}
|
||||||
restoreObjectMetadata(x, z, continuation);
|
ObjectContinuationPersistence.restore(getMantle().getMantle(), x >> 4, z >> 4,
|
||||||
|
continuation, blocks, false);
|
||||||
}
|
}
|
||||||
return continuations;
|
return continuations;
|
||||||
} catch (IOException failure) {
|
} catch (IOException failure) {
|
||||||
@@ -265,42 +258,6 @@ public interface EngineMode extends Staged {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void restoreObjectMetadata(int x, int z, Matter continuation) {
|
|
||||||
MantleChunk<Matter> chunk = getMantle().getMantle().useChunk(x >> 4, z >> 4);
|
|
||||||
try {
|
|
||||||
synchronized (chunk) {
|
|
||||||
MatterSlice<NativeBlockState> states = continuation.getSlice(NativeBlockState.class);
|
|
||||||
if (states != null) {
|
|
||||||
states.iterateSync((localX, y, localZ, state) -> {
|
|
||||||
Matter section = chunk.get(y >> 4);
|
|
||||||
if (section != null) {
|
|
||||||
clearObjectMetadata(section, localX, y & 15, localZ);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
for (Map.Entry<Class<?>, MatterSlice<?>> entry : continuation.getSliceMap().entrySet()) {
|
|
||||||
entry.getValue().iterateSync((localX, y, localZ, value) ->
|
|
||||||
chunk.getOrCreate(y >> 4).slice(entry.getKey()).set(localX, y & 15, localZ, value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
chunk.release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void clearObjectMetadata(Matter section, int x, int y, int z) {
|
|
||||||
clearObjectMetadata(section.getSlice(Identifier.class), x, y, z);
|
|
||||||
clearObjectMetadata(section.getSlice(String.class), x, y, z);
|
|
||||||
clearObjectMetadata(section.getSlice(TreeBlockMaterial.class), x, y, z);
|
|
||||||
clearObjectMetadata(section.getSlice(TileWrapper.class), x, y, z);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void clearObjectMetadata(MatterSlice<?> slice, int x, int y, int z) {
|
|
||||||
if (slice != null) {
|
|
||||||
slice.set(x, y, z, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void recordNaturalTerrain(int x, int z, Hunk<NativeBlockState> blocks,
|
private void recordNaturalTerrain(int x, int z, Hunk<NativeBlockState> blocks,
|
||||||
Hunk<NativeBiome> biomes, ChunkContext context) {
|
Hunk<NativeBiome> biomes, ChunkContext context) {
|
||||||
if (!(getEngine() instanceof IrisEngine irisEngine)) {
|
if (!(getEngine() instanceof IrisEngine irisEngine)) {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import art.arcane.iris.generation.block.TileData;
|
|||||||
import art.arcane.iris.pack.loading.IrisData;
|
import art.arcane.iris.pack.loading.IrisData;
|
||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||||
|
import art.arcane.volmlib.util.math.BlockPosition;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
public class DecayControlPlacer implements IObjectPlacer {
|
public class DecayControlPlacer implements IObjectPlacer {
|
||||||
@@ -47,6 +48,16 @@ public class DecayControlPlacer implements IObjectPlacer {
|
|||||||
delegate.set(x, y, z, d);
|
delegate.set(x, y, z, d);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean beginTreeCanopy(BlockPosition position) {
|
||||||
|
return delegate.beginTreeCanopy(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void endTreeCanopy() {
|
||||||
|
delegate.endTreeCanopy();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public NativeBlockState get(int x, int y, int z) {
|
public NativeBlockState get(int x, int y, int z) {
|
||||||
return delegate.get(x, y, z);
|
return delegate.get(x, y, z);
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import art.arcane.iris.pack.loading.IrisData;
|
|||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
import art.arcane.iris.generation.block.TileData;
|
import art.arcane.iris.generation.block.TileData;
|
||||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||||
|
import art.arcane.volmlib.util.math.BlockPosition;
|
||||||
import art.arcane.volmlib.util.math.RNG;
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
|
|
||||||
public class HeightmapObjectPlacer implements IObjectPlacer {
|
public class HeightmapObjectPlacer implements IObjectPlacer {
|
||||||
@@ -47,6 +48,16 @@ public class HeightmapObjectPlacer implements IObjectPlacer {
|
|||||||
oplacer.set(param1Int1, param1Int2, param1Int3, param1BlockData);
|
oplacer.set(param1Int1, param1Int2, param1Int3, param1BlockData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean beginTreeCanopy(BlockPosition position) {
|
||||||
|
return oplacer.beginTreeCanopy(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void endTreeCanopy() {
|
||||||
|
oplacer.endTreeCanopy();
|
||||||
|
}
|
||||||
|
|
||||||
public NativeBlockState get(int param1Int1, int param1Int2, int param1Int3) {
|
public NativeBlockState get(int param1Int1, int param1Int2, int param1Int3) {
|
||||||
return oplacer.get(param1Int1, param1Int2, param1Int3);
|
return oplacer.get(param1Int1, param1Int2, param1Int3);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import art.arcane.iris.pack.loading.IrisData;
|
|||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
|
||||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||||
|
import art.arcane.volmlib.util.math.BlockPosition;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
public interface IObjectPlacer {
|
public interface IObjectPlacer {
|
||||||
@@ -35,6 +36,14 @@ public interface IObjectPlacer {
|
|||||||
|
|
||||||
NativeBlockState get(int x, int y, int z);
|
NativeBlockState get(int x, int y, int z);
|
||||||
|
|
||||||
|
default boolean beginTreeCanopy(BlockPosition position) {
|
||||||
|
NativeBlockState existing = get(position.getX(), position.getY(), position.getZ());
|
||||||
|
return existing == null || !existing.isSolid();
|
||||||
|
}
|
||||||
|
|
||||||
|
default void endTreeCanopy() {
|
||||||
|
}
|
||||||
|
|
||||||
boolean isPreventingDecay();
|
boolean isPreventingDecay();
|
||||||
|
|
||||||
boolean isCarved(int x, int y, int z);
|
boolean isCarved(int x, int y, int z);
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ package art.arcane.iris.structure.object;
|
|||||||
|
|
||||||
import art.arcane.iris.structure.object.FloatingObjectFootprint;
|
import art.arcane.iris.structure.object.FloatingObjectFootprint;
|
||||||
import art.arcane.iris.generation.block.TileData;
|
import art.arcane.iris.generation.block.TileData;
|
||||||
|
import art.arcane.iris.generation.decoration.tree.TreeGrounding;
|
||||||
import art.arcane.iris.generation.cave.CarveResult;
|
import art.arcane.iris.generation.cave.CarveResult;
|
||||||
import art.arcane.iris.pack.value.IrisPosition;
|
import art.arcane.iris.pack.value.IrisPosition;
|
||||||
|
|
||||||
@@ -144,6 +145,9 @@ public class IrisObject extends IrisRegistrant {
|
|||||||
transient final AtomicCache<KList<IrisBlockVector>> surfaceSupportOffsets = new AtomicCache<>();
|
transient final AtomicCache<KList<IrisBlockVector>> surfaceSupportOffsets = new AtomicCache<>();
|
||||||
transient final AtomicCache<FloatingObjectFootprint> floatingFootprint = new AtomicCache<>();
|
transient final AtomicCache<FloatingObjectFootprint> floatingFootprint = new AtomicCache<>();
|
||||||
@Getter
|
@Getter
|
||||||
|
@Setter
|
||||||
|
private transient TreeGrounding treeGrounding;
|
||||||
|
@Getter
|
||||||
VectorMap<NativeBlockState> blocks;
|
VectorMap<NativeBlockState> blocks;
|
||||||
@Getter
|
@Getter
|
||||||
VectorMap<TileData> states;
|
VectorMap<TileData> states;
|
||||||
@@ -203,6 +207,7 @@ public class IrisObject extends IrisRegistrant {
|
|||||||
copy.setLoader(getLoader());
|
copy.setLoader(getLoader());
|
||||||
copy.setLoadFile(getLoadFile());
|
copy.setLoadFile(getLoadFile());
|
||||||
copy.setCenter(getCenter().clone());
|
copy.setCenter(getCenter().clone());
|
||||||
|
copy.setTreeGrounding(treeGrounding);
|
||||||
|
|
||||||
blocks.forEach(copy.blocks::put);
|
blocks.forEach(copy.blocks::put);
|
||||||
states.forEach((position, tile) -> copy.states.put(position, tile.clone()));
|
states.forEach((position, tile) -> copy.states.put(position, tile.clone()));
|
||||||
@@ -349,6 +354,9 @@ public class IrisObject extends IrisRegistrant {
|
|||||||
|
|
||||||
KList<IrisBlockVector> getSurfaceSupportOffsets() {
|
KList<IrisBlockVector> getSurfaceSupportOffsets() {
|
||||||
return surfaceSupportOffsets.aquire(() -> {
|
return surfaceSupportOffsets.aquire(() -> {
|
||||||
|
if (treeGrounding != null) {
|
||||||
|
return treeGrounding.supportOffsets();
|
||||||
|
}
|
||||||
readLock.lock();
|
readLock.lock();
|
||||||
try {
|
try {
|
||||||
int lowestY = Integer.MAX_VALUE;
|
int lowestY = Integer.MAX_VALUE;
|
||||||
|
|||||||
+69
-31
@@ -23,6 +23,7 @@ import art.arcane.iris.generation.block.TileData;
|
|||||||
import art.arcane.iris.generation.cave.CarveResult;
|
import art.arcane.iris.generation.cave.CarveResult;
|
||||||
import art.arcane.iris.generation.cave.CarvingMode;
|
import art.arcane.iris.generation.cave.CarvingMode;
|
||||||
import art.arcane.iris.generation.decoration.IrisProceduralBlocks;
|
import art.arcane.iris.generation.decoration.IrisProceduralBlocks;
|
||||||
|
import art.arcane.iris.generation.decoration.tree.TreeGrounding;
|
||||||
import art.arcane.iris.generation.decoration.IrisStiltSettings;
|
import art.arcane.iris.generation.decoration.IrisStiltSettings;
|
||||||
import art.arcane.iris.structure.object.IrisSurfaceSupport;
|
import art.arcane.iris.structure.object.IrisSurfaceSupport;
|
||||||
import art.arcane.iris.generation.decoration.IrisVacuumSettings;
|
import art.arcane.iris.generation.decoration.IrisVacuumSettings;
|
||||||
@@ -331,6 +332,18 @@ final class IrisObjectPlacementRunner {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TreeGrounding.Plan grounding = null;
|
||||||
|
if (self.getTreeGrounding() != null && yv < 0 && config.getCarvingSupport().supportsSurface()
|
||||||
|
&& !config.isForcePlace() && !config.isFromBottom() && !paint && !ceilingHang
|
||||||
|
&& config.getMode() != ObjectPlaceMode.FLOATING && !rawStructurePiece) {
|
||||||
|
grounding = self.getTreeGrounding().plan(placer, new TreeGrounding.Placement(rdata, config,
|
||||||
|
new BlockPosition(x, y + yrand, z), new BlockPosition(spinx, spiny, spinz)));
|
||||||
|
if (!grounding.supported()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
stilting = false;
|
||||||
|
}
|
||||||
|
|
||||||
IrisPaintSurfaceProjection paintProjection = null;
|
IrisPaintSurfaceProjection paintProjection = null;
|
||||||
KList<IrisBlockVector> paintSupport = null;
|
KList<IrisBlockVector> paintSupport = null;
|
||||||
if (paint) {
|
if (paint) {
|
||||||
@@ -467,6 +480,7 @@ final class IrisObjectPlacementRunner {
|
|||||||
try {
|
try {
|
||||||
VectorMap<NativeBlockState> blocks = self.blocks;
|
VectorMap<NativeBlockState> blocks = self.blocks;
|
||||||
VectorMap<TileData> states = self.states;
|
VectorMap<TileData> states = self.states;
|
||||||
|
TreeGrounding tree = self.getTreeGrounding();
|
||||||
// Zero-tile objects are the common case: skip the two Key allocations VectorMap#get needs to prove null.
|
// Zero-tile objects are the common case: skip the two Key allocations VectorMap#get needs to prove null.
|
||||||
boolean hasStates = !states.isEmpty();
|
boolean hasStates = !states.isEmpty();
|
||||||
|
|
||||||
@@ -622,42 +636,53 @@ final class IrisObjectPlacementRunner {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
data = normalizePlacedWaterlogging(data, placer, xx, yy, zz, waterlogCandidate);
|
boolean canopy = tree != null && tree.isCanopy(g.getBlockX(), g.getBlockY(), g.getBlockZ());
|
||||||
|
if (canopy && !placer.beginTreeCanopy(new BlockPosition(xx, yy, zz))) {
|
||||||
if (!rawStructurePiece && B.isVineBlock(data)) {
|
continue;
|
||||||
data = attachVineFaces(placer, data, xx, yy, zz);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Short-circuit order matters for cost only: the mantle read is paid solely
|
try {
|
||||||
// for vine blocks. Both operands are pure, so the value is unchanged.
|
data = normalizePlacedWaterlogging(data, placer, xx, yy, zz, waterlogCandidate);
|
||||||
boolean wouldReplace = !rawStructurePiece && B.isVineBlock(data) && B.isSolid(placer.get(xx, yy, zz));
|
|
||||||
String material = IrisObjectShaping.materialKey(data);
|
|
||||||
boolean air = material.equals("minecraft:air") || material.equals("minecraft:cave_air");
|
|
||||||
boolean place = shouldPlaceObjectBlock(rawStructurePiece, air, wouldReplace);
|
|
||||||
|
|
||||||
if (data.isCustom() || place) {
|
if (!rawStructurePiece && B.isVineBlock(data)) {
|
||||||
placer.set(xx, yy, zz, data);
|
data = attachVineFaces(placer, data, xx, yy, zz);
|
||||||
if (heightmap != null) {
|
}
|
||||||
Position2 pos = new Position2(xx, zz);
|
|
||||||
Integer currentHeight = heightmap.get(pos);
|
// Short-circuit order matters for cost only: the mantle read is paid solely
|
||||||
if (currentHeight == null || currentHeight < yy) {
|
// for vine blocks. Both operands are pure, so the value is unchanged.
|
||||||
heightmap.put(pos, yy);
|
boolean wouldReplace = !rawStructurePiece && B.isVineBlock(data) && B.isSolid(placer.get(xx, yy, zz));
|
||||||
|
String material = IrisObjectShaping.materialKey(data);
|
||||||
|
boolean air = material.equals("minecraft:air") || material.equals("minecraft:cave_air");
|
||||||
|
boolean place = shouldPlaceObjectBlock(rawStructurePiece, air, wouldReplace);
|
||||||
|
|
||||||
|
if (data.isCustom() || place) {
|
||||||
|
placer.set(xx, yy, zz, data);
|
||||||
|
if (heightmap != null) {
|
||||||
|
Position2 pos = new Position2(xx, zz);
|
||||||
|
Integer currentHeight = heightmap.get(pos);
|
||||||
|
if (currentHeight == null || currentHeight < yy) {
|
||||||
|
heightmap.put(pos, yy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tile != null) {
|
||||||
|
placer.setTile(xx, yy, zz, tile);
|
||||||
|
}
|
||||||
|
if (markers != null && markers.containsKey(g)) {
|
||||||
|
placer.setData(xx, yy, zz, new MatterMarker(markers.get(g)));
|
||||||
|
}
|
||||||
|
if (listener != null) {
|
||||||
|
listener.accept(new BlockPosition(xx, yy, zz), data);
|
||||||
|
}
|
||||||
|
if (vacuuming && yy < vacuumLowest) {
|
||||||
|
vacuumLowest = yy;
|
||||||
|
}
|
||||||
|
if (vacuuming && yy > vacuumHighest) {
|
||||||
|
vacuumHighest = yy;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (tile != null) {
|
} finally {
|
||||||
placer.setTile(xx, yy, zz, tile);
|
if (canopy) {
|
||||||
}
|
placer.endTreeCanopy();
|
||||||
if (markers != null && markers.containsKey(g)) {
|
|
||||||
placer.setData(xx, yy, zz, new MatterMarker(markers.get(g)));
|
|
||||||
}
|
|
||||||
if (listener != null) {
|
|
||||||
listener.accept(new BlockPosition(xx, yy, zz), data);
|
|
||||||
}
|
|
||||||
if (vacuuming && yy < vacuumLowest) {
|
|
||||||
vacuumLowest = yy;
|
|
||||||
}
|
|
||||||
if (vacuuming && yy > vacuumHighest) {
|
|
||||||
vacuumHighest = yy;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -665,6 +690,19 @@ final class IrisObjectPlacementRunner {
|
|||||||
self.readLock.unlock();
|
self.readLock.unlock();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (grounding != null) {
|
||||||
|
for (TreeGrounding.Extension extension : grounding.extensions()) {
|
||||||
|
for (int rootY = extension.topY(); rootY >= extension.bottomY(); rootY--) {
|
||||||
|
NativeBlockState rootState = normalizePlacedWaterlogging(extension.state(), placer,
|
||||||
|
extension.x(), rootY, extension.z(), waterlogCandidate);
|
||||||
|
placer.set(extension.x(), rootY, extension.z(), rootState);
|
||||||
|
if (listener != null) {
|
||||||
|
listener.accept(new BlockPosition(extension.x(), rootY, extension.z()), rootState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (stilting) {
|
if (stilting) {
|
||||||
self.readLock.lock();
|
self.readLock.lock();
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ package art.arcane.iris.structure.object;
|
|||||||
|
|
||||||
import art.arcane.iris.generation.block.TileData;
|
import art.arcane.iris.generation.block.TileData;
|
||||||
import art.arcane.iris.generation.decoration.IrisProceduralBlocks;
|
import art.arcane.iris.generation.decoration.IrisProceduralBlocks;
|
||||||
|
import art.arcane.iris.generation.decoration.tree.TreeBlockCanvas;
|
||||||
|
import art.arcane.iris.generation.decoration.tree.TreeGrounding;
|
||||||
|
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||||
@@ -29,6 +31,9 @@ import art.arcane.volmlib.util.math.Vector3i;
|
|||||||
import art.arcane.volmlib.util.format.Form;
|
import art.arcane.volmlib.util.format.Form;
|
||||||
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
|
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -264,6 +269,19 @@ final class IrisObjectShaping {
|
|||||||
self.shrinkOffset = offset;
|
self.shrinkOffset = offset;
|
||||||
self.blocks = b;
|
self.blocks = b;
|
||||||
self.states = s;
|
self.states = s;
|
||||||
|
if (self.getTreeGrounding() != null) {
|
||||||
|
List<TreeGrounding.Root> roots = new ArrayList<>();
|
||||||
|
for (TreeGrounding.Root root : self.getTreeGrounding().roots()) {
|
||||||
|
roots.add(new TreeGrounding.Root(root.x() + offset.getX(), root.y() + offset.getY(),
|
||||||
|
root.z() + offset.getZ(), root.state()));
|
||||||
|
}
|
||||||
|
Set<TreeBlockCanvas.Vec> canopy = new HashSet<>();
|
||||||
|
for (TreeBlockCanvas.Vec leaf : self.getTreeGrounding().canopy()) {
|
||||||
|
canopy.add(new TreeBlockCanvas.Vec(leaf.x() + offset.getX(), leaf.y() + offset.getY(),
|
||||||
|
leaf.z() + offset.getZ()));
|
||||||
|
}
|
||||||
|
self.setTreeGrounding(TreeGrounding.create(roots, self.getTreeGrounding().maxExtension(), canopy));
|
||||||
|
}
|
||||||
self.surfaceSupportOffsets.reset();
|
self.surfaceSupportOffsets.reset();
|
||||||
self.floatingFootprint.reset();
|
self.floatingFootprint.reset();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import art.arcane.iris.pack.value.IrisPosition;
|
|||||||
|
|
||||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||||
import art.arcane.iris.generation.block.B;
|
import art.arcane.iris.generation.block.B;
|
||||||
|
import art.arcane.iris.generation.decoration.tree.TreeBlockCanvas;
|
||||||
|
import art.arcane.iris.generation.decoration.tree.TreeGrounding;
|
||||||
import art.arcane.iris.generation.block.VectorMap;
|
import art.arcane.iris.generation.block.VectorMap;
|
||||||
import art.arcane.iris.generation.geometry.AxisAlignedBB;
|
import art.arcane.iris.generation.geometry.AxisAlignedBB;
|
||||||
import art.arcane.iris.generation.geometry.IrisBlockVector;
|
import art.arcane.iris.generation.geometry.IrisBlockVector;
|
||||||
@@ -33,10 +35,12 @@ import art.arcane.volmlib.util.interpolation.Interpolation3D;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Geometric transforms for {@link IrisObject}: rotation, scaling and the interpolated upscalers.
|
* Geometric transforms for {@link IrisObject}: rotation, scaling and the interpolated upscalers.
|
||||||
@@ -56,6 +60,8 @@ final class IrisObjectTransforms {
|
|||||||
try {
|
try {
|
||||||
VectorMap<NativeBlockState> d = new VectorMap<>();
|
VectorMap<NativeBlockState> d = new VectorMap<>();
|
||||||
VectorMap<TileData> dx = new VectorMap<>();
|
VectorMap<TileData> dx = new VectorMap<>();
|
||||||
|
TreeGrounding grounding = self.getTreeGrounding();
|
||||||
|
Set<TreeBlockCanvas.Vec> canopy = grounding == null ? null : new HashSet<>();
|
||||||
|
|
||||||
for (Map.Entry<IrisBlockVector, NativeBlockState> entry : self.blocks) {
|
for (Map.Entry<IrisBlockVector, NativeBlockState> entry : self.blocks) {
|
||||||
NativeBlockState rotated = r.rotate(entry.getValue(), spinx, spiny, spinz);
|
NativeBlockState rotated = r.rotate(entry.getValue(), spinx, spiny, spinz);
|
||||||
@@ -64,6 +70,16 @@ final class IrisObjectTransforms {
|
|||||||
}
|
}
|
||||||
IrisBlockVector destination = r.rotate(entry.getKey(), spinx, spiny, spinz);
|
IrisBlockVector destination = r.rotate(entry.getKey(), spinx, spiny, spinz);
|
||||||
d.put(destination, rotated);
|
d.put(destination, rotated);
|
||||||
|
if (canopy != null) {
|
||||||
|
TreeBlockCanvas.Vec cell = new TreeBlockCanvas.Vec(destination.getBlockX(),
|
||||||
|
destination.getBlockY(), destination.getBlockZ());
|
||||||
|
IrisBlockVector source = entry.getKey();
|
||||||
|
if (grounding.isCanopy(source.getBlockX(), source.getBlockY(), source.getBlockZ())) {
|
||||||
|
canopy.add(cell);
|
||||||
|
} else {
|
||||||
|
canopy.remove(cell);
|
||||||
|
}
|
||||||
|
}
|
||||||
TileData tile = self.states.get(entry.getKey());
|
TileData tile = self.states.get(entry.getKey());
|
||||||
if (tile == null) {
|
if (tile == null) {
|
||||||
dx.remove(destination);
|
dx.remove(destination);
|
||||||
@@ -74,6 +90,17 @@ final class IrisObjectTransforms {
|
|||||||
|
|
||||||
self.blocks = d;
|
self.blocks = d;
|
||||||
self.states = dx;
|
self.states = dx;
|
||||||
|
if (grounding != null) {
|
||||||
|
List<TreeGrounding.Root> roots = new ArrayList<>();
|
||||||
|
for (TreeGrounding.Root root : grounding.roots()) {
|
||||||
|
IrisBlockVector position = r.rotate(new IrisBlockVector(root.x(), root.y(), root.z()), spinx, spiny, spinz);
|
||||||
|
NativeBlockState state = d.get(position);
|
||||||
|
if (state != null) {
|
||||||
|
roots.add(new TreeGrounding.Root(position.getBlockX(), position.getBlockY(), position.getBlockZ(), state));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.setTreeGrounding(TreeGrounding.create(roots, grounding.maxExtension(), canopy));
|
||||||
|
}
|
||||||
IrisObjectShaping.shrinkwrap(self);
|
IrisObjectShaping.shrinkwrap(self);
|
||||||
self.surfaceSupportOffsets.reset();
|
self.surfaceSupportOffsets.reset();
|
||||||
self.floatingFootprint.reset();
|
self.floatingFootprint.reset();
|
||||||
@@ -99,6 +126,8 @@ final class IrisObjectTransforms {
|
|||||||
boolean savedOrigin = origin == ScaleOrigin.SAVED;
|
boolean savedOrigin = origin == ScaleOrigin.SAVED;
|
||||||
List<ScaledVoxel> voxels;
|
List<ScaledVoxel> voxels;
|
||||||
IrisObject output;
|
IrisObject output;
|
||||||
|
TreeGrounding grounding;
|
||||||
|
Vector3i sourceCenter;
|
||||||
self.readLock.lock();
|
self.readLock.lock();
|
||||||
try {
|
try {
|
||||||
output = savedOrigin ? createOriginScaledObject(self, scale)
|
output = savedOrigin ? createOriginScaledObject(self, scale)
|
||||||
@@ -107,7 +136,8 @@ final class IrisObjectTransforms {
|
|||||||
output.setLoader(self.getLoader());
|
output.setLoader(self.getLoader());
|
||||||
output.setLoadFile(self.getLoadFile());
|
output.setLoadFile(self.getLoadFile());
|
||||||
voxels = new ArrayList<>(self.blocks.size());
|
voxels = new ArrayList<>(self.blocks.size());
|
||||||
Vector3i sourceCenter = self.getCenter();
|
sourceCenter = self.getCenter().clone();
|
||||||
|
grounding = self.getTreeGrounding();
|
||||||
IrisBlockVector targetCenter = new IrisBlockVector(output.getCenter().getX(),
|
IrisBlockVector targetCenter = new IrisBlockVector(output.getCenter().getX(),
|
||||||
output.getCenter().getY(), output.getCenter().getZ());
|
output.getCenter().getY(), output.getCenter().getZ());
|
||||||
for (Map.Entry<IrisBlockVector, NativeBlockState> entry : self.blocks) {
|
for (Map.Entry<IrisBlockVector, NativeBlockState> entry : self.blocks) {
|
||||||
@@ -124,7 +154,8 @@ final class IrisObjectTransforms {
|
|||||||
}
|
}
|
||||||
TileData tile = self.states.get(sourcePosition);
|
TileData tile = self.states.get(sourcePosition);
|
||||||
voxels.add(new ScaledVoxel(sourcePosition.clone(), minimum, maximum, entry.getValue(),
|
voxels.add(new ScaledVoxel(sourcePosition.clone(), minimum, maximum, entry.getValue(),
|
||||||
tile == null ? null : tile.clone()));
|
tile == null ? null : tile.clone(), grounding != null && grounding.isCanopy(
|
||||||
|
sourcePosition.getBlockX(), sourcePosition.getBlockY(), sourcePosition.getBlockZ())));
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
self.readLock.unlock();
|
self.readLock.unlock();
|
||||||
@@ -134,8 +165,9 @@ final class IrisObjectTransforms {
|
|||||||
.thenComparingInt(voxel -> voxel.source().getBlockY())
|
.thenComparingInt(voxel -> voxel.source().getBlockY())
|
||||||
.thenComparingInt(voxel -> voxel.source().getBlockZ()));
|
.thenComparingInt(voxel -> voxel.source().getBlockZ()));
|
||||||
}
|
}
|
||||||
|
Set<TreeBlockCanvas.Vec> canopy = grounding == null ? null : new HashSet<>();
|
||||||
for (ScaledVoxel voxel : voxels) {
|
for (ScaledVoxel voxel : voxels) {
|
||||||
writeScaledVoxel(output, voxel);
|
writeScaledVoxel(output, voxel, canopy);
|
||||||
}
|
}
|
||||||
|
|
||||||
VectorMap<NativeBlockState> scaledBlocks = output.blocks;
|
VectorMap<NativeBlockState> scaledBlocks = output.blocks;
|
||||||
@@ -147,10 +179,43 @@ final class IrisObjectTransforms {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
removeInapplicableTiles(output, scaledBlocks);
|
removeInapplicableTiles(output, scaledBlocks);
|
||||||
|
if (grounding != null) {
|
||||||
|
canopy.removeIf(cell -> !output.blocks.containsKey(new IrisBlockVector(cell.x(), cell.y(), cell.z())));
|
||||||
|
scaleRoots(grounding, sourceCenter, output, scale, savedOrigin, canopy);
|
||||||
|
}
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void writeScaledVoxel(IrisObject output, ScaledVoxel voxel) {
|
private static void scaleRoots(TreeGrounding grounding, Vector3i sourceCenter, IrisObject output,
|
||||||
|
double scale, boolean savedOrigin, Set<TreeBlockCanvas.Vec> canopy) {
|
||||||
|
List<TreeGrounding.Root> roots = new ArrayList<>();
|
||||||
|
Vector3i targetCenter = output.getCenter();
|
||||||
|
for (TreeGrounding.Root root : grounding.roots()) {
|
||||||
|
IrisBlockVector position = new IrisBlockVector(root.x(), root.y(), root.z());
|
||||||
|
if (!savedOrigin) {
|
||||||
|
position.add(new IrisBlockVector(sourceCenter.getX(), sourceCenter.getY(), sourceCenter.getZ()));
|
||||||
|
}
|
||||||
|
IrisBlockVector minimum = scaledMinimum(position, scale);
|
||||||
|
IrisBlockVector maximum = scaledMaximum(position, scale);
|
||||||
|
if (!savedOrigin) {
|
||||||
|
IrisBlockVector offset = new IrisBlockVector(targetCenter.getX(), targetCenter.getY(), targetCenter.getZ());
|
||||||
|
minimum.subtract(offset);
|
||||||
|
maximum.subtract(offset);
|
||||||
|
}
|
||||||
|
int y = minimum.getBlockY();
|
||||||
|
for (int x = minimum.getBlockX(); x <= maximum.getBlockX(); x++) {
|
||||||
|
for (int z = minimum.getBlockZ(); z <= maximum.getBlockZ(); z++) {
|
||||||
|
NativeBlockState state = output.blocks.get(new IrisBlockVector(x, y, z));
|
||||||
|
if (state != null) {
|
||||||
|
roots.add(new TreeGrounding.Root(x, y, z, state));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output.setTreeGrounding(TreeGrounding.create(roots, (int) Math.ceil(grounding.maxExtension() * scale), canopy));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeScaledVoxel(IrisObject output, ScaledVoxel voxel, Set<TreeBlockCanvas.Vec> canopy) {
|
||||||
IrisBlockVector minimum = voxel.minimum();
|
IrisBlockVector minimum = voxel.minimum();
|
||||||
IrisBlockVector maximum = voxel.maximum();
|
IrisBlockVector maximum = voxel.maximum();
|
||||||
IrisBlockVector position = minimum.clone();
|
IrisBlockVector position = minimum.clone();
|
||||||
@@ -161,6 +226,14 @@ final class IrisObjectTransforms {
|
|||||||
for (int y = minimum.getBlockY(); y <= maximum.getBlockY(); y++) {
|
for (int y = minimum.getBlockY(); y <= maximum.getBlockY(); y++) {
|
||||||
position.setY(y);
|
position.setY(y);
|
||||||
output.blocks.put(position, voxel.block());
|
output.blocks.put(position, voxel.block());
|
||||||
|
if (canopy != null) {
|
||||||
|
TreeBlockCanvas.Vec cell = new TreeBlockCanvas.Vec(x, y, z);
|
||||||
|
if (voxel.canopy()) {
|
||||||
|
canopy.add(cell);
|
||||||
|
} else {
|
||||||
|
canopy.remove(cell);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (voxel.tile() == null) {
|
if (voxel.tile() == null) {
|
||||||
output.states.remove(position);
|
output.states.remove(position);
|
||||||
} else {
|
} else {
|
||||||
@@ -468,7 +541,7 @@ final class IrisObjectTransforms {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private record ScaledVoxel(IrisBlockVector source, IrisBlockVector minimum, IrisBlockVector maximum,
|
private record ScaledVoxel(IrisBlockVector source, IrisBlockVector minimum, IrisBlockVector maximum,
|
||||||
NativeBlockState block, TileData tile) {
|
NativeBlockState block, TileData tile, boolean canopy) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum ScaleOrigin {
|
private enum ScaleOrigin {
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ public final class IrisMatterSupport {
|
|||||||
IrisMatter.registerSliceType(new EntityMatter());
|
IrisMatter.registerSliceType(new EntityMatter());
|
||||||
IrisMatter.registerSliceType(new CustomBlockOwnershipMatter());
|
IrisMatter.registerSliceType(new CustomBlockOwnershipMatter());
|
||||||
IrisMatter.registerSliceType(new ObjectContinuationMatter());
|
IrisMatter.registerSliceType(new ObjectContinuationMatter());
|
||||||
|
IrisMatter.registerSliceType(new TreeCanopyMatter());
|
||||||
IrisMatter.registerSliceType("art.arcane.iris.core.link.Identifier", new IdentifierMatter());
|
IrisMatter.registerSliceType("art.arcane.iris.core.link.Identifier", new IdentifierMatter());
|
||||||
IrisMatter.registerSliceType("art.arcane.iris.engine.framework.NativeStructureOwnershipBundle", new NativeStructureOwnershipMatter());
|
IrisMatter.registerSliceType("art.arcane.iris.engine.framework.NativeStructureOwnershipBundle", new NativeStructureOwnershipMatter());
|
||||||
IrisMatter.registerSliceType(new PlatformBlockMatter());
|
IrisMatter.registerSliceType(new PlatformBlockMatter());
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package art.arcane.iris.world.storage.matter;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.mantle.TreeCanopy;
|
||||||
|
import art.arcane.volmlib.util.data.palette.Palette;
|
||||||
|
import art.arcane.volmlib.util.matter.Sliced;
|
||||||
|
import art.arcane.volmlib.util.matter.slices.RawMatter;
|
||||||
|
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
@Sliced
|
||||||
|
public final class TreeCanopyMatter extends RawMatter<TreeCanopy> {
|
||||||
|
public TreeCanopyMatter() {
|
||||||
|
this(1, 1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TreeCanopyMatter(int width, int height, int depth) {
|
||||||
|
super(width, height, depth, TreeCanopy.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Palette<TreeCanopy> getGlobalPalette() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeNode(TreeCanopy canopy, DataOutputStream output) throws IOException {
|
||||||
|
output.writeByte(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TreeCanopy readNode(DataInputStream input) throws IOException {
|
||||||
|
if (input.readUnsignedByte() != 1) {
|
||||||
|
throw new IOException("Invalid tree canopy placement condition");
|
||||||
|
}
|
||||||
|
return TreeCanopy.INSTANCE;
|
||||||
|
}
|
||||||
|
}
|
||||||
+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.HydrologyCaveAction;
|
||||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
|
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
|
||||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||||
|
import art.arcane.volmlib.util.math.BlockPosition;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.junit.Assert.assertSame;
|
import static org.junit.Assert.assertSame;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyInt;
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
@@ -42,6 +44,26 @@ import static org.mockito.Mockito.verify;
|
|||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
public class CaveObjectPlacementTransactionTest {
|
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
|
@Test
|
||||||
public void validPlacementCommitsBlocksTilesAndMetadataTogether() {
|
public void validPlacementCommitsBlocksTilesAndMetadataTogether() {
|
||||||
IObjectPlacer delegate = createPlacer(128, 80, 20, 60);
|
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.runtime.Engine;
|
||||||
import art.arcane.iris.generation.block.TileData;
|
import art.arcane.iris.generation.block.TileData;
|
||||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||||
|
import art.arcane.volmlib.util.math.BlockPosition;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
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 art.arcane.iris.world.storage.matter.IrisMatterSupport;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.junit.Assert.assertSame;
|
import static org.junit.Assert.assertSame;
|
||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNotNull;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
@@ -42,6 +44,28 @@ import static org.mockito.Mockito.verify;
|
|||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
public class FloatingObjectPlacementTransactionTest {
|
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
|
@Test
|
||||||
public void unsupportedFinalTerrainRejectsBlocksTilesAndMarkersTogether() {
|
public void unsupportedFinalTerrainRejectsBlocksTilesAndMarkersTogether() {
|
||||||
IslandObjectPlacer delegate = createPlacer(false);
|
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");
|
properties.put("CustomName", "continued chest");
|
||||||
TileWrapper tile = new TileWrapper(new TileData("minecraft:chest", properties));
|
TileWrapper tile = new TileWrapper(new TileData("minecraft:chest", properties));
|
||||||
List<ObjectDestinationTransaction.Mutation> mutations = List.of(
|
List<ObjectDestinationTransaction.Mutation> mutations = List.of(
|
||||||
new ObjectDestinationTransaction.CustomBlockMutation(key(NativeBlockState.class), custom),
|
new ObjectDestinationTransaction.CustomBlockMutation(key(NativeBlockState.class), custom, false),
|
||||||
new ObjectDestinationTransaction.SetMutation(key(String.class), "tree@42"),
|
new ObjectDestinationTransaction.SetMutation(key(String.class), "tree@42", false),
|
||||||
new ObjectDestinationTransaction.SetMutation(key(TreeBlockMaterial.class), TreeBlockMaterial.of("minecraft:oak_log")),
|
new ObjectDestinationTransaction.SetMutation(key(TreeBlockMaterial.class), TreeBlockMaterial.of("minecraft:oak_log"), false),
|
||||||
new ObjectDestinationTransaction.SetMutation(key(TileWrapper.class), tile),
|
new ObjectDestinationTransaction.SetMutation(key(TileWrapper.class), tile, false),
|
||||||
new ObjectDestinationTransaction.SetMutation(new ObjectDestinationTransaction.DataKey(17, 5, 1, NativeBlockState.class), air));
|
new ObjectDestinationTransaction.SetMutation(new ObjectDestinationTransaction.DataKey(17, 5, 1, NativeBlockState.class), air, false));
|
||||||
ObjectContinuationBundle.Fragment fragment = new ObjectContinuationBundle.Fragment(
|
ObjectContinuationBundle.Fragment fragment = new ObjectContinuationBundle.Fragment(
|
||||||
new ObjectContinuationBundle.PlacementKey(ObjectContinuationBundle.Kind.BIOME, 0, 0, 2), new ObjectContinuationBundle.Bounds(15, 1, 17, 1),
|
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)),
|
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(
|
ObjectContinuationBundle.Fragment fragment = new ObjectContinuationBundle.Fragment(
|
||||||
new ObjectContinuationBundle.PlacementKey(ObjectContinuationBundle.Kind.BIOME, 0, 0, 0), new ObjectContinuationBundle.Bounds(15, 1, 16, 1),
|
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)),
|
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)) {
|
try (IrisMatterContext.Scope outer = IrisMatterContext.open(current)) {
|
||||||
assertThrows(IOException.class, fragment::decode);
|
assertThrows(IOException.class, fragment::decode);
|
||||||
@@ -278,7 +278,7 @@ public class ObjectContinuationTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static ObjectDestinationTransaction.Mutation marker(int x, String marker) {
|
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 {
|
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.IrisPlatform;
|
||||||
import art.arcane.iris.spi.IrisPlatforms;
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
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.spi.PlatformRegistries;
|
||||||
import art.arcane.iris.testsupport.BukkitTestServer;
|
import art.arcane.iris.testsupport.BukkitTestServer;
|
||||||
import art.arcane.iris.world.storage.matter.IrisMatterSupport;
|
import art.arcane.iris.world.storage.matter.IrisMatterSupport;
|
||||||
@@ -51,6 +53,89 @@ import static org.mockito.Mockito.verify;
|
|||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
public class ObjectDestinationTransactionTest {
|
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
|
@Test
|
||||||
public void sourceCaptureCannotReplaceAuthoredRoomSolids() {
|
public void sourceCaptureCannotReplaceAuthoredRoomSolids() {
|
||||||
MantleWriter writer = writer();
|
MantleWriter writer = writer();
|
||||||
|
|||||||
@@ -326,7 +326,7 @@ public class ObjectSourcePlanCacheTest {
|
|||||||
List<ObjectDestinationTransaction.Mutation> mutations = new ArrayList<>();
|
List<ObjectDestinationTransaction.Mutation> mutations = new ArrayList<>();
|
||||||
for (int[] chunk : chunks) {
|
for (int[] chunk : chunks) {
|
||||||
mutations.add(new ObjectDestinationTransaction.SetMutation(
|
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);
|
return new ObjectSourcePlan(mutations);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,14 +74,14 @@ public class ObjectSourcePlanTest {
|
|||||||
int z = centerZ + random.nextInt(2 * reach + 1) - reach;
|
int z = centerZ + random.nextInt(2 * reach + 1) - reach;
|
||||||
int y = random.nextInt(768);
|
int y = random.nextInt(768);
|
||||||
mutations.add(switch (random.nextInt(6)) {
|
mutations.add(switch (random.nextInt(6)) {
|
||||||
case 0 -> new ObjectDestinationTransaction.CustomBlockMutation(key(x, y, z, NativeBlockState.class), custom);
|
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);
|
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),
|
case 2 -> new ObjectDestinationTransaction.SetMutation(key(x, y, z, TreeBlockMaterial.class),
|
||||||
new TreeBlockMaterial("minecraft:oak_log"));
|
new TreeBlockMaterial("minecraft:oak_log"), random.nextBoolean());
|
||||||
case 3 -> new ObjectDestinationTransaction.SetMutation(key(x, y, z, MatterMarker.class), marker);
|
case 3 -> new ObjectDestinationTransaction.SetMutation(key(x, y, z, MatterMarker.class), marker, random.nextBoolean());
|
||||||
default -> {
|
default -> {
|
||||||
NativeBlockState state = states[random.nextInt(states.length)];
|
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
|
@Test
|
||||||
public void heightsOutsideThePackedRangeAreRejected() {
|
public void heightsOutsideThePackedRangeAreRejected() {
|
||||||
assertThrows(IllegalArgumentException.class, () -> new ObjectSourcePlan(List.of(
|
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(
|
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(
|
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
|
@Test
|
||||||
@@ -139,7 +139,7 @@ public class ObjectSourcePlanTest {
|
|||||||
List<ObjectDestinationTransaction.Mutation> trees = new ArrayList<>();
|
List<ObjectDestinationTransaction.Mutation> trees = new ArrayList<>();
|
||||||
for (int index = 0; index < 100; index++) {
|
for (int index = 0; index < 100; index++) {
|
||||||
trees.add(new ObjectDestinationTransaction.SetMutation(key(index & 15, 4, 0, TreeBlockMaterial.class),
|
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();
|
int single = new ObjectSourcePlan(one).estimatedRetainedBytes();
|
||||||
|
|
||||||
@@ -150,15 +150,15 @@ public class ObjectSourcePlanTest {
|
|||||||
@Test
|
@Test
|
||||||
public void retainedBytesIncludeVariableLengthMarkers() {
|
public void retainedBytesIncludeVariableLengthMarkers() {
|
||||||
ObjectDestinationTransaction.DataKey key = new ObjectDestinationTransaction.DataKey(0, 4, 0, String.class);
|
ObjectDestinationTransaction.DataKey key = new ObjectDestinationTransaction.DataKey(0, 4, 0, String.class);
|
||||||
ObjectSourcePlan shortMarker = new ObjectSourcePlan(List.of(new ObjectDestinationTransaction.SetMutation(key, "a")));
|
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))));
|
ObjectSourcePlan longMarker = new ObjectSourcePlan(List.of(new ObjectDestinationTransaction.SetMutation(key, "a".repeat(4096), false)));
|
||||||
assertTrue(longMarker.estimatedRetainedBytes() - shortMarker.estimatedRetainedBytes() >= 8190);
|
assertTrue(longMarker.estimatedRetainedBytes() - shortMarker.estimatedRetainedBytes() >= 8190);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void unknownPayloadsAreWeighedInsteadOfDisablingRetention() {
|
public void unknownPayloadsAreWeighedInsteadOfDisablingRetention() {
|
||||||
ObjectSourcePlan unknown = new ObjectSourcePlan(List.of(new ObjectDestinationTransaction.SetMutation(
|
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() > new ObjectSourcePlan(List.of(mutationAt(0, 0))).estimatedRetainedBytes());
|
||||||
assertTrue(unknown.estimatedRetainedBytes() < 4096);
|
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> shortMarkers = List.of(mutationAt(0, 0), mutationAt(16, 0));
|
||||||
List<ObjectDestinationTransaction.Mutation> longMarkers = List.of(
|
List<ObjectDestinationTransaction.Mutation> longMarkers = List.of(
|
||||||
new ObjectDestinationTransaction.SetMutation(
|
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.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);
|
ObjectDestinationTransaction.PlacementRange whole = new ObjectDestinationTransaction.PlacementRange(0, 2);
|
||||||
ObjectSourcePlan shortPlain = new ObjectSourcePlan(shortMarkers);
|
ObjectSourcePlan shortPlain = new ObjectSourcePlan(shortMarkers);
|
||||||
ObjectSourcePlan shortCrossing = new ObjectSourcePlan(shortMarkers, List.of(whole));
|
ObjectSourcePlan shortCrossing = new ObjectSourcePlan(shortMarkers, List.of(whole));
|
||||||
@@ -224,6 +224,6 @@ public class ObjectSourcePlanTest {
|
|||||||
|
|
||||||
private static ObjectDestinationTransaction.Mutation mutationAt(int x, int z) {
|
private static ObjectDestinationTransaction.Mutation mutationAt(int x, int z) {
|
||||||
return new ObjectDestinationTransaction.SetMutation(
|
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
|
@ClassRule
|
||||||
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
|
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(
|
private static final List<Class<?>> SCHEMA_ROOTS = List.of(
|
||||||
IrisDimension.class,
|
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