mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-22 08:07:42 +00:00
Fixing a handfull of issues!
This commit is contained in:
1 parent
1132242c58
commit
c3359e0a16
30 files changed
+1324
-282
No files matched your search
+67
-2
@@ -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());
|
||||
|
||||
+12
-7
@@ -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) {
|
||||
|
||||
+67
-2
@@ -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());
|
||||
|
||||
+12
-7
@@ -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}
|
||||
}]
|
||||
}
|
||||
+15
@@ -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"}}]
|
||||
}
|
||||
Binary file not shown.
@@ -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) {
|
||||
|
||||
+2
-2
@@ -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) {
|
||||
|
||||
+1
-1
@@ -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);
|
||||
|
||||
+47
@@ -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}) {
|
||||
|
||||
+173
@@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
-162
@@ -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) {
|
||||
}
|
||||
}
|
||||
+81
-1
@@ -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) {
|
||||
|
||||
+1
-1
@@ -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 {
|
||||
|
||||
+46
-1
@@ -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));
|
||||
}
|
||||
}
|
||||
+184
@@ -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;
|
||||
}
|
||||
}
|
||||
+85
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user