Fixing a handfull of issues!

This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-21 18:07:10 -04:00
1 parent 1132242c58
commit c3359e0a16
30 files changed
+1324 -282

No files matched your search

@@ -245,9 +245,15 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
for (int y = groundY + 1; y <= groundY + 32; y++) {
assertTrue("Roof at " + x + "," + y, state(blocks, x, y, 0).isAir());
}
for (int y = 12; y < groundY; y++) {
int supportY = x < 2 ? groundY - 33 : 12;
for (int y = supportY; y < groundY; y++) {
assertTrue("Missing foundation at " + x + "," + y, state(blocks, x, y, 0).isSolid());
}
if (x < 2) {
for (int y = 13; y < supportY; y++) {
assertTrue("Cave beneath support at " + x + "," + y, state(blocks, x, y, 0).isAir());
}
}
}
}
@@ -268,6 +274,57 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
assertTrue(state(blocks, 0, groundY + 1, 0).isAir());
}
@Test
public void flattenPreservesCavesBelowIntactGroundAtTheSkirtEdge() {
StructureStart start = desertStart();
int groundY = start.getBoundingBox().minY() - 1;
int x = start.getBoundingBox().maxX() + 23;
int z = start.getBoundingBox().minZ();
BoundingBox area = new BoundingBox(x, 0, z, x, 127, z);
Map<BlockPos, BlockState> blocks = new HashMap<>();
for (int y = groundY - 35; y <= groundY; y++) {
BlockState block = y >= groundY - 34 && y <= groundY - 13
? Blocks.CAVE_AIR.defaultBlockState() : Blocks.STONE.defaultBlockState();
put(blocks, x, y, z, block);
}
put(blocks, x, groundY - 1, z, Blocks.DIRT.defaultBlockState());
put(blocks, x, groundY, z, Blocks.GRASS_BLOCK.defaultBlockState());
NativeStructureTerrainIntegrator.TerrainTarget target = surfaceTarget(start, StructureTerrainMode.FLATTEN);
((IrisStructureTerrain) target.terrain()).setHorizontalPadding(24).setFlattenRange(64);
NativeStructureSurfaceFitter.prepareSurfaceStructures(world(blocks), area, List.of(target), (px, pz) -> groundY);
for (int y = groundY - 34; y <= groundY - 13; y++) {
assertEquals("Cave at " + y, Blocks.CAVE_AIR.defaultBlockState(), state(blocks, x, y, z));
}
assertEquals(Blocks.DIRT.defaultBlockState(), state(blocks, x, groundY - 1, z));
}
@Test
public void flattenBridgesTheFoundationGapWithoutFillingACaveBelowItsSupport() {
StructureStart start = desertStart();
int groundY = start.getBoundingBox().minY() - 1;
BoundingBox area = new BoundingBox(0, 0, 0, 0, 127, 0);
Map<BlockPos, BlockState> blocks = new HashMap<>();
put(blocks, 0, groundY, 0, Blocks.GRASS_BLOCK.defaultBlockState());
for (int y = groundY - 11; y <= groundY - 9; y++) {
put(blocks, 0, y, 0, Blocks.STONE.defaultBlockState());
}
for (int y = groundY - 20; y <= groundY - 12; y++) {
put(blocks, 0, y, 0, Blocks.CAVE_AIR.defaultBlockState());
}
NativeStructureSurfaceFitter.prepareSurfaceStructures(world(blocks), area,
List.of(surfaceTarget(start, StructureTerrainMode.FLATTEN)), (x, z) -> groundY);
for (int y = groundY - 8; y < groundY; y++) {
assertEquals("Foundation at " + y, Blocks.STONE.defaultBlockState(), state(blocks, 0, y, 0));
}
for (int y = groundY - 20; y <= groundY - 12; y++) {
assertEquals("Cave at " + y, Blocks.CAVE_AIR.defaultBlockState(), state(blocks, 0, y, 0));
}
}
@Test
public void flattenLeavesBuriedAndSubmergedInstancesUntouched() {
StructureStart start = desertStart();
@@ -619,10 +676,18 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
for (int z = 0; z <= 8; z++) {
int groundY = plan.flattenedHeights().get(NativeStructureTemplateOccupancy.columnKey(x, z)) - 1;
assertTrue("Village grade at " + x + "," + z + ": " + groundY, groundY >= 64 && groundY <= 70);
for (int y = 20; y <= groundY; y++) {
boolean originalFoundation = (x == 0 || x == 8) && (z == 0 || z == 8);
int supportY = originalFoundation ? 63 : 20;
for (int y = supportY; y <= groundY; y++) {
assertTrue("Disconnected village ground at " + x + "," + y + "," + z,
state(blocks, x, y, z).isSolid());
}
if (originalFoundation) {
for (int y = 21; y < supportY; y++) {
assertTrue("Cave beneath foundation at " + x + "," + y + "," + z,
state(blocks, x, y, z).isAir());
}
}
for (int y = groundY + 1; y <= 96; y++) {
assertTrue("Roof remains above village at " + x + "," + y + "," + z,
state(blocks, x, y, z).isAir());
@@ -9,6 +9,7 @@ import art.arcane.iris.structure.nativegen.NativeStructureVolumeIndex;
import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume;
import art.arcane.volmlib.nativelib.terrain.structure.StructureTerrainSettings;
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
import art.arcane.volmlib.util.collection.KList;
import net.minecraft.SharedConstants;
@@ -69,7 +70,8 @@ public class NativeStructureVolumeIndexTest {
StructureStart start = swampHut(0, 0, 4, 6);
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
KList<NativeStructureVolume> volumes = NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, start);
KList<NativeStructureVolume> volumes = NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, start,
StructureTerrainSettings.SOURCE, (x, z) -> 63);
assertEquals(1, volumes.size());
NativeStructureVolume volume = volumes.getFirst();
@@ -84,8 +86,10 @@ public class NativeStructureVolumeIndexTest {
@Test
public void invalidStartsContributeNoVolumes() {
assertNull(NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, StructureStart.INVALID_START));
assertNull(NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, null));
assertNull(NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, StructureStart.INVALID_START,
StructureTerrainSettings.SOURCE, (x, z) -> 63));
assertNull(NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, null,
StructureTerrainSettings.SOURCE, (x, z) -> 63));
}
@Test
@@ -323,7 +327,7 @@ public class NativeStructureVolumeIndexTest {
BlockingResolver coordination = new BlockingResolver();
NativeStructureVolumeIndex.OriginResolver volumes = (engine, chunkX, chunkZ) ->
new KList<>(new NativeStructureVolume(chunkX + ":" + chunkZ,
-256, 0, -256, 256, 255, 256));
-256, 0, -256, 256, 255, 256, null));
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting((engine, chunkX, chunkZ) -> {
coordination.volumesAt(engine, chunkX, chunkZ);
return volumes.volumesAt(engine, chunkX, chunkZ);
@@ -369,7 +373,7 @@ public class NativeStructureVolumeIndexTest {
terrain.join();
}
return new KList<>(new NativeStructureVolume(chunkX + ":" + chunkZ,
-256, 0, -256, 256, 100, 256));
-256, 0, -256, 256, 100, 256, null));
});
ExecutorService owner = Executors.newSingleThreadExecutor();
ForkJoinPool workers = new ForkJoinPool(2);
@@ -544,7 +548,8 @@ public class NativeStructureVolumeIndexTest {
if (chunkX != originChunkX || chunkZ != originChunkZ) {
return NativeStructureVolume.NONE;
}
return NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, swampHut(chunkX, chunkZ, 4, 6));
return NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, swampHut(chunkX, chunkZ, 4, 6),
StructureTerrainSettings.SOURCE, (x, z) -> 63);
}
}
@@ -596,7 +601,7 @@ public class NativeStructureVolumeIndexTest {
return new KList<>(new NativeStructureVolume(
chunkX + ":" + chunkZ,
0, 0, 0,
15, 255, 15));
15, 255, 15, null));
}
}
private record ScopedEngine(IrisEngine engine, AtomicInteger scopes) {
@@ -245,9 +245,15 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
for (int y = groundY + 1; y <= groundY + 32; y++) {
assertTrue("Roof at " + x + "," + y, state(blocks, x, y, 0).isAir());
}
for (int y = 12; y < groundY; y++) {
int supportY = x < 2 ? groundY - 33 : 12;
for (int y = supportY; y < groundY; y++) {
assertTrue("Missing foundation at " + x + "," + y, state(blocks, x, y, 0).isSolid());
}
if (x < 2) {
for (int y = 13; y < supportY; y++) {
assertTrue("Cave beneath support at " + x + "," + y, state(blocks, x, y, 0).isAir());
}
}
}
}
@@ -268,6 +274,57 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
assertTrue(state(blocks, 0, groundY + 1, 0).isAir());
}
@Test
public void flattenPreservesCavesBelowIntactGroundAtTheSkirtEdge() {
StructureStart start = desertStart();
int groundY = start.getBoundingBox().minY() - 1;
int x = start.getBoundingBox().maxX() + 23;
int z = start.getBoundingBox().minZ();
BoundingBox area = new BoundingBox(x, 0, z, x, 127, z);
Map<BlockPos, BlockState> blocks = new HashMap<>();
for (int y = groundY - 35; y <= groundY; y++) {
BlockState block = y >= groundY - 34 && y <= groundY - 13
? Blocks.CAVE_AIR.defaultBlockState() : Blocks.STONE.defaultBlockState();
put(blocks, x, y, z, block);
}
put(blocks, x, groundY - 1, z, Blocks.DIRT.defaultBlockState());
put(blocks, x, groundY, z, Blocks.GRASS_BLOCK.defaultBlockState());
NativeStructureTerrainIntegrator.TerrainTarget target = surfaceTarget(start, StructureTerrainMode.FLATTEN);
((IrisStructureTerrain) target.terrain()).setHorizontalPadding(24).setFlattenRange(64);
NativeStructureSurfaceFitter.prepareSurfaceStructures(world(blocks), area, List.of(target), (px, pz) -> groundY);
for (int y = groundY - 34; y <= groundY - 13; y++) {
assertEquals("Cave at " + y, Blocks.CAVE_AIR.defaultBlockState(), state(blocks, x, y, z));
}
assertEquals(Blocks.DIRT.defaultBlockState(), state(blocks, x, groundY - 1, z));
}
@Test
public void flattenBridgesTheFoundationGapWithoutFillingACaveBelowItsSupport() {
StructureStart start = desertStart();
int groundY = start.getBoundingBox().minY() - 1;
BoundingBox area = new BoundingBox(0, 0, 0, 0, 127, 0);
Map<BlockPos, BlockState> blocks = new HashMap<>();
put(blocks, 0, groundY, 0, Blocks.GRASS_BLOCK.defaultBlockState());
for (int y = groundY - 11; y <= groundY - 9; y++) {
put(blocks, 0, y, 0, Blocks.STONE.defaultBlockState());
}
for (int y = groundY - 20; y <= groundY - 12; y++) {
put(blocks, 0, y, 0, Blocks.CAVE_AIR.defaultBlockState());
}
NativeStructureSurfaceFitter.prepareSurfaceStructures(world(blocks), area,
List.of(surfaceTarget(start, StructureTerrainMode.FLATTEN)), (x, z) -> groundY);
for (int y = groundY - 8; y < groundY; y++) {
assertEquals("Foundation at " + y, Blocks.STONE.defaultBlockState(), state(blocks, 0, y, 0));
}
for (int y = groundY - 20; y <= groundY - 12; y++) {
assertEquals("Cave at " + y, Blocks.CAVE_AIR.defaultBlockState(), state(blocks, 0, y, 0));
}
}
@Test
public void flattenLeavesBuriedAndSubmergedInstancesUntouched() {
StructureStart start = desertStart();
@@ -619,10 +676,18 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
for (int z = 0; z <= 8; z++) {
int groundY = plan.flattenedHeights().get(NativeStructureTemplateOccupancy.columnKey(x, z)) - 1;
assertTrue("Village grade at " + x + "," + z + ": " + groundY, groundY >= 64 && groundY <= 70);
for (int y = 20; y <= groundY; y++) {
boolean originalFoundation = (x == 0 || x == 8) && (z == 0 || z == 8);
int supportY = originalFoundation ? 63 : 20;
for (int y = supportY; y <= groundY; y++) {
assertTrue("Disconnected village ground at " + x + "," + y + "," + z,
state(blocks, x, y, z).isSolid());
}
if (originalFoundation) {
for (int y = 21; y < supportY; y++) {
assertTrue("Cave beneath foundation at " + x + "," + y + "," + z,
state(blocks, x, y, z).isAir());
}
}
for (int y = groundY + 1; y <= 96; y++) {
assertTrue("Roof remains above village at " + x + "," + y + "," + z,
state(blocks, x, y, z).isAir());
@@ -9,6 +9,7 @@ import art.arcane.iris.structure.nativegen.NativeStructureVolumeIndex;
import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume;
import art.arcane.volmlib.nativelib.terrain.structure.StructureTerrainSettings;
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
import art.arcane.volmlib.util.collection.KList;
import net.minecraft.SharedConstants;
@@ -69,7 +70,8 @@ public class NativeStructureVolumeIndexTest {
StructureStart start = swampHut(0, 0, 4, 6);
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
KList<NativeStructureVolume> volumes = NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, start);
KList<NativeStructureVolume> volumes = NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, start,
StructureTerrainSettings.SOURCE, (x, z) -> 63);
assertEquals(1, volumes.size());
NativeStructureVolume volume = volumes.getFirst();
@@ -84,8 +86,10 @@ public class NativeStructureVolumeIndexTest {
@Test
public void invalidStartsContributeNoVolumes() {
assertNull(NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, StructureStart.INVALID_START));
assertNull(NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, null));
assertNull(NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, StructureStart.INVALID_START,
StructureTerrainSettings.SOURCE, (x, z) -> 63));
assertNull(NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, null,
StructureTerrainSettings.SOURCE, (x, z) -> 63));
}
@Test
@@ -323,7 +327,7 @@ public class NativeStructureVolumeIndexTest {
BlockingResolver coordination = new BlockingResolver();
NativeStructureVolumeIndex.OriginResolver volumes = (engine, chunkX, chunkZ) ->
new KList<>(new NativeStructureVolume(chunkX + ":" + chunkZ,
-256, 0, -256, 256, 255, 256));
-256, 0, -256, 256, 255, 256, null));
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting((engine, chunkX, chunkZ) -> {
coordination.volumesAt(engine, chunkX, chunkZ);
return volumes.volumesAt(engine, chunkX, chunkZ);
@@ -369,7 +373,7 @@ public class NativeStructureVolumeIndexTest {
terrain.join();
}
return new KList<>(new NativeStructureVolume(chunkX + ":" + chunkZ,
-256, 0, -256, 256, 100, 256));
-256, 0, -256, 256, 100, 256, null));
});
ExecutorService owner = Executors.newSingleThreadExecutor();
ForkJoinPool workers = new ForkJoinPool(2);
@@ -544,7 +548,8 @@ public class NativeStructureVolumeIndexTest {
if (chunkX != originChunkX || chunkZ != originChunkZ) {
return NativeStructureVolume.NONE;
}
return NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, swampHut(chunkX, chunkZ, 4, 6));
return NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, swampHut(chunkX, chunkZ, 4, 6),
StructureTerrainSettings.SOURCE, (x, z) -> 63);
}
}
@@ -596,7 +601,7 @@ public class NativeStructureVolumeIndexTest {
return new KList<>(new NativeStructureVolume(
chunkX + ":" + chunkZ,
0, 0, 0,
15, 255, 15));
15, 255, 15, null));
}
}
private record ScopedEngine(IrisEngine engine, AtomicInteger scopes) {
@@ -0,0 +1,61 @@
import { readFile } from 'node:fs/promises'
import path from 'node:path'
const world = 'native-terrain-support'
const air = new Set(['air', 'cave_air', 'void_air'])
export default {
name: 'iris-native-terrain-support',
description: 'Verify native terrain fitting preserves buried caves and excludes trees from lowered ground.',
async run(context) {
const bot = context.bot
const source = await readFile(path.join(context.server.directory, '.server-source'), 'utf8')
context.expect(/^isolated=true\s*$/m.test(source), 'Requires an isolated instance')
const position = bot.entity.position.clone()
const at = (x, y, z) => bot.blockAt(position.clone().set(x, y, z))
const teleport = async (x, y, z) => {
await context.command(`/minecraft:tp @s ${x + 0.5} ${y} ${z + 0.5}`, /Teleported/i, 60000)
await context.waitUntil(() => Math.abs(bot.entity.position.x - x - 0.5) < 1
&& Math.abs(bot.entity.position.z - z - 0.5) < 1 && at(x, y, z),
{ label: 'Coordinate chunk received', timeoutMs: 90000, intervalMs: 100 })
}
await context.step('enter the seeded overworld', async () => {
if (context.options.command !== 'reuse') {
await context.command(`/iris create name=${world} type=overworld seed=1337`,
/Successfully created your world/i, 240000)
}
await context.command(`/iris tp ${world}`)
await context.waitUntil(async () => {
const snapshot = await context.observe()
const player = snapshot.players.find(entry => entry.username === bot.username)
return snapshot.worlds.some(entry => entry.id === player?.world && entry.name.endsWith(world))
}, { label: 'Entered seeded overworld', timeoutMs: 90000, intervalMs: 100 })
await context.command('/minecraft:gamemode spectator @s', /Spectator Mode/i)
await context.command('/minecraft:seed', /Seed:.*1337/)
})
await context.step('preserve caves beneath the ruined portal terrain skirt', async () => {
await teleport(995, 44, -3611)
await context.waitUntil(() => at(990, 43, -3626) && at(992, 43, -3627),
{ label: 'Cave columns received', timeoutMs: 90000, intervalMs: 100 })
for (const [x, z, bottom, top] of [[990, -3626, 33, 54], [992, -3627, 34, 55]]) {
for (let y = bottom; y <= top; y++) {
const block = at(x, y, z)
context.expect(block && air.has(block.name), 'Native terrain fitting filled a buried cave',
{ x, y, z, material: block?.name })
}
await context.command(`/execute if block ${x} 43 ${z} minecraft:cave_air run say CAVE_PRESERVED_${x}`,
new RegExp(`CAVE_PRESERVED_${x}`))
}
})
await context.step('exclude the pine whose support is removed by village grading', async () => {
await teleport(-2601, 174, -7063)
await context.waitUntil(() => at(-2601, 167, -7063),
{ label: 'Pine column received', timeoutMs: 90000, intervalMs: 100 })
const block = at(-2601, 167, -7063)
context.expect(air.has(block.name), 'Tree remains above terrain removed by native grading',
{ x: -2601, y: 167, z: -7063, material: block.name })
await context.command('/execute if block -2601 167 -7063 minecraft:air run say PINE_EXCLUDED',
/PINE_EXCLUDED/)
})
}
}
@@ -0,0 +1,20 @@
{
"name": "Flat",
"derivative": "minecraft:plains",
"vanillaDerivative": "minecraft:plains",
"layers": [
{"minHeight": 1, "maxHeight": 1, "palette": [{"block": "minecraft:lime_wool"}]},
{"minHeight": 128, "maxHeight": 128, "palette": [{"block": "minecraft:stone"}]}
],
"generators": [{"generator": "flat", "min": 32, "max": 32}],
"objects": [{
"place": ["smolshroom1"],
"chance": 1,
"density": 8,
"mode": "ORGANIC_STILT",
"rotation": {"enabled": false},
"scale": {"size": 0.75, "interpolation": "NONE"},
"translate": {"y": 6},
"stiltSettings": {"organicMaxScan": 8, "organicJitter": 0, "organicScratch": 0}
}]
}
@@ -0,0 +1,15 @@
{
"name": "Object Scaling Fixture",
"version": 1,
"mode": {"type": "OVERWORLD"},
"regions": ["flat"],
"focusRegion": "flat",
"focus": "flat",
"landChance": 1,
"environment": "NORMAL",
"dimensionHeight": {"min": 0, "max": 128},
"logicalHeight": 128,
"fluidHeight": 0,
"bedrock": false,
"caveProfile": {"enabled": false}
}
@@ -0,0 +1,5 @@
{
"seed": 310,
"interpolator": {"function": "NONE", "horizontalScale": 1},
"composite": [{"seed": 310, "style": {"style": "FLAT"}}]
}
@@ -0,0 +1,6 @@
{
"name": "Flat",
"landBiomes": ["flat"],
"seaBiomes": ["flat"],
"shoreBiomes": ["flat"]
}
@@ -0,0 +1,105 @@
import { createHash } from 'node:crypto'
import { readFile, realpath } from 'node:fs/promises'
import path from 'node:path'
export default {
name: 'iris-object-scaling',
description: 'Verify fractional mushroom scaling retains its cap and uses its stem for organic supports.',
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), 'Object scaling requires an isolated instance')
const pack = await realpath(path.join(directory, 'plugins', 'Iris', 'packs', 'object-scaling-fixture'))
context.expect(pack.startsWith(`${directory}${path.sep}`), 'Object scaling requires a local fixture pack')
const object = await readFile(path.join(pack, 'objects', 'smolshroom1.iob'))
const objectHash = createHash('sha256').update(object).digest('hex')
const biome = JSON.parse(await readFile(path.join(pack, 'biomes', 'flat.json'), 'utf8'))
const surfaceY = biome.generators[0].min
const originalStemY = surfaceY + biome.objects[0].translate.y
const expectedCapY = originalStemY + 1
const maximumExtension = biome.objects[0].stiltSettings.organicMaxScan
context.expect(surfaceY === 32 && expectedCapY === 39 && maximumExtension === 8,
'Expected the bounded organic support fixture', { surfaceY, expectedCapY, maximumExtension })
const world = context.options.command ?? 'object_scaling_probe'
context.expect(/^[a-z0-9_]+$/.test(world), 'Fixture world name must be a simple identifier')
await context.step('create the fractional mushroom fixture world', async () => {
const response = await context.command(`/iris create name=${world} type=object-scaling-fixture seed=1337`,
/Successfully created your world|World ready|Failed to create|Exception raised/i, 180000)
context.expect(/Successfully created your world|World ready/i.test(response), response)
await context.command(`/iris tp ${world}`)
await context.waitUntil(async () => {
const observation = await context.observe()
const player = observation.players.find(entry => entry.username === context.bot.username)
const current = observation.worlds.find(entry => entry.id === player?.world)
return current?.key === `iris:${world}` || current?.name.endsWith(world)
}, { label: 'Enter the scaling fixture world', timeoutMs: 60000, intervalMs: 100 })
await context.command('/minecraft:gamemode spectator @s')
await context.waitUntil(() => context.bot.game.gameMode === 'spectator',
{ label: 'Spectator mode', timeoutMs: 30000, intervalMs: 100 })
await context.command('/minecraft:tp @s 0.5 48 0.5', /Teleported/i, 30000)
})
const columns = []
await context.step('client sees one cap and continuous stem supports', async () => {
const position = context.bot.entity.position.clone()
await context.waitUntil(() => {
if (Math.abs(context.bot.entity.position.x - 0.5) > 1
|| Math.abs(context.bot.entity.position.z - 0.5) > 1) return false
for (const x of [-16, 0, 16, 31]) {
for (const z of [-16, 0, 16, 31]) {
for (const y of [0, 32, 63]) {
if (!context.bot.blockAt(position.set(x, y, z))) return false
}
}
}
return true
}, { label: 'Complete mushroom fixture chunks', timeoutMs: 60000, intervalMs: 100 })
for (let x = -16; x < 32; x++) {
for (let z = -16; z < 32; z++) {
const caps = []
const stems = []
for (let y = 1; y < 63; y++) {
const block = context.bot.blockAt(position.set(x, y, z))
context.expect(block !== null, 'Every sampled mushroom block must be loaded', { x, y, z })
if (block.name === 'red_mushroom_block') caps.push(y)
if (block.name === 'mushroom_stem') stems.push(y)
}
if (caps.length === 0) continue
context.expect(caps.length === 1, 'Mushroom cap was copied into its stem or support', { x, z, caps })
const capY = caps[0]
context.expect(capY === expectedCapY, 'Fractional scaling changed the expected mushroom height',
{ x, z, capY, expectedCapY })
const bottomY = stems[0]
const stemBlocks = stems.length
context.expect(bottomY >= originalStemY - maximumExtension && bottomY <= surfaceY + 1,
'Mushroom support must reach the surface within its configured extension limit',
{ x, z, capY, bottomY, surfaceY, maximumExtension, stems })
context.expect(stemBlocks === capY - bottomY
&& stems.every((y, index) => y === bottomY + index),
'Scaled mushroom must have a continuous stem from its cap to the ground', { x, z, capY, stems })
const supportY = bottomY - 1
const support = context.bot.blockAt(position.set(x, supportY, z))?.name
const above = context.bot.blockAt(position.set(x, capY + 1, z))?.name
context.expect(support === (supportY === surfaceY ? 'lime_wool' : 'stone'),
'Mushroom stem must terminate on the fixture terrain', { x, z, capY, supportY, support })
context.expect(above === 'air', 'Scaled mushroom has unexpected blocks above its cap', { x, z, capY, above })
columns.push({ x, z, surfaceY, capY, bottomY, stemBlocks,
buriedStemBlocks: Math.max(0, surfaceY - bottomY + 1) })
}
}
context.expect(columns.length >= 8, 'Expected at least eight scaled mushroom columns', { columns })
})
await context.step('server confirms the sampled stem and cap materials', async () => {
for (const [index, column] of columns.slice(0, 8).entries()) {
const { x, z, surfaceY, capY, bottomY } = column
const marker = `IRIS_SCALING_OK_${index}`
await context.command(`/minecraft:execute if block ${x} ${surfaceY + 1} ${z} minecraft:mushroom_stem`
+ ` if block ${x} ${bottomY} ${z} minecraft:mushroom_stem`
+ ` if block ${x} ${capY} ${z} minecraft:red_mushroom_block run minecraft:say ${marker}`,
new RegExp(marker), 10000)
}
})
context.report.objectScaling = { world, scale: 0.75, object: 'smolshroom1', objectHash,
surfaceY, maximumExtension, checkedColumns: columns.length,
serverConfirmedColumns: Math.min(8, columns.length), columns }
}
}
@@ -0,0 +1,82 @@
import { createHash } from 'node:crypto'
import { readFile, writeFile } from 'node:fs/promises'
import path from 'node:path'
const worlds = [
{ name: 'seed-selection-default', seed: '1337', argument: '' },
{ name: 'seed-selection-positive', seed: '78264193', argument: ' seed=78264193' },
{ name: 'seed-selection-negative', seed: '-734829104958217364', argument: ' seed=-734829104958217364' },
{ name: 'seed-selection-zero', seed: '0', argument: ' seed=0' }
]
export default {
name: 'iris-seed-selection',
description: 'Verify omitted, positive, negative, and zero creation seeds against visible terrain and a cold restart.',
async run(context) {
const source = await readFile(path.join(context.server.directory, '.server-source'), 'utf8')
context.expect(/^isolated=true\s*$/m.test(source), 'Seed selection requires an isolated instance')
const wait = (predicate, label) => context.waitUntil(predicate, { label, timeoutMs: 60000, intervalMs: 100 })
const restart = context.options.command === 'restart-check'
const stateFile = path.join(context.server.directory, '.seed-selection.json')
const saved = restart ? JSON.parse(await readFile(stateFile, 'utf8')) : undefined
const jarHash = createHash('sha256').update(await readFile(path.join(context.server.directory, 'plugins', 'Iris.jar'))).digest('hex')
const processId = (await context.observe()).processId
if (restart) {
context.expect(saved.processId !== processId, 'Restart-check requires a cold server restart')
context.expect(saved.jarHash === jarHash, 'Restart-check requires the same Iris jar')
}
const outputs = {}
for (const world of worlds) {
await context.step(`${restart ? 'Reload' : 'Create'} ${world.name} with seed ${world.seed}`, async () => {
if (!restart) {
await context.command(`/iris create name=${world.name} type=cohesion${world.argument}`,
/Successfully created your world/i, 180000)
}
await context.command(`/iris tp ${world.name}`)
await wait(async () => {
const observation = await context.observe()
const player = observation.players.find(entry => entry.username === context.bot.username)
const current = observation.worlds.find(entry => entry.id === player?.world)
return current?.key === `iris:${world.name}` || current?.name.endsWith(world.name)
}, `Enter ${world.name}`)
await context.command('/minecraft:gamemode spectator @s')
await wait(() => context.bot.game.gameMode === 'spectator', 'Spectator mode')
await context.command('/minecraft:seed', new RegExp(`Seed:\\s*\\[?${world.seed}\\]?(?:\\s|$)`))
const x = 1024
const z = -1024
await context.command(`/minecraft:tp @s ${x + 8.5} 48 ${z + 8.5}`, /Teleported/i, 30000)
await wait(() => Math.abs(context.bot.entity.position.x - x - 8.5) < 1
&& Math.abs(context.bot.entity.position.z - z - 8.5) < 1, 'Sample teleport')
const position = context.bot.entity.position.clone()
await wait(() => context.bot.blockAt(position.clone().set(x, 0, z))
&& context.bot.blockAt(position.clone().set(x + 15, 63, z + 15)), 'Complete sample chunk')
const digest = createHash('sha256')
let stone = 0
for (let dx = 0; dx < 16; dx++) {
for (let dz = 0; dz < 16; dz++) {
for (let y = 0; y < 64; y++) {
const block = context.bot.blockAt(position.set(x + dx, y, z + dz))
context.expect(block !== null, 'Missing sampled block', { x: x + dx, y, z: z + dz })
digest.update(`${block.stateId},`)
if (block.name === 'stone') stone++
}
}
}
context.expect(stone > 1000, 'Fixture terrain was not generated', { world: world.name, stone })
const blocks = digest.digest('hex')
outputs[world.name] = { seed: world.seed, blocks }
if (restart) {
context.expect(saved.worlds[world.name].seed === world.seed, 'Seed expectation changed after restart')
context.expect(saved.worlds[world.name].blocks === blocks, 'Persisted terrain changed after restart',
{ world: world.name, before: saved.worlds[world.name].blocks, after: blocks })
}
})
}
const signatures = new Set(Object.values(outputs).map(output => output.blocks))
context.expect(signatures.size === worlds.length, 'Different requested seeds generated identical sample chunks', outputs)
context.report.seedSelection = { restart, jarHash, worlds: outputs }
if (!restart) {
await writeFile(stateFile, JSON.stringify({ processId, jarHash, worlds: outputs }))
}
}
}
@@ -316,6 +316,11 @@ public class IrisCaveCarver3D {
for (int lz = 0; lz < 16; lz++) {
int z = z0 + lz;
int index = PowerOfTwoCoordinates.packLocal16(lx, lz);
clampedWeights[index] = clampColumnWeight(columnWeights[index]);
if (clampedWeights[index] <= resolvedMinWeight) {
columnMaxY[index] = minY - 1;
continue;
}
int columnSurfaceY;
if (precomputedSurfaceHeights != null && precomputedSurfaceHeights.length > index) {
columnSurfaceY = precomputedSurfaceHeights[index];
@@ -340,7 +345,6 @@ public class IrisCaveCarver3D {
columnThreshold[index] = (thresholdDensity == null
? constantThreshold
: thresholdDensity.fitDouble(thresholdMin, thresholdMax, x, z)) - thresholdBias;
clampedWeights[index] = clampColumnWeight(columnWeights[index]);
}
}
@@ -56,7 +56,6 @@ public class MantleCarvingComponent extends IrisMantleComponent {
private static final int FIELD_SIZE = CHUNK_SIZE + (BLEND_RADIUS * 2);
private static final double MIN_WEIGHT = 0.08D;
private static final double THRESHOLD_PENALTY = 0.24D;
private static final int MAX_BLENDED_PROFILE_PASSES = 2;
private static final int MAX_POOLED_WEIGHT_BUFFERS = 64;
/** Lattice columns kept for neighbouring chunks: about 3 KB each on land, roughly 50 MB when full. */
private static final int DENSITY_GRID_COLUMNS = 16_384;
@@ -348,9 +347,9 @@ public class MantleCarvingComponent extends IrisMantleComponent {
columnWeightedProfiles.add(new WeightedProfile(profile, weights, averageWeight, null, columnWeightedProfiles.size()));
}
List<WeightedProfile> blendedProfiles = limitAndMergeBlendedProfiles(columnWeightedProfiles, MAX_BLENDED_PROFILE_PASSES, CHUNK_AREA);
columnWeightedProfiles.sort(MantleCarvingComponent::compareByCarveOrder);
List<WeightedProfile> resolvedProfiles = resolveDimensionCarvingProfiles(chunkX, chunkZ, resolverState, blendScratch);
resolvedProfiles.addAll(blendedProfiles);
resolvedProfiles.addAll(columnWeightedProfiles);
return resolvedProfiles;
}
@@ -619,67 +618,6 @@ public class MantleCarvingComponent extends IrisMantleComponent {
return surfaceHeights;
}
private static List<WeightedProfile> limitAndMergeBlendedProfiles(List<WeightedProfile> blendedProfiles, int maxProfiles) {
return limitAndMergeBlendedProfiles(blendedProfiles, maxProfiles, CHUNK_AREA);
}
private static List<WeightedProfile> limitAndMergeBlendedProfiles(List<WeightedProfile> blendedProfiles, int maxProfiles, int areaSize) {
if (blendedProfiles == null || blendedProfiles.isEmpty()) {
return new ArrayList<>();
}
int clampedLimit = Math.max(1, maxProfiles);
List<WeightedProfile> rankedProfiles = new ArrayList<>(blendedProfiles);
rankedProfiles.sort(MantleCarvingComponent::compareBySelectionRank);
List<WeightedProfile> keptProfiles = new ArrayList<>();
int keptCount = Math.min(clampedLimit, rankedProfiles.size());
for (int index = 0; index < keptCount; index++) {
keptProfiles.add(rankedProfiles.get(index));
}
if (rankedProfiles.size() > keptCount) {
for (int columnIndex = 0; columnIndex < areaSize; columnIndex++) {
int dominantIndex = 0;
double dominantWeight = Double.NEGATIVE_INFINITY;
for (int keptIndex = 0; keptIndex < keptProfiles.size(); keptIndex++) {
double keptWeight = keptProfiles.get(keptIndex).columnWeights[columnIndex];
if (keptWeight > dominantWeight) {
dominantWeight = keptWeight;
dominantIndex = keptIndex;
}
}
double droppedWeight = 0D;
for (int droppedIndex = keptCount; droppedIndex < rankedProfiles.size(); droppedIndex++) {
droppedWeight += rankedProfiles.get(droppedIndex).columnWeights[columnIndex];
}
if (droppedWeight <= 0D) {
continue;
}
WeightedProfile dominantProfile = keptProfiles.get(dominantIndex);
double mergedWeight = dominantProfile.columnWeights[columnIndex] + droppedWeight;
dominantProfile.columnWeights[columnIndex] = clampWeight(mergedWeight);
}
}
List<WeightedProfile> mergedProfiles = new ArrayList<>();
for (WeightedProfile keptProfile : keptProfiles) {
double averageWeight = computeAverageWeight(keptProfile.columnWeights, areaSize);
mergedProfiles.add(new WeightedProfile(keptProfile.profile, keptProfile.columnWeights, averageWeight, keptProfile.worldYRange, keptProfile.sequence));
}
mergedProfiles.sort(MantleCarvingComponent::compareByCarveOrder);
return mergedProfiles;
}
private static int compareBySelectionRank(WeightedProfile a, WeightedProfile b) {
int weightOrder = Double.compare(b.averageWeight, a.averageWeight);
if (weightOrder != 0) {
return weightOrder;
}
return Integer.compare(a.sequence, b.sequence);
}
private static int compareByCarveOrder(WeightedProfile a, WeightedProfile b) {
int weightOrder = Double.compare(a.averageWeight, b.averageWeight);
if (weightOrder != 0) {
@@ -689,10 +627,6 @@ public class MantleCarvingComponent extends IrisMantleComponent {
}
private static double computeAverageWeight(double[] weights) {
return computeAverageWeight(weights, CHUNK_AREA);
}
private static double computeAverageWeight(double[] weights, int areaSize) {
if (weights == null || weights.length == 0) {
return 0D;
}
@@ -700,7 +634,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
for (double weight : weights) {
sum += weight;
}
return sum / Math.max(1, areaSize);
return sum / CHUNK_AREA;
}
private static double clampWeight(double value) {
@@ -730,10 +664,6 @@ public class MantleCarvingComponent extends IrisMantleComponent {
this.worldYRange = worldYRange;
this.sequence = sequence;
}
private double averageWeight() {
return averageWeight;
}
}
private static final class BlendScratch {
@@ -930,7 +930,7 @@ public class IrisEngine implements Engine {
queryMaximumX,
queryMaximumZ);
for (NativeStructureVolume volume : resolved) {
if (!volume.intersectsRect(minimumX, minimumZ, maximumX, maximumZ)) {
if (!volume.intersectsInfluenceRect(minimumX, minimumZ, maximumX, maximumZ)) {
continue;
}
if (matches == null) {
@@ -115,7 +115,7 @@ public final class NativeStructureVolumeIndex {
for (int chunkX = fromChunkX; chunkX <= toChunkX; chunkX++) {
for (int chunkZ = fromChunkZ; chunkZ <= toChunkZ; chunkZ++) {
for (NativeStructureVolume volume : chunkVolumes(engine, runtimeId, chunkX, chunkZ)) {
if (!volume.intersectsRect(minX, minZ, maxX, maxZ)) {
if (!volume.intersectsInfluenceRect(minX, minZ, maxX, maxZ)) {
continue;
}
if (matches == null) {
@@ -150,7 +150,7 @@ public final class NativeStructureVolumeIndex {
for (int originX = chunkX - ORIGIN_REACH_CHUNKS; originX <= chunkX + ORIGIN_REACH_CHUNKS; originX++) {
for (int originZ = chunkZ - ORIGIN_REACH_CHUNKS; originZ <= chunkZ + ORIGIN_REACH_CHUNKS; originZ++) {
for (NativeStructureVolume volume : scopedOriginVolumes(engine, originX, originZ, pinnedRuntimeId)) {
if (!volume.intersectsRect(minX, minZ, maxX, maxZ)) {
if (!volume.intersectsInfluenceRect(minX, minZ, maxX, maxZ)) {
continue;
}
if (volumes == null) {
@@ -54,7 +54,7 @@ public final class NativeStructureVolumeMemo {
for (int chunkX = fromChunkX; chunkX <= toChunkX; chunkX++) {
for (int chunkZ = fromChunkZ; chunkZ <= toChunkZ; chunkZ++) {
for (NativeStructureVolume volume : chunkVolumes(engine, hooks, chunkX, chunkZ)) {
if (!volume.intersectsRect(minX, minZ, maxX, maxZ)) {
if (!volume.intersectsInfluenceRect(minX, minZ, maxX, maxZ)) {
continue;
}
if (matches == null) {
@@ -49,6 +49,7 @@ import art.arcane.volmlib.util.math.BlockPosition;
import art.arcane.volmlib.util.math.Position2;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.matter.MatterMarker;
import it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap;
import java.util.Comparator;
import java.util.Objects;
@@ -1135,10 +1136,14 @@ final class IrisObjectPlacementRunner {
int envelopeMinY = projection != null ? Integer.MIN_VALUE : worldY + bounds.minY() - warpMargin;
int envelopeMaxY = projection != null ? Integer.MAX_VALUE : worldY + bounds.maxY() + warpMargin;
boolean envelopeMeetsPiece = false;
boolean includesSurfaceCuts = false;
for (NativeStructureVolume volume : volumes) {
if (volume.intersects(minX, envelopeMinY, minZ, maxX, envelopeMaxY, maxZ)) {
envelopeMeetsPiece = true;
break;
}
if (volume.surfaceAdjustment() != null && volume.intersectsInfluenceRect(minX, minZ, maxX, maxZ)) {
envelopeMeetsPiece = true;
includesSurfaceCuts = true;
}
}
@@ -1148,6 +1153,7 @@ final class IrisObjectPlacementRunner {
self.readLock.lock();
try {
Long2LongOpenHashMap surfaceCuts = null;
VectorMap<NativeBlockState>.Cursor cursor = self.blocks.cursor();
while (cursor.next()) {
NativeBlockState state = cursor.value();
@@ -1187,6 +1193,28 @@ final class IrisObjectPlacementRunner {
return true;
}
}
if (!includesSurfaceCuts) {
continue;
}
if (surfaceCuts == null) {
surfaceCuts = new Long2LongOpenHashMap();
surfaceCuts.defaultReturnValue(Long.MIN_VALUE);
}
long column = ((long) xx << 32) ^ (zz & 0xffffffffL);
long cut = surfaceCuts.get(column);
if (cut == Long.MIN_VALUE) {
int surfaceY = Engine.hostHeight(engine, xx, zz, true) + engine.getMinHeight();
int targetY = NativeStructureVolume.surfaceCutY(volumes, xx, zz, surfaceY);
cut = ((long) surfaceY << 32) ^ (targetY & 0xffffffffL);
surfaceCuts.put(column, cut);
}
int surfaceY = (int) (cut >> 32);
int targetY = (int) cut;
int margin = projection == null ? warpMargin : 0;
if (targetY < surfaceY && worldBlockY > (long) targetY - margin
&& worldBlockY <= (long) surfaceY + 1 + margin) {
return true;
}
}
} finally {
self.readLock.unlock();
@@ -124,6 +124,7 @@ final class IrisObjectTransforms {
interpolation = IrisObjectPlacementScaleInterpolator.NONE;
}
boolean savedOrigin = origin == ScaleOrigin.SAVED;
boolean fractional = scale != Math.rint(scale);
List<ScaledVoxel> voxels;
IrisObject output;
TreeGrounding grounding;
@@ -148,26 +149,33 @@ final class IrisObjectTransforms {
sourcePosition.getBlockZ() + sourceCenter.getZ());
IrisBlockVector minimum = scaledMinimum(position, scale);
IrisBlockVector maximum = scaledMaximum(position, scale);
IrisVector corner = fractional ? new IrisVector(position.getX() * scale,
position.getY() * scale, position.getZ() * scale) : minimum;
if (!savedOrigin) {
minimum.subtract(targetCenter);
maximum.subtract(targetCenter);
if (fractional) {
corner.subtract(targetCenter);
}
}
TileData tile = self.states.get(sourcePosition);
voxels.add(new ScaledVoxel(sourcePosition.clone(), minimum, maximum, entry.getValue(),
voxels.add(new ScaledVoxel(sourcePosition.clone(), minimum, maximum, corner, entry.getValue(),
tile == null ? null : tile.clone(), grounding != null && grounding.isCanopy(
sourcePosition.getBlockX(), sourcePosition.getBlockY(), sourcePosition.getBlockZ())));
}
} finally {
self.readLock.unlock();
}
if (scale != Math.rint(scale)) {
Set<TreeBlockCanvas.Vec> canopy = grounding == null ? null : new HashSet<>();
if (fractional) {
voxels.sort(Comparator.comparingInt((ScaledVoxel voxel) -> voxel.source().getBlockX())
.thenComparingInt(voxel -> voxel.source().getBlockY())
.thenComparingInt(voxel -> voxel.source().getBlockZ()));
}
Set<TreeBlockCanvas.Vec> canopy = grounding == null ? null : new HashSet<>();
for (ScaledVoxel voxel : voxels) {
writeScaledVoxel(output, voxel, canopy);
writeResampledVoxels(output, voxels, scale, canopy);
} else {
for (ScaledVoxel voxel : voxels) {
writeScaledVoxel(output, voxel, canopy);
}
}
VectorMap<NativeBlockState> scaledBlocks = output.blocks;
@@ -206,7 +214,7 @@ final class IrisObjectTransforms {
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) {
if (state != null && !canopy.contains(new TreeBlockCanvas.Vec(x, y, z))) {
roots.add(new TreeGrounding.Root(x, y, z, state));
}
}
@@ -225,23 +233,53 @@ final class IrisObjectTransforms {
position.setZ(z);
for (int y = minimum.getBlockY(); y <= maximum.getBlockY(); y++) {
position.setY(y);
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);
writeScaledBlock(output, position, voxel, canopy);
}
}
}
}
private static void writeResampledVoxels(IrisObject output, List<ScaledVoxel> voxels, double scale,
Set<TreeBlockCanvas.Vec> canopy) {
VectorMap<ScaledVoxel> samples = new VectorMap<>();
IrisBlockVector position = new IrisBlockVector();
for (ScaledVoxel voxel : voxels) {
for (int x = voxel.minimum().getBlockX(); x <= voxel.maximum().getBlockX(); x++) {
position.setX(x);
for (int z = voxel.minimum().getBlockZ(); z <= voxel.maximum().getBlockZ(); z++) {
position.setZ(z);
for (int y = voxel.minimum().getBlockY(); y <= voxel.maximum().getBlockY(); y++) {
position.setY(y);
ScaledVoxel previous = samples.get(position);
if (previous == null || voxel.overlap(position, scale) >= previous.overlap(position, scale)) {
samples.put(position, voxel);
}
}
if (voxel.tile() == null) {
output.states.remove(position);
} else {
output.states.put(position, voxel.tile().clone());
}
}
}
}
for (Map.Entry<IrisBlockVector, ScaledVoxel> entry : samples) {
writeScaledBlock(output, entry.getKey(), entry.getValue(), canopy);
}
}
private static void writeScaledBlock(IrisObject output, IrisBlockVector position, ScaledVoxel voxel,
Set<TreeBlockCanvas.Vec> canopy) {
output.blocks.put(position, voxel.block());
if (canopy != null) {
TreeBlockCanvas.Vec cell = new TreeBlockCanvas.Vec(position.getBlockX(),
position.getBlockY(), position.getBlockZ());
if (voxel.canopy()) {
canopy.add(cell);
} else {
canopy.remove(cell);
}
}
if (voxel.tile() == null) {
output.states.remove(position);
} else {
output.states.put(position, voxel.tile().clone());
}
}
private static IrisObject createCenteredScaledObject(IrisObject source, double scale) {
@@ -541,7 +579,16 @@ final class IrisObjectTransforms {
}
private record ScaledVoxel(IrisBlockVector source, IrisBlockVector minimum, IrisBlockVector maximum,
NativeBlockState block, TileData tile, boolean canopy) {
IrisVector corner, NativeBlockState block, TileData tile, boolean canopy) {
private double overlap(IrisBlockVector position, double scale) {
return overlap(corner.getX(), position.getX(), scale)
* overlap(corner.getY(), position.getY(), scale)
* overlap(corner.getZ(), position.getZ(), scale);
}
private static double overlap(double source, double target, double scale) {
return Math.max(0D, Math.min(source + scale, target + 1D) - Math.max(source, target));
}
}
private enum ScaleOrigin {
@@ -1,17 +1,26 @@
package art.arcane.iris.generation.chunk;
import art.arcane.iris.platform.bukkit.nms.INMS;
import art.arcane.iris.platform.bukkit.nms.INMSBinding;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import org.bukkit.block.data.BlockData;
import org.bukkit.generator.ChunkGenerator;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
public class ChunkDataHunkHolderTest {
@@ -90,6 +99,53 @@ public class ChunkDataHunkHolderTest {
}
}
@Test
public void fallbackAppliesExplicitCaveAirBetweenSolidRunsWithNegativeMinimumHeight() {
ChunkGenerator.ChunkData chunk = mock(ChunkGenerator.ChunkData.class);
when(chunk.getMinHeight()).thenReturn(-256);
when(chunk.getMaxHeight()).thenReturn(512);
ChunkDataHunkHolder holder = new ChunkDataHunkHolder(chunk);
BlockData dirt = mock(BlockData.class);
BlockData coal = mock(BlockData.class);
BlockData caveAir = mock(BlockData.class);
NativeBlockState dirtState = state(dirt);
NativeBlockState coalState = state(coal);
NativeBlockState caveAirState = state(caveAir);
BlockData[] applied = new BlockData[768];
Arrays.fill(applied, dirt);
for (int y = 0; y <= 75; y++) {
holder.setRaw(14, y + 256, 6, y < 33 ? dirtState : y <= 54 ? caveAirState : coalState);
}
doAnswer(call -> {
int y = call.getArgument(1);
applied[y + 256] = call.getArgument(3);
return null;
}).when(chunk).setBlock(anyInt(), anyInt(), anyInt(), any(BlockData.class));
doAnswer(call -> {
int from = call.getArgument(1);
int to = call.getArgument(4);
BlockData block = call.getArgument(6);
Arrays.fill(applied, from + 256, to + 256, block);
return null;
}).when(chunk).setRegion(anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), any(BlockData.class));
try (MockedStatic<INMS> nms = mockStatic(INMS.class)) {
nms.when(INMS::get).thenReturn(mock(INMSBinding.class));
holder.apply();
}
for (int y = -256; y < 512; y++) {
BlockData expected = y >= 33 && y <= 54 ? caveAir : y >= 55 && y <= 75 ? coal : dirt;
assertSame("column at " + y, expected, applied[y + 256]);
}
}
private static NativeBlockState state(BlockData data) {
NativeBlockState state = mock(NativeBlockState.class);
when(state.nativeHandle()).thenReturn(data);
return state;
}
private static ChunkDataHunkHolder holder(int height) {
ChunkGenerator.ChunkData chunk = mock(ChunkGenerator.ChunkData.class);
when(chunk.getMinHeight()).thenReturn(0);
@@ -231,6 +231,53 @@ public class IrisCaveCarver3DNearParityTest {
}
}
@Test
public void sparseProfilesSampleOnlyActiveColumnsAndDiscardPreviousPassColumns() {
for (int sampleStep : new int[]{1, 2, 3}) {
for (boolean adaptive : new boolean[]{false, true}) {
Engine engine = createEngine(128, 110);
AtomicInteger heightSamples = new AtomicInteger();
doAnswer(invocation -> {
heightSamples.incrementAndGet();
return 110;
}).when(engine).getHeight(anyInt(), anyInt(), anyBoolean());
IrisCaveProfile profile = createProfile(false, false)
.setSampleStep(sampleStep).setAdaptiveSampling(adaptive)
.setAllowFluid(false).setAllowSurfaceBreak(false)
.setDensityThreshold(new IrisStyledRange(2D, 2D, new IrisGeneratorStyle(NoiseStyle.FLAT)));
IrisCaveCarver3D carver = new IrisCaveCarver3D(engine, profile);
double[] weights = new double[256];
weights[0] = 1D;
weights[255] = 1D;
WriterCapture first = createWriterCapture(128);
carver.carve(first.writer, 0, 0, weights, 0.08D, 0.24D);
assertEquals(2, heightSamples.get());
assertFalse(first.carvedCells.isEmpty());
for (String cell : first.carvedCells) {
int x = coordinate(cell, 0);
int z = coordinate(cell, 2);
assertTrue(x == 0 && z == 0 || x == 15 && z == 15);
}
Arrays.fill(weights, 0D);
weights[(7 << 4) | 3] = 1D;
heightSamples.set(0);
WriterCapture second = createWriterCapture(128);
carver.carve(second.writer, 0, 0, weights, 0.08D, 0.24D);
assertEquals(1, heightSamples.get());
assertFalse(second.carvedCells.isEmpty());
for (String cell : second.carvedCells) {
assertEquals(7, coordinate(cell, 0));
assertEquals(3, coordinate(cell, 2));
}
}
}
}
@Test
public void verticalDensityPreservesTwoBlockModuleTunnelsWithoutBleedingAcrossBands() throws Exception {
for (int step : new int[]{2, 4, 8}) {
@@ -0,0 +1,173 @@
package art.arcane.iris.generation.mantle;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.cave.IrisCaveProfile;
import art.arcane.iris.generation.context.ChunkContext;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.EngineMetrics;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.world.IrisWorld;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.stream.ProceduralStream;
import org.junit.Test;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class MantleCarvingComponentJunctionTest {
@Test
public void twoCaveProfilesBlendAcrossBothSidesOfAChunkBoundary() throws Exception {
Fixture fixture = fixture(false);
fixture.generate(false);
assertTrue(fixture.weight(-1, fixture.west(), 15, 12) > 0D);
assertTrue(fixture.weight(-1, fixture.east(), 15, 12) > 0D);
assertTrue(fixture.weight(0, fixture.west(), 0, 12) > 0D);
assertTrue(fixture.weight(0, fixture.east(), 0, 12) > 0D);
assertEquals(fixture.weight(-1, fixture.west(), 15, 12),
fixture.weight(0, fixture.east(), 0, 12), 0D);
assertEquals(fixture.weight(-1, fixture.east(), 15, 12),
fixture.weight(0, fixture.west(), 0, 12), 0D);
}
@Test
public void thirdProfileOutsideTheBlendRadiusDoesNotChangeBoundaryWeights() throws Exception {
Fixture control = fixture(false);
Fixture junction = fixture(true);
control.generate(false);
junction.generate(false);
assertEquals("A third biome outside the local blend radius must not remove the east cave from the west chunk",
control.weight(-1, control.east(), 15, 12),
junction.weight(-1, junction.east(), 15, 12), 0D);
assertEquals("A third biome outside the local blend radius must not remove the west cave from the east chunk",
control.weight(0, control.west(), 0, 12),
junction.weight(0, junction.west(), 0, 12), 0D);
}
@Test
public void threeWayJunctionRetainsEachProfileOnlyInsideItsLocalBlend() throws Exception {
Fixture fixture = fixture(true);
fixture.generate(false);
for (int chunkX : new int[]{-1, 0}) {
int boundaryX = chunkX == -1 ? 15 : 0;
assertEquals(3, fixture.captured().get(chunkX).size());
assertTrue(fixture.weight(chunkX, fixture.west(), boundaryX, 6) > 0D);
assertTrue(fixture.weight(chunkX, fixture.east(), boundaryX, 6) > 0D);
assertTrue(fixture.weight(chunkX, fixture.north(), boundaryX, 6) > 0D);
assertEquals(0D, fixture.weight(chunkX, fixture.north(), boundaryX, 12), 0D);
assertEquals(0D, fixture.weight(chunkX, fixture.west(), boundaryX, 0), 0D);
assertEquals(0D, fixture.weight(chunkX, fixture.east(), boundaryX, 0), 0D);
assertEquals(1D, fixture.weight(chunkX, fixture.north(), boundaryX, 0), 0D);
}
}
@Test
public void junctionWeightsRemainStableAcrossGenerationOrder() throws Exception {
Fixture forward = fixture(true);
Fixture reverse = fixture(true);
forward.generate(false);
reverse.generate(true);
for (int chunkX : new int[]{-1, 0}) {
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
assertEquals(forward.weight(chunkX, forward.west(), x, z),
reverse.weight(chunkX, reverse.west(), x, z), 0D);
assertEquals(forward.weight(chunkX, forward.east(), x, z),
reverse.weight(chunkX, reverse.east(), x, z), 0D);
assertEquals(forward.weight(chunkX, forward.north(), x, z),
reverse.weight(chunkX, reverse.north(), x, z), 0D);
}
}
}
}
@SuppressWarnings("unchecked")
private static Fixture fixture(boolean thirdProfile) throws ReflectiveOperationException {
Engine engine = mock(Engine.class);
EngineMantle engineMantle = mock(EngineMantle.class);
when(engineMantle.getEngine()).thenReturn(engine);
MantleCarvingComponent component = new MantleCarvingComponent(engineMantle);
when(engine.getDimension()).thenReturn(new IrisDimension());
when(engine.getWorld()).thenReturn(IrisWorld.builder().minHeight(0).maxHeight(128).build());
when(engine.getMetrics()).thenReturn(new EngineMetrics(16));
IrisComplex complex = mock(IrisComplex.class);
when(complex.allowsMantleChunkWrite(anyInt(), anyInt())).thenReturn(true);
ProceduralStream<Double> heights = mock(ProceduralStream.class);
when(heights.getDouble(anyDouble(), anyDouble())).thenReturn(100D);
when(complex.getNaturalHeightStream()).thenReturn(heights);
ProceduralStream<IrisRegion> regions = mock(ProceduralStream.class);
ProceduralStream<IrisBiome> surfaces = mock(ProceduralStream.class);
ProceduralStream<IrisBiome> caves = mock(ProceduralStream.class);
ProceduralStream<NativeBlockState> fluids = mock(ProceduralStream.class);
when(complex.getRegionStream()).thenReturn(regions);
when(complex.getNaturalTrueBiomeStream()).thenReturn(surfaces);
when(complex.getCaveBiomeStream()).thenReturn(caves);
when(complex.getFluidStream()).thenReturn(fluids);
IrisCaveProfile west = new IrisCaveProfile().setEnabled(true);
IrisCaveProfile east = new IrisCaveProfile().setEnabled(true);
IrisCaveProfile north = new IrisCaveProfile().setEnabled(true);
IrisBiome westBiome = new IrisBiome().setCaveProfile(west);
IrisBiome eastBiome = new IrisBiome().setCaveProfile(east);
IrisBiome northBiome = new IrisBiome().setCaveProfile(north);
when(caves.get(anyDouble(), anyDouble())).thenAnswer(call -> {
double x = call.getArgument(0);
double z = call.getArgument(1);
return thirdProfile && z < 7D ? northBiome : x < 0D ? westBiome : eastBiome;
});
ChunkContext context = mock(ChunkContext.class);
when(context.getComplex()).thenReturn(complex);
MantleWriter writer = mock(MantleWriter.class);
Mantle<Matter> storage = mock(Mantle.class);
when(storage.getWorldHeight()).thenReturn(128);
when(writer.getMantle()).thenReturn(storage);
Map<Integer, Map<IrisCaveProfile, double[]>> captured = new HashMap<>();
Map<IrisCaveProfile, IrisCaveCarver3D> carvers = new IdentityHashMap<>();
for (IrisCaveProfile profile : new IrisCaveProfile[]{west, east, north}) {
IrisCaveCarver3D carver = mock(IrisCaveCarver3D.class);
doAnswer(call -> {
int chunkX = call.getArgument(1);
double[] weights = call.getArgument(3);
captured.computeIfAbsent(chunkX, ignored -> new IdentityHashMap<>()).put(profile, weights.clone());
return 0;
}).when(carver).carve(any(), anyInt(), anyInt(), any(), anyDouble(), anyDouble(),
any(), any(), any(), any(), any());
carvers.put(profile, carver);
}
Field profileCarvers = MantleCarvingComponent.class.getDeclaredField("profileCarvers");
profileCarvers.setAccessible(true);
profileCarvers.set(component, carvers);
return new Fixture(component, context, writer, west, east, north, captured);
}
private record Fixture(MantleCarvingComponent component, ChunkContext context, MantleWriter writer,
IrisCaveProfile west, IrisCaveProfile east, IrisCaveProfile north,
Map<Integer, Map<IrisCaveProfile, double[]>> captured) {
private void generate(boolean reverse) {
component.generateLayer(writer, reverse ? 0 : -1, 0, context);
component.generateLayer(writer, reverse ? -1 : 0, 0, context);
}
private double weight(int chunkX, IrisCaveProfile profile, int x, int z) {
double[] weights = captured.get(chunkX).get(profile);
return weights == null ? 0D : weights[(x << 4) | z];
}
}
}
@@ -1,162 +0,0 @@
package art.arcane.iris.generation.mantle;
import art.arcane.iris.generation.cave.IrisCaveProfile;
import org.junit.BeforeClass;
import org.junit.Test;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
public class MantleCarvingComponentTop2BlendTest {
private static Constructor<?> weightedProfileConstructor;
private static Method limitMethod;
private static Field profileField;
private static Field columnWeightsField;
@BeforeClass
public static void setup() throws Exception {
Class<?> weightedProfileClass = Class.forName("art.arcane.iris.generation.mantle.MantleCarvingComponent$WeightedProfile");
weightedProfileConstructor = weightedProfileClass.getDeclaredConstructor(IrisCaveProfile.class, double[].class, double.class, Class.forName("art.arcane.iris.pack.value.IrisRange"), int.class);
weightedProfileConstructor.setAccessible(true);
limitMethod = MantleCarvingComponent.class.getDeclaredMethod("limitAndMergeBlendedProfiles", List.class, int.class, int.class);
limitMethod.setAccessible(true);
profileField = weightedProfileClass.getDeclaredField("profile");
profileField.setAccessible(true);
columnWeightsField = weightedProfileClass.getDeclaredField("columnWeights");
columnWeightsField.setAccessible(true);
}
@Test
public void topTwoProfilesAreKeptAndDroppedWeightsAreMergedIntoDominantColumns() throws Exception {
WeightedInput input = createWeightedProfiles();
List<?> limited = invokeLimit(input.weightedProfiles(), 2);
assertEquals(2, limited.size());
Map<IrisCaveProfile, double[]> byProfile = extractWeightsByProfile(limited);
IrisCaveProfile first = input.profiles().first();
IrisCaveProfile second = input.profiles().second();
assertEquals(1.0D, byProfile.get(first)[1], 0D);
assertEquals(1.0D, byProfile.get(second)[0], 0D);
}
@Test
public void topTwoMergeIsDeterministicAcrossRuns() throws Exception {
WeightedInput firstInput = createWeightedProfiles();
WeightedInput secondInput = createWeightedProfiles();
List<?> first = invokeLimit(firstInput.weightedProfiles(), 2);
List<?> second = invokeLimit(secondInput.weightedProfiles(), 2);
Map<IrisCaveProfile, double[]> firstByProfile = extractWeightsByProfile(first);
Map<IrisCaveProfile, double[]> secondByProfile = extractWeightsByProfile(second);
assertEquals(firstByProfile.get(firstInput.profiles().first())[0], secondByProfile.get(secondInput.profiles().first())[0], 0D);
assertEquals(firstByProfile.get(firstInput.profiles().first())[1], secondByProfile.get(secondInput.profiles().first())[1], 0D);
assertEquals(firstByProfile.get(firstInput.profiles().second())[0], secondByProfile.get(secondInput.profiles().second())[0], 0D);
assertEquals(firstByProfile.get(firstInput.profiles().second())[1], secondByProfile.get(secondInput.profiles().second())[1], 0D);
}
@Test
public void topTwoMergePreservesIndependentWeightsAtHighColumnIndexes() throws Exception {
WeightedInput input = createWeightedProfiles();
List<?> limited = invokeLimit(input.weightedProfiles(), 2);
Map<IrisCaveProfile, double[]> byProfile = extractWeightsByProfile(limited);
IrisCaveProfile first = input.profiles().first();
IrisCaveProfile second = input.profiles().second();
assertEquals(1.0D, byProfile.get(first)[255], 0D);
assertEquals(1.0D, byProfile.get(second)[254], 0D);
}
@Test
public void topTwoTieBreaksBySequenceNotIdentity() throws Exception {
IrisCaveProfile first = new IrisCaveProfile().setEnabled(true).setBaseWeight(1.0D);
IrisCaveProfile second = new IrisCaveProfile().setEnabled(true).setBaseWeight(1.0D);
IrisCaveProfile third = new IrisCaveProfile().setEnabled(true).setBaseWeight(1.0D);
double[] weightsA = new double[256];
double[] weightsB = new double[256];
double[] weightsC = new double[256];
weightsA[0] = 0.5D;
weightsB[0] = 0.5D;
weightsC[0] = 0.5D;
List<Object> weighted = new ArrayList<>();
weighted.add(weightedProfileConstructor.newInstance(first, weightsA, average(weightsA), null, 0));
weighted.add(weightedProfileConstructor.newInstance(second, weightsB, average(weightsB), null, 1));
weighted.add(weightedProfileConstructor.newInstance(third, weightsC, average(weightsC), null, 2));
List<?> limited = invokeLimit(weighted, 2);
assertEquals(2, limited.size());
Map<IrisCaveProfile, double[]> byProfile = extractWeightsByProfile(limited);
assertEquals(true, byProfile.containsKey(first));
assertEquals(true, byProfile.containsKey(second));
assertEquals(false, byProfile.containsKey(third));
}
private WeightedInput createWeightedProfiles() throws Exception {
IrisCaveProfile first = new IrisCaveProfile().setEnabled(true).setBaseWeight(1.31D);
IrisCaveProfile second = new IrisCaveProfile().setEnabled(true).setBaseWeight(1.17D);
IrisCaveProfile third = new IrisCaveProfile().setEnabled(true).setBaseWeight(0.93D);
Profiles profiles = new Profiles(first, second, third);
double[] firstWeights = new double[256];
firstWeights[0] = 0.2D;
firstWeights[1] = 0.8D;
firstWeights[255] = 0.6D;
double[] secondWeights = new double[256];
secondWeights[0] = 0.7D;
secondWeights[1] = 0.1D;
secondWeights[254] = 0.7D;
double[] thirdWeights = new double[256];
thirdWeights[0] = 0.3D;
thirdWeights[1] = 0.4D;
thirdWeights[254] = 0.3D;
thirdWeights[255] = 0.4D;
List<Object> weighted = new ArrayList<>();
weighted.add(weightedProfileConstructor.newInstance(first, firstWeights, average(firstWeights), null, 0));
weighted.add(weightedProfileConstructor.newInstance(second, secondWeights, average(secondWeights), null, 1));
weighted.add(weightedProfileConstructor.newInstance(third, thirdWeights, average(thirdWeights), null, 2));
return new WeightedInput(weighted, profiles);
}
private List<?> invokeLimit(List<Object> weightedProfiles, int limit) throws Exception {
return (List<?>) limitMethod.invoke(null, weightedProfiles, limit, 256);
}
private Map<IrisCaveProfile, double[]> extractWeightsByProfile(List<?> weightedProfiles) throws Exception {
Map<IrisCaveProfile, double[]> byProfile = new IdentityHashMap<>();
for (Object weightedProfile : weightedProfiles) {
IrisCaveProfile profile = (IrisCaveProfile) profileField.get(weightedProfile);
double[] weights = (double[]) columnWeightsField.get(weightedProfile);
byProfile.put(profile, weights);
}
return byProfile;
}
private double average(double[] weights) {
double total = 0D;
for (double weight : weights) {
total += weight;
}
return total / weights.length;
}
private record Profiles(IrisCaveProfile first, IrisCaveProfile second, IrisCaveProfile third) {
}
private record WeightedInput(List<Object> weightedProfiles, Profiles profiles) {
}
}
@@ -13,6 +13,7 @@ import art.arcane.iris.structure.object.ObjectPlaceMode;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.mantle.EngineMantle;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.iris.testsupport.PlatformBinding;
import art.arcane.volmlib.util.math.Vector3i;
@@ -30,7 +31,10 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class NativeStructureObjectVetoTest {
@@ -52,6 +56,9 @@ public class NativeStructureObjectVetoTest {
when(complex.getHeightStream()).thenReturn(heightStream);
engine = mock(Engine.class);
when(engine.getHeight()).thenReturn(256);
when(engine.getHeight(anyInt(), anyInt())).thenReturn(SURFACE_Y);
when(engine.getHeight(anyInt(), anyInt(), eq(true))).thenAnswer(invocation ->
engine.getHeight(invocation.getArgument(0), invocation.getArgument(1)));
when(engine.getComplex()).thenReturn(complex);
when(engine.getDimension()).thenReturn(mock(IrisDimension.class));
volumes();
@@ -67,6 +74,79 @@ public class NativeStructureObjectVetoTest {
assertFalse(placer.written().isEmpty());
}
@Test
public void flattenSkirtRejectsATreeWhoseGroundWillBeLoweredOutsideThePiece() {
RecordingPlacer placer = new RecordingPlacer(engine);
volumes(new NativeStructureVolume("minecraft:village_plains", 1, 64, -1, 11, 65, 7,
new NativeStructureVolume.SurfaceAdjustment(List.of(
new NativeStructureVolume.SurfaceAnchor(1, 11, -1, 7, 64, 2)), 24, 64)));
assertEquals(-1, place(placer));
assertTrue(placer.written().isEmpty());
}
@Test
public void flattenSkirtKeepsATreeWhereTheGradeDoesNotLowerItsGround() {
RecordingPlacer placer = new RecordingPlacer(engine);
volumes(new NativeStructureVolume("minecraft:village_plains", 1, 96, -1, 11, 97, 7,
new NativeStructureVolume.SurfaceAdjustment(List.of(
new NativeStructureVolume.SurfaceAnchor(1, 11, -1, 7, 96, 2)), 24, 64)));
assertTrue(place(placer) >= 0);
assertFalse(placer.written().isEmpty());
}
@Test
public void flattenCutKeepsObjectsBelowTheRemainingCaveRoof() {
when(engine.getHeight(anyInt(), anyInt())).thenReturn(140);
RecordingPlacer placer = new RecordingPlacer(engine);
volumes(new NativeStructureVolume("minecraft:village_plains", 1, 120, -1, 11, 121, 7,
new NativeStructureVolume.SurfaceAdjustment(List.of(
new NativeStructureVolume.SurfaceAnchor(1, 11, -1, 7, 120, 2)), 24, 64)));
assertTrue(place(placer) >= 0);
assertFalse(placer.written().isEmpty());
verify(engine, times(1)).getHeight(0, 0);
}
@Test
public void flattenCutKeepsObjectsAboveTheRemovedSurfaceBand() {
when(engine.getHeight(anyInt(), anyInt())).thenReturn(40);
RecordingPlacer placer = new RecordingPlacer(engine);
volumes(new NativeStructureVolume("minecraft:village_plains", 1, 20, -1, 11, 21, 7,
new NativeStructureVolume.SurfaceAdjustment(List.of(
new NativeStructureVolume.SurfaceAnchor(1, 11, -1, 7, 20, 2)), 24, 64)));
assertTrue(place(placer) >= 0);
assertFalse(placer.written().isEmpty());
}
@Test
public void flattenCutUsesHostGroundBelowAnUpperTerrainLayer() {
EngineMantle mantle = mock(EngineMantle.class);
when(engine.getMantle()).thenReturn(mantle);
when(mantle.trueHeight(anyInt(), anyInt())).thenReturn(40);
RecordingPlacer placer = new RecordingPlacer(engine);
volumes(new NativeStructureVolume("minecraft:village_plains", 1, 20, -1, 11, 21, 7,
new NativeStructureVolume.SurfaceAdjustment(List.of(
new NativeStructureVolume.SurfaceAnchor(1, 11, -1, 7, 20, 2)), 24, 64)));
assertTrue(place(placer) >= 0);
assertFalse(placer.written().isEmpty());
}
@Test
public void flattenCutConvertsTheHostGroundToNegativeWorldCoordinates() {
when(engine.getMinHeight()).thenReturn(-256);
RecordingPlacer placer = new RecordingPlacer(engine);
volumes(new NativeStructureVolume("minecraft:village_plains", 1, -190, -1, 11, -189, 7,
new NativeStructureVolume.SurfaceAdjustment(List.of(
new NativeStructureVolume.SurfaceAnchor(1, 11, -1, 7, -190, 2)), 24, 64)));
assertEquals(-1, place(placer));
assertTrue(placer.written().isEmpty());
}
@Test
public void canopyBlockInsideAPieceRejectsTheWholeObject() {
int canopyY = plantedTopY();
@@ -250,7 +330,7 @@ public class NativeStructureObjectVetoTest {
}
private NativeStructureVolume volume(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
return new NativeStructureVolume("minecraft:village_plains", minX, minY, minZ, maxX, maxY, maxZ);
return new NativeStructureVolume("minecraft:village_plains", minX, minY, minZ, maxX, maxY, maxZ, null);
}
private static NativeBlockState state(String key, boolean solid) {
@@ -105,7 +105,7 @@ public class NativeStructureVolumeMemoTest {
}
private static NativeStructureVolume volume(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
return new NativeStructureVolume("minecraft:village_swamp", minX, minY, minZ, maxX, maxY, maxZ);
return new NativeStructureVolume("minecraft:village_swamp", minX, minY, minZ, maxX, maxY, maxZ, null);
}
private static final class RecordingHooks implements EnginePlatformHooks {
@@ -4,13 +4,16 @@ import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume;
import org.junit.Test;
import java.util.List;
import java.util.ArrayList;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class NativeStructureVolumeTest {
private static final NativeStructureVolume PIECE =
new NativeStructureVolume("minecraft:village_plains", 10, 64, 20, 25, 78, 40);
new NativeStructureVolume("minecraft:village_plains", 10, 64, 20, 25, 78, 40, null);
@Test
public void rectQueriesIgnoreTheVerticalAxis() {
@@ -53,4 +56,46 @@ public class NativeStructureVolumeTest {
assertEquals(PIECE.maxY(), normalized.maxY());
assertEquals(PIECE.maxZ(), normalized.maxZ());
}
@Test
public void terrainInfluenceDoesNotChangePhysicalIntersection() {
NativeStructureVolume volume = adjustedPoint(0, 90, 0, 24);
assertTrue(volume.intersectsInfluenceRect(-1, 0, -1, 0));
assertFalse(volume.intersects(-1, 40, 0, -1, 160, 0));
assertFalse(volume.intersectsRect(-1, 0, -1, 0));
}
@Test
public void oppositeGradesBlendWithoutCuttingTheSurface() {
List<NativeStructureVolume> volumes = List.of(adjustedPoint(0, 90, 0, 20), adjustedPoint(20, 110, 0, 20));
assertEquals(100, NativeStructureVolume.surfaceCutY(volumes, 10, 0, 100));
}
@Test
public void halfBlockCutsRoundTheirSignedDepthLikeTheFitter() {
assertEquals(99, NativeStructureVolume.surfaceCutY(List.of(adjustedPoint(0, 99, 0, 24)), 12, 0, 100));
}
@Test
public void realVillageJunctionsLowerTheReportedPineGround() {
int[][] junctions = {{-2591,144,-7062},{-2591,144,-7061},{-2593,141,-7055},{-2592,141,-7055},
{-2591,163,-7070},{-2591,163,-7071},{-2608,149,-7075},{-2609,149,-7075},
{-2591,136,-7051},{-2591,137,-7051},{-2602,126,-7047},{-2591,136,-7050},
{-2591,137,-7050},{-2602,126,-7046},{-2602,122,-7042},{-2602,122,-7041},
{-2624,124,-7067},{-2624,124,-7068}};
List<NativeStructureVolume.SurfaceAnchor> anchors = new ArrayList<>();
for (int[] junction : junctions) {
anchors.add(new NativeStructureVolume.SurfaceAnchor(junction[0], junction[0], junction[2], junction[2], junction[1], 1));
}
NativeStructureVolume volume = new NativeStructureVolume("minecraft:village_plains", -2600, 144, -7070, -2590, 145, -7062,
new NativeStructureVolume.SurfaceAdjustment(anchors, 24, 64));
assertEquals(151, NativeStructureVolume.surfaceCutY(List.of(volume), -2601, -7063, 162));
assertEquals(162, NativeStructureVolume.surfaceCutY(List.of(volume), -2700, -7063, 162));
}
private static NativeStructureVolume adjustedPoint(int x, int groundY, int z, int padding) {
return new NativeStructureVolume("minecraft:village_plains", x, groundY, z, x, groundY, z,
new NativeStructureVolume.SurfaceAdjustment(List.of(
new NativeStructureVolume.SurfaceAnchor(x, x, z, z, groundY, 1)), padding, 64));
}
}
@@ -0,0 +1,184 @@
package art.arcane.iris.structure.object;
import art.arcane.iris.generation.block.TileData;
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.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.math.Vector3i;
import org.junit.Test;
import java.util.List;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisObjectFractionalScalingTest {
@Test
public void fractionalReductionRetainsBothLayersWhenTwoOutputVoxelsRemain() {
NativeBlockState stem = state("minecraft:mushroom_stem");
NativeBlockState cap = state("minecraft:red_mushroom_block");
IrisObject source = layers(stem, cap);
for (boolean savedOrigin : new boolean[]{false, true}) {
for (double factor : new double[]{0.6D, 0.75D, 0.9D}) {
IrisObject scaled = scale(source, factor, savedOrigin);
assertEquals(2, scaled.getH());
assertEquals(2, scaled.getBlocks().size());
assertSame(stem, scaled.getBlocks().get(scaled.getSigned(0, 0, 0)));
assertSame(cap, scaled.getBlocks().get(scaled.getSigned(0, 1, 0)));
}
}
assertSame(stem, source.getBlocks().get(source.getSigned(0, 0, 0)));
assertSame(cap, source.getBlocks().get(source.getSigned(0, 1, 0)));
}
@Test
public void fractionalEnlargementUsesTheDominantMaterialInSharedOutputVoxels() {
NativeBlockState lower = state("minecraft:stone");
NativeBlockState upper = state("minecraft:dirt");
IrisObject source = layers(lower, upper);
for (boolean savedOrigin : new boolean[]{false, true}) {
IrisObject scaled = scale(source, 1.75D, savedOrigin);
assertEquals(4, scaled.getH());
for (int y = 0; y < 4; y++) {
assertSame(y < 2 ? lower : upper, scaled.getBlocks().get(scaled.getSigned(0, y, 0)));
}
}
}
@Test
public void fractionalReductionResolvesMaterialCoverageAcrossAllThreeAxes() {
IrisObject source = new IrisObject(2, 2, 2);
for (int x = 0; x < 2; x++) {
for (int y = 0; y < 2; y++) {
for (int z = 0; z < 2; z++) {
source.setUnsigned(x, y, z, state("test:voxel_" + x + y + z));
}
}
}
IrisObject scaled = source.scaled(0.75D, IrisObjectPlacementScaleInterpolator.NONE);
assertEquals(8, scaled.getBlocks().size());
for (int x = 0; x < 2; x++) {
for (int y = 0; y < 2; y++) {
for (int z = 0; z < 2; z++) {
assertSame(source.getBlocks().get(source.getSigned(x, y, z)),
scaled.getBlocks().get(scaled.getSigned(x, y, z)));
}
}
}
}
@Test
public void fractionalSavedOriginPreservesNegativeCoordinatesAndTheirTiles() {
NativeBlockState lower = state("minecraft:chest");
NativeBlockState upper = state("minecraft:barrel");
IrisObject source = new IrisObject(1, 4, 1);
source.setCenter(new Vector3i(0, 3, 0));
source.setUnsigned(0, 1, 0, lower);
source.setUnsignedTile(0, 1, 0, tile("minecraft:chest", "lower"));
source.setUnsigned(0, 2, 0, upper);
source.setUnsignedTile(0, 2, 0, tile("minecraft:barrel", "upper"));
IrisObject scaled = source.scaledAroundOrigin(0.75D, IrisObjectPlacementScaleInterpolator.NONE);
assertEquals(new Vector3i(0, 3, 0), scaled.getCenter());
assertEquals(-3, scaled.getAABB().min().getY());
assertEquals(0, scaled.getAABB().max().getY());
assertSame(lower, scaled.getBlocks().get(new IrisBlockVector(0, -2, 0)));
assertSame(upper, scaled.getBlocks().get(new IrisBlockVector(0, -1, 0)));
assertEquals("lower", scaled.getStates().get(new IrisBlockVector(0, -2, 0)).getProperties().get("name"));
assertEquals("upper", scaled.getStates().get(new IrisBlockVector(0, -1, 0)).getProperties().get("name"));
}
@Test
public void sameMaterialTilesFollowTheirDominantSourceVoxel() {
NativeBlockState chest = state("minecraft:chest");
IrisObject source = layers(chest, chest);
TileData lower = tile("minecraft:chest", "lower");
TileData upper = tile("minecraft:chest", "upper");
source.setUnsignedTile(0, 0, 0, lower);
source.setUnsignedTile(0, 1, 0, upper);
IrisObject scaled = source.scaled(0.75D, IrisObjectPlacementScaleInterpolator.NONE);
TileData scaledLower = scaled.getStates().get(scaled.getSigned(0, 0, 0));
TileData scaledUpper = scaled.getStates().get(scaled.getSigned(0, 1, 0));
assertEquals(lower, scaledLower);
assertEquals(upper, scaledUpper);
assertNotSame(lower, scaledLower);
assertNotSame(lower.getProperties(), scaledLower.getProperties());
assertNotSame(upper, scaledUpper);
assertNotSame(scaledLower.getProperties(), scaledUpper.getProperties());
}
@Test
public void sameMaterialCanopyRolesAndRootContactsFollowTheDominantSourceVoxel() {
NativeBlockState wood = state("minecraft:spruce_wood");
IrisObject source = layers(wood, wood);
TreeGrounding grounding = TreeGrounding.create(List.of(new TreeGrounding.Root(0, 0, 0, wood)),
8, Set.of(new TreeBlockCanvas.Vec(0, 1, 0)));
source.setTreeGrounding(grounding);
for (boolean savedOrigin : new boolean[]{false, true}) {
IrisObject scaled = scale(source, 0.75D, savedOrigin);
int bottom = -scaled.getCenter().getY();
TreeGrounding result = scaled.getTreeGrounding();
assertEquals(List.of(new TreeGrounding.Root(0, bottom, 0, wood)), result.roots());
assertFalse(result.isCanopy(0, bottom, 0));
assertTrue(result.isCanopy(0, bottom + 1, 0));
assertEquals(6, result.maxExtension());
}
assertSame(grounding, source.getTreeGrounding());
}
@Test
public void canopyWinningAnUnresolvableRootCollisionDoesNotBecomeAGroundContact() {
NativeBlockState wood = state("minecraft:spruce_wood");
IrisObject source = layers(wood, wood);
source.setTreeGrounding(TreeGrounding.create(List.of(new TreeGrounding.Root(0, 0, 0, wood)),
8, Set.of(new TreeBlockCanvas.Vec(0, 1, 0))));
IrisObject scaled = source.scaled(0.5D, IrisObjectPlacementScaleInterpolator.NONE);
assertEquals(1, scaled.getBlocks().size());
assertTrue(scaled.getTreeGrounding().roots().isEmpty());
assertTrue(scaled.getTreeGrounding().isCanopy(0, 0, 0));
}
private static IrisObject layers(NativeBlockState lower, NativeBlockState upper) {
IrisObject source = new IrisObject(1, 2, 1);
source.setCenter(new Vector3i(0, 0, 0));
source.setUnsigned(0, 0, 0, lower);
source.setUnsigned(0, 1, 0, upper);
return source;
}
private static IrisObject scale(IrisObject source, double scale, boolean savedOrigin) {
return savedOrigin ? source.scaledAroundOrigin(scale, IrisObjectPlacementScaleInterpolator.NONE)
: source.scaled(scale, IrisObjectPlacementScaleInterpolator.NONE);
}
private static TileData tile(String material, String name) {
KMap<String, Object> properties = new KMap<>();
properties.put("name", name);
return new TileData(material, properties);
}
private static NativeBlockState state(String key) {
NativeBlockState state = mock(NativeBlockState.class);
when(state.key()).thenReturn(key);
when(state.materialKey()).thenReturn(key);
return state;
}
}
@@ -620,6 +620,91 @@ public class IrisObjectPlacementRunnerRegressionTest {
assertSame(machine, placer.get(0, ANCHOR_Y + 9, 0));
}
@Test
public void organicMushroomStiltsUseTheStemAndStopAtTheCaveFloor() {
NativeBlockState stem = state("minecraft:mushroom_stem", true);
NativeBlockState cap = state("minecraft:red_mushroom_block", true);
IrisObject mushroom = new IrisObject(1, 2, 1);
mushroom.setUnsigned(0, 0, 0, stem);
mushroom.setUnsigned(0, 1, 0, cap);
RecordingPlacer placer = new RecordingPlacer(null);
placer.terrain(0, 0, 0, 30, 120, 140);
placer.set(0, 30, 0, solid);
placer.writes().clear();
IrisObjectPlacement placement = placement().setMode(ObjectPlaceMode.ORGANIC_STILT)
.setStiltSettings(new IrisStiltSettings().setOrganicJitter(0).setOrganicScratch(0));
mushroom.place(0, 60, 0, placer, placement, new RNG(2L), data);
assertEquals(1, placer.writesOf(cap).size());
for (int y = 31; y < 60; y++) {
assertSame(stem, placer.get(0, y, 0));
}
assertSame(solid, placer.get(0, 30, 0));
assertTrue(placer.writes().stream().allMatch(write -> write.y() >= 31));
}
@Test
public void defaultOrganicStiltsDoNotReachAFloorMoreThanOneHundredBlocksBelow() {
NativeBlockState stem = state("minecraft:mushroom_stem", true);
IrisObject mushroom = new IrisObject(1, 1, 1);
mushroom.setUnsigned(0, 0, 0, stem);
RecordingPlacer placer = new RecordingPlacer(null);
placer.set(0, 30, 0, solid);
placer.writes().clear();
IrisObjectPlacement placement = placement().setMode(ObjectPlaceMode.ORGANIC_STILT)
.setStiltSettings(new IrisStiltSettings().setOrganicJitter(0).setOrganicScratch(0));
mushroom.place(0, 160, 0, placer, placement, new RNG(2L), data);
assertEquals(9, placer.world.values().stream().filter(block -> block == stem).count());
assertTrue(placer.writesOf(stem).stream().allMatch(write -> write.y() >= 152));
assertNull(placer.get(0, 151, 0));
assertSame(solid, placer.get(0, 30, 0));
}
@Test
public void shrinkingTwoBlockMushroomsPreservesTheStemUsedByOrganicStilts() {
NativeBlockState stem = state("minecraft:mushroom_stem", true);
NativeBlockState cap = state("minecraft:red_mushroom_block", true);
IrisObject mushroom = new IrisObject(1, 2, 1);
mushroom.setUnsigned(0, 0, 0, stem);
mushroom.setUnsigned(0, 1, 0, cap);
IrisObject scaled = mushroom.scaled(0.75, IrisObjectPlacementScaleInterpolator.NONE);
RecordingPlacer placer = new RecordingPlacer(null);
placer.set(0, ANCHOR_Y, 0, solid);
placer.writes().clear();
IrisObjectPlacement placement = placement().setMode(ObjectPlaceMode.ORGANIC_STILT)
.setStiltSettings(new IrisStiltSettings().setOrganicJitter(0).setOrganicScratch(0));
scaled.place(0, ANCHOR_Y + 10, 0, placer, placement, new RNG(2L), data);
assertSame(stem, placer.get(0, ANCHOR_Y + 1, 0));
assertEquals(1, placer.writesOf(cap).size());
}
@Test
public void centerHeightSurfaceSupportAllowsAnImportedRootAboveALowerAdjacentColumn() {
NativeBlockState wood = state("minecraft:spruce_wood", true);
IrisObject tree = new IrisObject(7, 27, 7);
tree.getBlocks().put(new IrisBlockVector(0, -13, 0), wood);
tree.getBlocks().put(new IrisBlockVector(1, -13, 0), wood);
tree.getBlocks().put(new IrisBlockVector(1, -13, -1), wood);
RecordingPlacer placer = new RecordingPlacer(null);
placer.terrain(0, 0, 0, 167);
placer.terrain(1, 0, 0, 163);
placer.terrain(1, -1, 0, 163);
IrisObjectPlacement placement = new IrisObjectPlacement()
.setRotation(IrisObjectRotation.of(0, 0, 0));
assertTrue(tree.place(0, -1, 0, placer, placement, new RNG(2L), data) >= 0);
assertSame(wood, placer.get(1, 167, 0));
assertNull(placer.get(1, 166, 0));
assertFalse(placer.isSurfaceSolid(1, 166, 0));
assertEquals(163, placer.highest(1, 0));
}
@Test
public void stiltPaletteCannotIntroduceExcludedMaterialsOrContainers() {
NativeBlockState calcite = state("minecraft:calcite", true);
@@ -10,6 +10,8 @@ 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.nativelib.terrain.structure.NativeStructureVolume;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Before;
import org.junit.Rule;
@@ -84,6 +86,30 @@ public class TreeGroundingPlacementTest {
assertFalse(terrain.written.containsKey(new Position(2, 79, 0)));
}
@Test
public void nativeFlattenInfluenceDoesNotBlockRootExtensionsBelowItsPiece() {
Terrain terrain = new Terrain();
terrain.heights.put(new Column(1, 0), 77);
Engine engine = data.getEngine();
when(terrain.placer.getEngine()).thenReturn(engine);
when(engine.getDimension()).thenReturn(new IrisDimension());
when(engine.getHeight(anyInt(), anyInt(), anyBoolean())).thenAnswer(invocation ->
terrain.surface(invocation.getArgument(0), invocation.getArgument(1)));
NativeStructureVolume volume = new NativeStructureVolume("minecraft:village_plains",
10, 100, 0, 12, 110, 2,
new NativeStructureVolume.SurfaceAdjustment(List.of(
new NativeStructureVolume.SurfaceAnchor(10, 12, 0, 2, 100, 4)), 16, 16));
when(engine.getNativeStructureVolumes(anyInt(), anyInt(), anyInt(), anyInt()))
.thenReturn(new KList<>(List.of(volume)));
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(1, 76, 0)));
}
@Test
public void cliffRejectionWritesNoPartOfTheTreeOrListener() {
Terrain terrain = new Terrain();
@@ -0,0 +1,65 @@
package art.arcane.iris.world;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.platform.generation.BukkitChunkGenerator;
import art.arcane.iris.world.lifecycle.WorldLifecycleCaller;
import art.arcane.iris.world.lifecycle.WorldLifecycleRequest;
import org.bukkit.NamespacedKey;
import org.bukkit.WorldCreator;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.mockito.MockedConstruction;
import org.mockito.MockedStatic;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mockConstruction;
import static org.mockito.Mockito.mockStatic;
public class IrisWorldCreatorSeedTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void explicitSeedsReachTheGeneratorAndLifecycleRequestWithoutDefaultSubstitution() throws Exception {
for (long seed : new long[]{0L, 78264193L, -734829104958217364L, Long.MIN_VALUE, Long.MAX_VALUE}) {
assertCreatedSeed(new IrisWorldCreator().seed(seed), seed);
}
}
@Test
public void omittedSeedUsesTheCreationDefault() throws Exception {
assertCreatedSeed(new IrisWorldCreator(), 1337L);
}
private void assertCreatedSeed(IrisWorldCreator creator, long expectedSeed) throws Exception {
NamespacedKey key = new NamespacedKey("iris", "seed-selection");
File root = temporaryFolder.newFolder();
IrisDimension dimension = new IrisDimension();
dimension.setLoadKey("cohesion");
WorldCreator bukkitCreator = WorldCreatorCompat.ofKey(key);
List<IrisWorld> worlds = new ArrayList<>();
try (MockedStatic<IrisWorldStorage> storage = mockStatic(IrisWorldStorage.class);
MockedStatic<WorldCreatorCompat> compatibility = mockStatic(WorldCreatorCompat.class);
MockedConstruction<BukkitChunkGenerator> generators = mockConstruction(BukkitChunkGenerator.class,
(generator, context) -> worlds.add((IrisWorld) context.arguments().getFirst()))) {
storage.when(() -> IrisWorldStorage.keyFromName("seed-selection")).thenReturn(key);
storage.when(() -> IrisWorldStorage.dimensionRoot(key)).thenReturn(root);
compatibility.when(() -> WorldCreatorCompat.ofKey(key)).thenReturn(bukkitCreator);
compatibility.when(() -> WorldCreatorCompat.keyOf(bukkitCreator)).thenReturn(key);
WorldCreator created = creator.name("seed-selection").dimension(dimension).create();
WorldLifecycleRequest request = WorldLifecycleRequest.fromCreator(
created, false, false, WorldLifecycleCaller.CREATE);
assertEquals(1, generators.constructed().size());
assertEquals(expectedSeed, worlds.getFirst().getRawWorldSeed());
assertEquals(expectedSeed, created.seed());
assertEquals(expectedSeed, request.seed());
}
}
}