mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-23 00:28:02 +00:00
Compare commits
37
Commits
No files matched your search
@@ -248,34 +248,26 @@ which is what a board reader means.
|
||||
|
||||
Requirements: JDK 25 (set `JAVA_HOME` to it). The Gradle wrapper handles everything else.
|
||||
|
||||
```
|
||||
./gradlew buildAllToOut
|
||||
```sh
|
||||
./gradlew buildAll
|
||||
```
|
||||
|
||||
builds every platform artifact into the workspace-level `../PluginOuts/` directory: the Bukkit jar
|
||||
and a Fabric, Forge and NeoForge jar for every Minecraft version in `gradle/mod-targets.json`
|
||||
(1.21.1, 26.1.2, 26.2 and 26.3):
|
||||
This builds and verifies the Bukkit plugin, API, SPI and the Fabric, Forge and NeoForge jars
|
||||
for every Minecraft version declared in `gradle/mod-targets.json`. Artifacts stay in the
|
||||
producing module's `build/` directory. The packed Bukkit plugin is in `build/libs/`, the
|
||||
API is in `build/api/`, the SPI is in `spi/build/libs/`, and mod jars are in
|
||||
`adapters/<loader>/build/<minecraft>/libs/`.
|
||||
|
||||
```
|
||||
Iris v<version> [CraftBukkit] <mc>.jar
|
||||
Iris v<version> [Fabric] <mc>+<loader>.jar
|
||||
Iris v<version> [Forge] <mc>+<loader>.jar
|
||||
Iris v<version> [NeoForge] <mc>+<loader>.jar
|
||||
```
|
||||
Use `./gradlew buildBukkit` for the plugin, or `verify<Loader><mc>` for one mod jar,
|
||||
for example `verifyFabric26_2` or `verifyNeoForge1_21_1`. `buildFabric`, `buildForge`
|
||||
and `buildNeoforge` select the `minecraftVersion` Gradle property. `./gradlew checkAll`
|
||||
runs every check plus the complete mod matrix. Forge 1.21.1 compiles its tests because
|
||||
Forge 52 cannot bootstrap Minecraft outside FML.
|
||||
|
||||
Per-platform tasks: `./gradlew buildBukkit`, and `build<Loader><mc>` for one mod jar, for example
|
||||
`buildFabric26_2` or `buildNeoForge1_21_1`. `buildFabric`, `buildForge` and `buildNeoforge` export the
|
||||
26.3 jars. `./gradlew checkAll` runs every check, then builds and verifies all twelve mod jars and runs their
|
||||
unit tests; Forge 1.21.1 only compiles its tests because Forge 52 cannot bootstrap Minecraft outside
|
||||
FML. The
|
||||
SPI jar (the pure-JVM adapter/platform contract, not the stable plugin API) is built to `spi/build/libs/` by
|
||||
`./gradlew :spi:jar`.
|
||||
|
||||
`./gradlew buildAll` also runs `buildAllToOut` and copies the jars into a consumer dropin tree
|
||||
for a local test server. The consumer directory defaults to `../../[Minecraft Server]/consumers/`
|
||||
when it exists, otherwise `build/consumers/` inside the repo. `-Plocation=/path/to/consumers`
|
||||
changes only the consumer directory. Both tasks copy every verified platform jar into
|
||||
`../PluginOuts/`.
|
||||
Multiplexor reads `gradle/multiplexor.gradle` to discover targets and build each in a
|
||||
separate source sandbox. Use its project build and deployment commands to export jars
|
||||
or install them into managed test instances. Gradle builds do not copy artifacts to
|
||||
external destinations.
|
||||
|
||||
VolmLib is built from source when a `VolmLib` checkout sits beside the repo or in a parent directory
|
||||
(`VOLMLIB_DIR` or `-PlocalVolmLibDirectory=<path>` pick another one). Without one, or with
|
||||
|
||||
+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());
|
||||
|
||||
+10
-6
@@ -3,6 +3,7 @@ package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
import art.arcane.volmlib.nativelib.v26_2_R1.terrain.NativeStructureWorldgenAccess;
|
||||
|
||||
import art.arcane.iris.structure.placement.IrisStructureTerrain;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockChangeSet;
|
||||
import art.arcane.volmlib.util.structure.StructureTerrainMode;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructurePostProcessor;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureTerrainIntegrator;
|
||||
@@ -230,8 +231,9 @@ public class NativeStructureWorldgenAccessTest {
|
||||
recording.world(), generationCenter,
|
||||
(x, z) -> SURFACE_FIRST_FREE_Y,
|
||||
(x, z) -> FLOOR_FIRST_FREE_Y,
|
||||
true,
|
||||
position -> false);
|
||||
true, true,
|
||||
position -> false,
|
||||
NativeBlockChangeSet.disabled());
|
||||
int x = generationCenter.getMiddleBlockX();
|
||||
int z = generationCenter.getMiddleBlockZ();
|
||||
|
||||
@@ -248,8 +250,9 @@ public class NativeStructureWorldgenAccessTest {
|
||||
recording.world(), generationCenter(),
|
||||
(x, z) -> SURFACE_FIRST_FREE_Y,
|
||||
(x, z) -> FLOOR_FIRST_FREE_Y,
|
||||
false,
|
||||
position::equals);
|
||||
false, true,
|
||||
position::equals,
|
||||
NativeBlockChangeSet.disabled());
|
||||
|
||||
assertSame(Blocks.DIRT, access.getBlockState(position).getBlock());
|
||||
assertFalse(access.ensureCanWrite(position));
|
||||
@@ -319,8 +322,9 @@ public class NativeStructureWorldgenAccessTest {
|
||||
recording.world(), generationCenter(),
|
||||
(x, z) -> SURFACE_FIRST_FREE_Y,
|
||||
(x, z) -> FLOOR_FIRST_FREE_Y,
|
||||
false,
|
||||
position -> false);
|
||||
false, true,
|
||||
position -> false,
|
||||
NativeBlockChangeSet.disabled());
|
||||
}
|
||||
|
||||
private static ChunkPos generationCenter() {
|
||||
|
||||
+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());
|
||||
|
||||
+10
-6
@@ -3,6 +3,7 @@ package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
|
||||
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureWorldgenAccess;
|
||||
|
||||
import art.arcane.iris.structure.placement.IrisStructureTerrain;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockChangeSet;
|
||||
import art.arcane.volmlib.util.structure.StructureTerrainMode;
|
||||
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructurePostProcessor;
|
||||
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureTerrainIntegrator;
|
||||
@@ -230,8 +231,9 @@ public class NativeStructureWorldgenAccessTest {
|
||||
recording.world(), generationCenter,
|
||||
(x, z) -> SURFACE_FIRST_FREE_Y,
|
||||
(x, z) -> FLOOR_FIRST_FREE_Y,
|
||||
true,
|
||||
position -> false);
|
||||
true, true,
|
||||
position -> false,
|
||||
NativeBlockChangeSet.disabled());
|
||||
int x = generationCenter.getMiddleBlockX();
|
||||
int z = generationCenter.getMiddleBlockZ();
|
||||
|
||||
@@ -248,8 +250,9 @@ public class NativeStructureWorldgenAccessTest {
|
||||
recording.world(), generationCenter(),
|
||||
(x, z) -> SURFACE_FIRST_FREE_Y,
|
||||
(x, z) -> FLOOR_FIRST_FREE_Y,
|
||||
false,
|
||||
position::equals);
|
||||
false, true,
|
||||
position::equals,
|
||||
NativeBlockChangeSet.disabled());
|
||||
|
||||
assertSame(Blocks.DIRT, access.getBlockState(position).getBlock());
|
||||
assertFalse(access.ensureCanWrite(position));
|
||||
@@ -319,8 +322,9 @@ public class NativeStructureWorldgenAccessTest {
|
||||
recording.world(), generationCenter(),
|
||||
(x, z) -> SURFACE_FIRST_FREE_Y,
|
||||
(x, z) -> FLOOR_FIRST_FREE_Y,
|
||||
false,
|
||||
position -> false);
|
||||
false, true,
|
||||
position -> false,
|
||||
NativeBlockChangeSet.disabled());
|
||||
}
|
||||
|
||||
private static ChunkPos generationCenter() {
|
||||
|
||||
+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) {
|
||||
|
||||
+11
@@ -19,6 +19,10 @@
|
||||
package art.arcane.iris.world.runtime;
|
||||
|
||||
import art.arcane.iris.api.world.IrisWorldPhase;
|
||||
import art.arcane.iris.generation.mantle.CustomBlockOwnership;
|
||||
import art.arcane.iris.integration.ExternalDataSVC;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockPoint;
|
||||
import art.arcane.iris.pack.datapack.ServerConfigurator;
|
||||
import art.arcane.iris.pack.datapack.DatapackIngestService;
|
||||
import art.arcane.iris.world.event.IrisEngineHotloadEvent;
|
||||
@@ -51,6 +55,13 @@ import org.bukkit.World;
|
||||
import org.bukkit.WorldBorder;
|
||||
|
||||
public final class BukkitEnginePlatformHooks implements EnginePlatformHooks {
|
||||
@Override
|
||||
public boolean removeCustomBlock(Engine engine, NativeBlockPoint position, CustomBlockOwnership.Entry entry) {
|
||||
ExternalDataSVC service = IrisServices.getOrNull(ExternalDataSVC.class);
|
||||
return service != null && service.removeGenerated(
|
||||
BukkitWorldBinding.world(engine.getWorld()).getBlockAt(position.x(), position.y(), position.z()), entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> flushSavedTerrainCapture(Engine engine) {
|
||||
return INMS.get().flushSavedTerrainCapture(BukkitWorldBinding.world(engine.getWorld()));
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "Flat",
|
||||
"derivative": "minecraft:snowy_plains",
|
||||
"vanillaDerivative": "minecraft:snowy_plains",
|
||||
"layers": [
|
||||
{
|
||||
"minHeight": 1,
|
||||
"maxHeight": 1,
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:lime_wool"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"minHeight": 384,
|
||||
"maxHeight": 384,
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:green_concrete"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"generators": [
|
||||
{
|
||||
"generator": "flat",
|
||||
"min": 96,
|
||||
"max": 96
|
||||
}
|
||||
],
|
||||
"decorators": [
|
||||
{
|
||||
"palette": [
|
||||
{
|
||||
"block": "craftengine:default/flower_basket",
|
||||
"data": {
|
||||
"variant": "ground"
|
||||
}
|
||||
}
|
||||
],
|
||||
"chance": 1,
|
||||
"forcePlace": true
|
||||
}
|
||||
]
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "CraftEngine Imported Feature Fixture",
|
||||
"version": 1,
|
||||
"mode": {
|
||||
"type": "OVERWORLD"
|
||||
},
|
||||
"regions": [
|
||||
"flat"
|
||||
],
|
||||
"focusRegion": "flat",
|
||||
"focus": "flat",
|
||||
"landChance": 1,
|
||||
"environment": "NORMAL",
|
||||
"dimensionHeight": {
|
||||
"min": -64,
|
||||
"max": 320
|
||||
},
|
||||
"logicalHeight": 384,
|
||||
"fluidHeight": 0,
|
||||
"bedrock": false,
|
||||
"rockPalette": {
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:red_concrete"
|
||||
}
|
||||
]
|
||||
},
|
||||
"importedStructures": {
|
||||
"disabled": [
|
||||
"minecraft:"
|
||||
]
|
||||
},
|
||||
"importedFeatures": {
|
||||
"enabled": true,
|
||||
"steps": [
|
||||
"TOP_LAYER_MODIFICATION"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"interpolator": {
|
||||
"function": "NONE",
|
||||
"horizontalScale": 1
|
||||
},
|
||||
"seed": 310,
|
||||
"composite": [
|
||||
{
|
||||
"seed": 310,
|
||||
"style": {
|
||||
"style": "FLAT"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "Flat",
|
||||
"landBiomes": [
|
||||
"flat"
|
||||
],
|
||||
"seaBiomes": [
|
||||
"flat"
|
||||
],
|
||||
"shoreBiomes": [
|
||||
"flat"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
}
|
||||
|
||||
version = '1.0.0'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven { url = 'https://repo.papermc.io/repository/maven-public/' }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly files(providers.gradleProperty('irisJar').get())
|
||||
compileOnly files(providers.gradleProperty('craftEngineJar').get())
|
||||
compileOnly 'io.papermc.paper:paper-api:26.1.2.build.74-stable'
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain.languageVersion = JavaLanguageVersion.of(25)
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
java.srcDirs = ['src']
|
||||
resources.srcDirs = ['resources']
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
options.compilerArgs += '-parameters'
|
||||
}
|
||||
|
||||
tasks.named('jar') {
|
||||
archiveBaseName = 'IrisCraftEngineNativeFixture'
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
name: IrisCraftEngineNativeFixture
|
||||
version: 1.0.0
|
||||
main: art.arcane.iris.fixture.CraftEngineNativeFixture
|
||||
api-version: '26.1.2'
|
||||
folia-supported: true
|
||||
depend: [Iris, CraftEngine]
|
||||
commands:
|
||||
iriscenative:
|
||||
permission: iriscenative.run
|
||||
permissions:
|
||||
iriscenative.run:
|
||||
default: op
|
||||
@@ -0,0 +1 @@
|
||||
rootProject.name = 'iris-craftengine-native-fixture'
|
||||
+385
@@ -0,0 +1,385 @@
|
||||
package art.arcane.iris.fixture;
|
||||
|
||||
import art.arcane.iris.generation.block.IrisCustomData;
|
||||
import art.arcane.iris.generation.block.TileData;
|
||||
import art.arcane.iris.generation.mantle.DeferredPlacementRecords;
|
||||
import art.arcane.iris.generation.mantle.CustomBlockOwnership;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.integration.ExternalDataSVC;
|
||||
import art.arcane.iris.integration.Identifier;
|
||||
import art.arcane.iris.platform.generation.EngineBukkitOps;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.world.IrisCreator;
|
||||
import art.arcane.iris.world.IrisToolbelt;
|
||||
import art.arcane.iris.world.storage.matter.TileWrapper;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockChangeSet;
|
||||
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import net.momirealms.craftengine.bukkit.api.CraftEngineBlocks;
|
||||
import net.momirealms.craftengine.bukkit.api.CraftEngineFurniture;
|
||||
import net.momirealms.craftengine.core.util.Key;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.GameMode;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public final class CraftEngineNativeFixture extends JavaPlugin {
|
||||
private static final NamespacedKey WORLD_KEY = new NamespacedKey("iris", "ce_native_probe");
|
||||
private static final NamespacedKey FEATURE_WORLD_KEY = new NamespacedKey("iris", "ce_native_feature_probe");
|
||||
private static final int TEST_Y = 180;
|
||||
private static final int TEST_Z = 8;
|
||||
private static final String BLOCK_ID = "craftengine:default/topaz_ore";
|
||||
private static final String FURNITURE_ID = "craftengine:default/flower_basket[variant=ground]";
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] arguments) {
|
||||
if (!(sender instanceof Player player) || arguments.length != 1) {
|
||||
sender.sendMessage("CE_FAILED Use /iriscenative prepare|pending|neighbor|loaded|features|restart from a player");
|
||||
return true;
|
||||
}
|
||||
if (arguments[0].equals("prepare")) {
|
||||
player.setGameMode(GameMode.CREATIVE);
|
||||
prepare(player);
|
||||
return true;
|
||||
}
|
||||
if (arguments[0].equals("features")) {
|
||||
importedFeatures(player);
|
||||
return true;
|
||||
}
|
||||
World world = Bukkit.getWorld(WORLD_KEY);
|
||||
if (world == null) {
|
||||
fail(player, new IllegalStateException("Fixture world has not been created"));
|
||||
return true;
|
||||
}
|
||||
if (arguments[0].equals("restart")) {
|
||||
loadChunks().thenCompose(ignored -> J.runRegionFuture(world, 0, 0,
|
||||
() -> later(player, world, () -> restarted(player, world))))
|
||||
.whenComplete((ignored, failure) -> {
|
||||
if (failure != null) {
|
||||
fail(player, failure);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
J.runRegionFuture(world, 0, 0, () -> {
|
||||
switch (arguments[0]) {
|
||||
case "pending" -> pending(player, world);
|
||||
case "neighbor" -> neighbor(player, world);
|
||||
case "loaded" -> invalidateLoaded(player, world, IrisToolbelt.access(world).getEngine());
|
||||
default -> throw new IllegalArgumentException("Unknown fixture operation");
|
||||
}
|
||||
}).whenComplete((ignored, failure) -> {
|
||||
if (failure != null) {
|
||||
fail(player, failure);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
private void prepare(Player player) {
|
||||
CompletableFuture.runAsync(() -> {
|
||||
if (Bukkit.getWorld(WORLD_KEY) == null) {
|
||||
try {
|
||||
new IrisCreator().name(WORLD_KEY.toString()).dimension("ce-native-fixture")
|
||||
.seed(78264193L).create();
|
||||
} catch (Exception failure) {
|
||||
throw new IllegalStateException("Could not create CraftEngine fixture world", failure);
|
||||
}
|
||||
}
|
||||
}).thenCompose(ignored -> loadChunks()).thenCompose(ignored -> {
|
||||
World world = Bukkit.getWorld(WORLD_KEY);
|
||||
return J.runRegionFuture(world, 0, 0, () -> preparePlatform(world));
|
||||
}).thenAccept(ignored -> {
|
||||
World world = Bukkit.getWorld(WORLD_KEY);
|
||||
if (!J.runEntity(player, () -> player.teleportAsync(new Location(world, 8.5, TEST_Y + 3, 8.5))
|
||||
.whenComplete((teleported, failure) -> {
|
||||
if (failure != null || !Boolean.TRUE.equals(teleported)) {
|
||||
fail(player, failure == null ? new IllegalStateException("Fixture teleport failed") : failure);
|
||||
return;
|
||||
}
|
||||
player.sendMessage("CE_READY world=" + WORLD_KEY + " folia=" + J.isFolia());
|
||||
}))) {
|
||||
throw new IllegalStateException("Could not schedule fixture teleport");
|
||||
}
|
||||
}).exceptionally(failure -> {
|
||||
fail(player, failure);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
private void importedFeatures(Player player) {
|
||||
CompletableFuture.runAsync(() -> {
|
||||
if (Bukkit.getWorld(FEATURE_WORLD_KEY) == null) {
|
||||
try {
|
||||
new IrisCreator().name(FEATURE_WORLD_KEY.toString()).dimension("ce-native-feature-fixture")
|
||||
.seed(78264193L).create();
|
||||
} catch (Exception failure) {
|
||||
throw new IllegalStateException("Could not create imported feature fixture world", failure);
|
||||
}
|
||||
}
|
||||
}).thenCompose(ignored -> {
|
||||
World world = Bukkit.getWorld(FEATURE_WORLD_KEY);
|
||||
return world.getChunkAtAsync(0, 0, true);
|
||||
}).thenCompose(chunk -> J.runRegionFuture(chunk.getWorld(), 0, 0, () -> {
|
||||
World world = chunk.getWorld();
|
||||
Engine engine = IrisToolbelt.access(world).getEngine();
|
||||
int snow = 0;
|
||||
int pending = 0;
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
if (world.getBlockAt(x, 97, z).getType() == Material.SNOW) {
|
||||
snow++;
|
||||
}
|
||||
if (engine.getMantle().getMantle().get(x, 97 - engine.getMinHeight(), z, Identifier.class) != null) {
|
||||
pending++;
|
||||
}
|
||||
}
|
||||
}
|
||||
require(snow == 256, "Imported snow feature did not cover all 256 test columns: " + snow);
|
||||
require(pending == 0, "Imported feature left pending furniture records: " + pending);
|
||||
for (Entity entity : chunk.getEntities()) {
|
||||
require(!CraftEngineFurniture.isFurniture(entity), "Imported snow feature left generated furniture");
|
||||
}
|
||||
if (!J.runEntity(player, () -> player.teleportAsync(new Location(world, 8.5, 101, 8.5))
|
||||
.whenComplete((teleported, failure) -> {
|
||||
if (failure != null || !Boolean.TRUE.equals(teleported)) {
|
||||
fail(player, failure == null ? new IllegalStateException("Feature world teleport failed") : failure);
|
||||
return;
|
||||
}
|
||||
player.sendMessage("CE_FEATURES snow=256 pending=0 furniture=0");
|
||||
}))) {
|
||||
throw new IllegalStateException("Could not schedule feature world teleport");
|
||||
}
|
||||
})).exceptionally(failure -> {
|
||||
fail(player, failure);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
private CompletableFuture<Void> loadChunks() {
|
||||
World world = Bukkit.getWorld(WORLD_KEY);
|
||||
require(world != null && IrisToolbelt.isIrisWorld(world), "Fixture must use a real Iris world");
|
||||
List<CompletableFuture<Void>> chunks = new ArrayList<>(15);
|
||||
for (int x = -1; x <= 3; x++) {
|
||||
for (int z = -1; z <= 1; z++) {
|
||||
chunks.add(world.getChunkAtAsync(x, z, true).thenCompose(chunk ->
|
||||
J.runRegionFuture(world, chunk.getX(), chunk.getZ(), () -> chunk.addPluginChunkTicket(this))));
|
||||
}
|
||||
}
|
||||
return CompletableFuture.allOf(chunks.toArray(CompletableFuture[]::new));
|
||||
}
|
||||
|
||||
private void preparePlatform(World world) {
|
||||
require(CraftEngineBlocks.byId(Key.of("default:topaz_ore")) != null, "CraftEngine default block is missing");
|
||||
require(CraftEngineFurniture.byId(Key.of("default:flower_basket")) != null, "CraftEngine furniture is missing");
|
||||
for (int chunkX = 0; chunkX <= 2; chunkX++) {
|
||||
for (Entity entity : world.getChunkAt(chunkX, 0).getEntities()) {
|
||||
if (CraftEngineFurniture.isFurniture(entity)) {
|
||||
CraftEngineFurniture.remove(entity, false, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int x = 0; x <= 47; x++) {
|
||||
for (int z = 2; z <= 14; z++) {
|
||||
world.getBlockAt(x, TEST_Y - 1, z).setType(Material.STONE, false);
|
||||
for (int y = TEST_Y; y <= TEST_Y + 5; y++) {
|
||||
world.getBlockAt(x, y, z).setType(Material.AIR, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void pending(Player player, World world) {
|
||||
Engine engine = IrisToolbelt.access(world).getEngine();
|
||||
Mantle<Matter> mantle = engine.getMantle().getMantle();
|
||||
queue(engine, world, 2, BLOCK_ID);
|
||||
queue(engine, world, 4, BLOCK_ID);
|
||||
queue(engine, world, 6, BLOCK_ID);
|
||||
queue(engine, world, 8, BLOCK_ID);
|
||||
queue(engine, world, 10, FURNITURE_ID);
|
||||
queue(engine, world, 12, FURNITURE_ID);
|
||||
Block chest = world.getBlockAt(14, TEST_Y, TEST_Z);
|
||||
chest.setType(Material.CHEST, false);
|
||||
mantle.set(14, TEST_Y - engine.getMinHeight(), TEST_Z, new TileWrapper(TileData.getTileState(chest, false)));
|
||||
world.getBlockAt(4, TEST_Y, TEST_Z).setType(Material.AIR, false);
|
||||
world.getBlockAt(6, TEST_Y, TEST_Z).setType(Material.COBBLESTONE, false);
|
||||
|
||||
NativeBlockChangeSet changes = DeferredPlacementRecords.track(engine, bounds(world));
|
||||
changes.add(8, TEST_Y, TEST_Z);
|
||||
changes.add(12, TEST_Y - 1, TEST_Z);
|
||||
changes.add(12, TEST_Y, TEST_Z);
|
||||
changes.add(14, TEST_Y, TEST_Z);
|
||||
world.getBlockAt(8, TEST_Y, TEST_Z).setType(Material.AIR, false);
|
||||
world.getBlockAt(12, TEST_Y - 1, TEST_Z).setType(Material.AIR, false);
|
||||
chest.setType(Material.COBBLESTONE, false);
|
||||
DeferredPlacementRecords.invalidate(engine, changes);
|
||||
require(record(engine, 8, Identifier.class) == null, "Native cut retained a pending custom record");
|
||||
require(record(engine, 12, Identifier.class) == null, "Flatten column retained pending furniture");
|
||||
require(record(engine, 14, TileWrapper.class) == null, "Native wall retained pending chest data");
|
||||
require(record(engine, 2, Identifier.class) != null, "Native writes invalidated an untouched custom record");
|
||||
replay(engine, world, 0);
|
||||
CustomBlockOwnership ownership = record(engine, 10, CustomBlockOwnership.class);
|
||||
require(record(engine, 2, Identifier.class) == null, "Custom materialization did not consume pending records");
|
||||
require(CraftEngineBlocks.isCustomBlock(world.getBlockAt(2, TEST_Y, TEST_Z)), "Intact custom block was not placed");
|
||||
require(world.getBlockAt(4, TEST_Y, TEST_Z).getType().isAir(), "Cut custom block was resurrected");
|
||||
require(world.getBlockAt(6, TEST_Y, TEST_Z).getType() == Material.COBBLESTONE, "Custom block replaced native wall");
|
||||
require(world.getBlockAt(8, TEST_Y, TEST_Z).getType().isAir(), "Invalidated custom block was resurrected");
|
||||
require(chest.getType() == Material.COBBLESTONE, "Chest data replaced native wall");
|
||||
later(player, world, () -> {
|
||||
int intactFurniture = furnitureAt(world, 10);
|
||||
require(intactFurniture == 1, "Intact furniture count is " + intactFurniture
|
||||
+ "; pending=" + record(engine, 10, Identifier.class)
|
||||
+ "; ownership=" + ownership
|
||||
+ "; chunkEntities=" + world.getChunkAt(0, 0).getEntities().length);
|
||||
require(furnitureAt(world, 12) == 0, "Flattened furniture was resurrected");
|
||||
player.sendMessage("CE_PENDING intact=1 air=1 wall=1 invalidated=1 flatten=1 chest=1 furniture=1");
|
||||
});
|
||||
}
|
||||
|
||||
private void neighbor(Player player, World world) {
|
||||
Engine engine = IrisToolbelt.access(world).getEngine();
|
||||
queue(engine, world, 18, FURNITURE_ID);
|
||||
queue(engine, world, 20, FURNITURE_ID);
|
||||
queue(engine, world, 22, FURNITURE_ID);
|
||||
queue(engine, world, 42, FURNITURE_ID);
|
||||
replay(engine, world, 1);
|
||||
replay(engine, world, 2);
|
||||
world.getBlockAt(40, TEST_Y, TEST_Z).setType(Material.DIAMOND_BLOCK, false);
|
||||
later(player, world, () -> {
|
||||
require(furnitureAt(world, 18) == 1 && furnitureAt(world, 20) == 1
|
||||
&& furnitureAt(world, 22) == 1, "Neighbor furniture did not materialize");
|
||||
require(furnitureAt(world, 42) == 1 && record(engine, 42, CustomBlockOwnership.class) != null,
|
||||
"Regeneration target furniture did not materialize with saved ownership");
|
||||
if (!J.runEntity(player, () -> player.teleportAsync(new Location(world, 40.5, TEST_Y + 3, TEST_Z + 0.5))
|
||||
.whenComplete((teleported, failure) -> {
|
||||
if (failure != null || !Boolean.TRUE.equals(teleported)) {
|
||||
fail(player, failure == null ? new IllegalStateException("Regeneration target teleport failed") : failure);
|
||||
return;
|
||||
}
|
||||
player.sendMessage("CE_NEIGHBOR_READY chunk=2,0 furniture=3 target=1");
|
||||
}))) {
|
||||
throw new IllegalStateException("Could not schedule regeneration target teleport");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void invalidateLoaded(Player player, World world, Engine engine) {
|
||||
require(world.getBlockAt(40, TEST_Y, TEST_Z).getType().isAir(), "Adjacent target chunk did not regenerate");
|
||||
require(furnitureAt(world, 18) == 1 && furnitureAt(world, 20) == 1
|
||||
&& furnitureAt(world, 22) == 1, "Neighbor furniture did not materialize");
|
||||
NativeBlockChangeSet changes = DeferredPlacementRecords.track(engine, bounds(world));
|
||||
changes.add(18, TEST_Y - 1, TEST_Z);
|
||||
changes.add(18, TEST_Y, TEST_Z);
|
||||
changes.add(22, TEST_Y, TEST_Z);
|
||||
world.getBlockAt(18, TEST_Y - 1, TEST_Z).setType(Material.AIR, false);
|
||||
world.getBlockAt(18, TEST_Y, TEST_Z).setType(Material.COBBLESTONE, false);
|
||||
DeferredPlacementRecords.invalidate(engine, changes);
|
||||
require(CraftEngineFurniture.place(new Location(world, 22.5, TEST_Y, TEST_Z + 0.5),
|
||||
Key.of("default:flower_basket"), "ground", false) != null,
|
||||
"Could not place replacement furniture before scheduled cleanup");
|
||||
later(player, world, () -> {
|
||||
require(furnitureAt(world, 42) == 0, "Regeneration retained target chunk furniture");
|
||||
require(furnitureAt(world, 18) == 0, "Native write left materialized neighbor furniture");
|
||||
require(furnitureAt(world, 20) == 1, "Native write removed untouched neighbor furniture");
|
||||
require(furnitureAt(world, 22) == 1, "Native cleanup removed replacement furniture or retained the old one");
|
||||
require(world.getBlockAt(18, TEST_Y, TEST_Z).getType() == Material.COBBLESTONE,
|
||||
"Furniture cleanup replaced the native wall");
|
||||
player.sendMessage("CE_LOADED removed=1 untouched=1 replacement=1 wall=1 target=1");
|
||||
});
|
||||
}
|
||||
|
||||
private void restarted(Player player, World world) {
|
||||
require(furnitureAt(world, 20) == 1, "Generated neighbor furniture did not survive restart");
|
||||
require(furnitureAt(world, 10) == 1, "Untouched furniture did not survive restart");
|
||||
Engine engine = IrisToolbelt.access(world).getEngine();
|
||||
NativeBlockChangeSet changes = DeferredPlacementRecords.track(engine, bounds(world));
|
||||
changes.add(20, TEST_Y, TEST_Z);
|
||||
DeferredPlacementRecords.invalidate(engine, changes);
|
||||
later(player, world, () -> {
|
||||
require(furnitureAt(world, 20) == 0, "Persisted furniture ownership was not invalidated after restart");
|
||||
require(furnitureAt(world, 10) == 1, "Restart cleanup removed untouched furniture");
|
||||
player.sendMessage("CE_RESTART removed=1 untouched=1");
|
||||
});
|
||||
}
|
||||
|
||||
private void queue(Engine engine, World world, int x, String identifier) {
|
||||
ExternalDataSVC service = IrisServices.get(ExternalDataSVC.class);
|
||||
BlockData data = service.getBlockData(Identifier.fromString(identifier)).orElseThrow();
|
||||
require(data instanceof IrisCustomData, "Iris did not resolve real CraftEngine content: " + identifier);
|
||||
IrisCustomData custom = (IrisCustomData) data;
|
||||
world.getBlockAt(x, TEST_Y, TEST_Z).setBlockData(custom.getBase(), false);
|
||||
engine.getMantle().getMantle().set(x, TEST_Y - engine.getMinHeight(), TEST_Z, custom.getCustom());
|
||||
}
|
||||
|
||||
private <T> T record(Engine engine, int x, Class<T> type) {
|
||||
return engine.getMantle().getMantle().get(x, TEST_Y - engine.getMinHeight(), TEST_Z, type);
|
||||
}
|
||||
|
||||
private void replay(Engine engine, World world, int chunkX) {
|
||||
Mantle<Matter> mantle = engine.getMantle().getMantle();
|
||||
for (int x = chunkX - 1; x <= chunkX + 1; x++) {
|
||||
for (int z = -1; z <= 1; z++) {
|
||||
require(world.isChunkLoaded(x, z), "Replay neighbor is not loaded: " + x + "," + z);
|
||||
}
|
||||
}
|
||||
require(mantle.isLoaded(world.getChunkAt(chunkX, 0)), "Replay mantle chunk is not loaded: " + chunkX);
|
||||
mantle.flag(chunkX, 0, MantleFlag.ETCHED, false);
|
||||
mantle.flag(chunkX, 0, MantleFlag.TILE, false);
|
||||
mantle.flag(chunkX, 0, MantleFlag.CUSTOM, false);
|
||||
EngineBukkitOps.updateChunk(engine, world.getChunkAt(chunkX, 0), mantle);
|
||||
}
|
||||
|
||||
private int furnitureAt(World world, int x) {
|
||||
int count = 0;
|
||||
for (Entity entity : world.getChunkAt(x >> 4, TEST_Z >> 4).getEntities()) {
|
||||
Location location = entity.getLocation();
|
||||
if (location.getBlockX() == x && location.getBlockY() == TEST_Y && location.getBlockZ() == TEST_Z
|
||||
&& CraftEngineFurniture.getLoadedFurnitureByMetaEntity(entity) != null) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private void later(Player player, World world, Runnable assertion) {
|
||||
Bukkit.getRegionScheduler().runDelayed(this, new Location(world, 8, TEST_Y, TEST_Z), task -> {
|
||||
try {
|
||||
assertion.run();
|
||||
} catch (Throwable failure) {
|
||||
fail(player, failure);
|
||||
}
|
||||
}, 20);
|
||||
}
|
||||
|
||||
private NativeBlockChangeSet.Bounds bounds(World world) {
|
||||
return new NativeBlockChangeSet.Bounds(-1, -1, 3, 1, world.getMinHeight(), world.getMaxHeight() - world.getMinHeight());
|
||||
}
|
||||
|
||||
private void require(boolean condition, String message) {
|
||||
if (!condition) {
|
||||
throw new IllegalStateException(message);
|
||||
}
|
||||
}
|
||||
|
||||
private void fail(CommandSender sender, Throwable failure) {
|
||||
getLogger().log(Level.SEVERE, "CraftEngine native placement assertion failed", failure);
|
||||
sender.sendMessage("CE_FAILED " + failure.getMessage());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "Flat",
|
||||
"derivative": "minecraft:plains",
|
||||
"vanillaDerivative": "minecraft:plains",
|
||||
"layers": [
|
||||
{
|
||||
"minHeight": 1,
|
||||
"maxHeight": 1,
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:lime_wool"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"minHeight": 384,
|
||||
"maxHeight": 384,
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:green_concrete"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"generators": [
|
||||
{
|
||||
"generator": "flat",
|
||||
"min": 96,
|
||||
"max": 96
|
||||
}
|
||||
]
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "CraftEngine Native Fixture",
|
||||
"version": 1,
|
||||
"mode": {
|
||||
"type": "OVERWORLD"
|
||||
},
|
||||
"regions": [
|
||||
"flat"
|
||||
],
|
||||
"focusRegion": "flat",
|
||||
"focus": "flat",
|
||||
"landChance": 1,
|
||||
"environment": "NORMAL",
|
||||
"dimensionHeight": {
|
||||
"min": -64,
|
||||
"max": 320
|
||||
},
|
||||
"logicalHeight": 384,
|
||||
"fluidHeight": 0,
|
||||
"bedrock": false,
|
||||
"rockPalette": {
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:red_concrete"
|
||||
}
|
||||
]
|
||||
},
|
||||
"importedStructures": {
|
||||
"disabled": [
|
||||
"minecraft:"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"interpolator": {
|
||||
"function": "NONE",
|
||||
"horizontalScale": 1
|
||||
},
|
||||
"seed": 310,
|
||||
"composite": [
|
||||
{
|
||||
"seed": 310,
|
||||
"style": {
|
||||
"style": "FLAT"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "Flat",
|
||||
"landBiomes": [
|
||||
"flat"
|
||||
],
|
||||
"seaBiomes": [
|
||||
"flat"
|
||||
],
|
||||
"shoreBiomes": [
|
||||
"flat"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
|
||||
export default {
|
||||
name: 'iris-craftengine-native-restart',
|
||||
description: 'Verify persisted generated furniture ownership after a full server restart.',
|
||||
async run(context) {
|
||||
const source = await readFile(path.join(context.server.directory, '.server-source'), 'utf8')
|
||||
context.expect(/^isolated=true\s*$/m.test(source), 'CraftEngine regression requires an isolated instance')
|
||||
await context.step('native writes remove persisted ownership after restart', async () => {
|
||||
const response = await context.command('/iriscenative restart', /CE_RESTART|CE_FAILED/, 60000)
|
||||
context.expect(response.includes('CE_RESTART removed=1 untouched=1'), response)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
|
||||
export default {
|
||||
name: 'iris-craftengine-native',
|
||||
description: 'Verify deferred native invalidation and loaded neighbor cleanup with real Iris and CraftEngine.',
|
||||
async run(context) {
|
||||
const source = await readFile(path.join(context.server.directory, '.server-source'), 'utf8')
|
||||
context.expect(/^isolated=true\s*$/m.test(source), 'CraftEngine regression requires an isolated instance')
|
||||
await context.step('create and enter a real Iris world with CraftEngine content', async () => {
|
||||
const response = await context.command('/iriscenative prepare', /CE_READY|CE_FAILED/, 180000)
|
||||
context.expect(response.includes('CE_READY'), response)
|
||||
})
|
||||
await context.step('native writes remove pending block, chest, and flatten furniture records', async () => {
|
||||
const response = await context.command('/iriscenative pending', /CE_PENDING|CE_FAILED/, 60000)
|
||||
context.expect(response.includes('CE_PENDING intact=1 air=1 wall=1 invalidated=1 flatten=1 chest=1 furniture=1'), response)
|
||||
await context.waitUntil(() => {
|
||||
const origin = context.bot.entity.position.floored()
|
||||
const cut = context.bot.blockAt(origin.set(4, 180, 8))
|
||||
const wall = context.bot.blockAt(origin.set(6, 180, 8))
|
||||
const invalidated = context.bot.blockAt(origin.set(8, 180, 8))
|
||||
const chest = context.bot.blockAt(origin.set(14, 180, 8))
|
||||
return cut?.name === 'air' && wall?.name === 'cobblestone'
|
||||
&& invalidated?.name === 'air' && chest?.name === 'cobblestone'
|
||||
}, { label: 'Client observes retained native air and walls', timeoutMs: 30000, intervalMs: 100 })
|
||||
})
|
||||
await context.step('regeneration removes target furniture and preserves neighbor ownership', async () => {
|
||||
const ready = await context.command('/iriscenative neighbor', /CE_NEIGHBOR_READY|CE_FAILED/, 30000)
|
||||
context.expect(ready.includes('CE_NEIGHBOR_READY chunk=2,0 furniture=3 target=1'), ready)
|
||||
const regenerated = await context.command('/iris developer regen radius=0', /Regen complete:|Regen failed:/, 90000)
|
||||
context.expect(regenerated.includes('Regen complete:'), regenerated)
|
||||
const response = await context.command('/iriscenative loaded', /CE_LOADED|CE_FAILED/, 30000)
|
||||
context.expect(response.includes('CE_LOADED removed=1 untouched=1 replacement=1 wall=1 target=1'), response)
|
||||
})
|
||||
await context.step('imported snow features replace generated furniture records', async () => {
|
||||
const response = await context.command('/iriscenative features', /CE_FEATURES|CE_FAILED/, 180000)
|
||||
context.expect(response.includes('CE_FEATURES snow=256 pending=0 furniture=0'), response)
|
||||
await context.waitUntil(() => {
|
||||
const position = context.bot.entity.position.floored()
|
||||
for (let x = 0; x < 16; x++) {
|
||||
for (let z = 0; z < 16; z++) {
|
||||
if (context.bot.blockAt(position.set(x, 97, z))?.name !== 'snow') return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}, { label: 'Client observes 256 imported snow layers', timeoutMs: 30000, intervalMs: 100 })
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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 }))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import { readFile, realpath } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
|
||||
const nonGround = new Set(['air', 'cave_air', 'void_air', 'water', 'lava', 'nether_wart_block', 'warped_wart_block'])
|
||||
const chunkOffsets = [-56, -40, -24, -8, 8, 24, 40, 56]
|
||||
|
||||
function isWood(block) {
|
||||
return block !== null && /_(wood|hyphae|log|stem)$/.test(block.name)
|
||||
}
|
||||
|
||||
function isGround(block) {
|
||||
return block !== null && block.boundingBox === 'block' && !nonGround.has(block.name)
|
||||
&& !block.name.endsWith('_leaves') && !isWood(block)
|
||||
}
|
||||
|
||||
function positionKey(position) {
|
||||
return `${position.x},${position.y},${position.z}`
|
||||
}
|
||||
|
||||
function groundSpread(bot, base) {
|
||||
const heights = []
|
||||
for (const x of [-3, 0, 3]) {
|
||||
for (const z of [-3, 0, 3]) {
|
||||
for (let y = base.y + 6; y >= base.y - 8; y--) {
|
||||
if (!isGround(bot.blockAt(base.offset(x, y - base.y, z)))) continue
|
||||
heights.push(y)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return heights.length === 9 ? Math.max(...heights) - Math.min(...heights) : null
|
||||
}
|
||||
|
||||
function inspectTrees(context) {
|
||||
const bot = context.bot
|
||||
const center = bot.entity.position.floored()
|
||||
const woodIds = Object.values(bot.registry.blocksByName).filter(isWood).map(block => block.id)
|
||||
const positions = bot.findBlocks({ matching: woodIds, point: center, maxDistance: 64, count: 20000 })
|
||||
const columns = new Map()
|
||||
for (const position of positions) {
|
||||
const key = `${position.x},${position.z}`
|
||||
if (!columns.has(key)) columns.set(key, [])
|
||||
columns.get(key).push(position)
|
||||
}
|
||||
const trees = []
|
||||
for (const column of columns.values()) {
|
||||
column.sort((first, second) => first.y - second.y)
|
||||
const base = column[0]
|
||||
const height = column[column.length - 1].y - base.y + 1
|
||||
if (column.length < 8 || height < 8 || column.length / height < 0.65) continue
|
||||
if (column.some(position => position.distanceTo(center) >= 60)) continue
|
||||
if (![0, 1, 2, 3].every(y => isWood(bot.blockAt(base.offset(0, y, 0))))) continue
|
||||
const upperGaps = []
|
||||
for (let y = base.y; y < base.y + height; y++) {
|
||||
const block = bot.blockAt(base.offset(0, y - base.y, 0))
|
||||
if (block === null || !isWood(block)) upperGaps.push({ y, block: block?.name ?? null })
|
||||
}
|
||||
const grounded = isGround(bot.blockAt(base.offset(0, -1, 0)))
|
||||
const spread = groundSpread(bot, base)
|
||||
trees.push({ base: { x: base.x, y: base.y, z: base.z }, woodBlocks: column.length,
|
||||
height, grounded, terrainSpread: spread, upperGaps })
|
||||
}
|
||||
return trees
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'iris-tree-grounding',
|
||||
description: 'Verify grounded spindle bases, continuous overlapping trunks, and paired pack geometry.',
|
||||
async run(context) {
|
||||
const directory = await realpath(context.server.directory)
|
||||
const source = await readFile(path.join(directory, '.server-source'), 'utf8')
|
||||
context.expect(/^isolated=true\s*$/m.test(source), 'Tree generation requires an isolated instance')
|
||||
const packs = await realpath(path.join(directory, 'plugins', 'Iris', 'packs'))
|
||||
context.expect(packs.startsWith(`${directory}${path.sep}`), 'Tree tests require local pack copies')
|
||||
context.report.treeWorlds = []
|
||||
context.report.treeGrounding = []
|
||||
context.report.treeContinuity = []
|
||||
for (const pack of ['overworld', 'underworld']) {
|
||||
const dimension = JSON.parse(await readFile(path.join(packs, pack, 'dimensions', `${pack}.json`), 'utf8'))
|
||||
context.expect(dimension.focus === 'estranged/spindlewood', 'Pack must focus on spindlewood', { pack })
|
||||
await context.step(`create ${pack} spindle world`, async () => {
|
||||
const world = `tree_${pack}_${Date.now().toString(36)}`
|
||||
const response = await context.command(`/iris create name=${world} type=${pack} seed=78264193`,
|
||||
/Successfully created your world|Failed to create/, 240000)
|
||||
context.expect(response.includes('Successfully created your world'), response)
|
||||
context.report.treeWorlds.push({ pack, name: world, seed: 78264193, biome: dimension.focus })
|
||||
if (context.bot.game.gameMode !== 'spectator') {
|
||||
await context.command('/minecraft:gamemode spectator', /spectator/i, 30000)
|
||||
}
|
||||
await context.waitUntil(() => context.bot.game.gameMode === 'spectator',
|
||||
{ label: 'Spectator mode for terrain inspection', timeoutMs: 30000, intervalMs: 100 })
|
||||
})
|
||||
await context.step(`${pack} trees connect to flat and sloped terrain`, async () => {
|
||||
const origin = context.bot.entity.position.floored()
|
||||
const inspected = new Map()
|
||||
for (const offset of [0, 80, 160]) {
|
||||
await context.command(`/minecraft:tp @s ${origin.x + offset + 0.5} ${origin.y + 12} ${origin.z + 0.5}`,
|
||||
/Teleported/i, 30000)
|
||||
await context.waitUntil(() => {
|
||||
const center = context.bot.entity.position.floored()
|
||||
if (Math.abs(center.x - origin.x - offset) > 1) return false
|
||||
return chunkOffsets.every(x => chunkOffsets.every(z => context.bot.blockAt(center.offset(x, 0, z)) !== null))
|
||||
}, { label: 'Complete terrain neighborhood', timeoutMs: 90000, intervalMs: 250 })
|
||||
const trees = inspectTrees(context)
|
||||
for (const tree of trees) {
|
||||
context.expect(tree.grounded, 'Generated spindle trunk base has no terrain connection', { pack, ...tree })
|
||||
inspected.set(positionKey(tree.base), tree)
|
||||
}
|
||||
if (inspected.size >= 8 && [...inspected.values()].some(tree => tree.terrainSpread >= 2)) break
|
||||
}
|
||||
const trees = [...inspected.values()]
|
||||
const sloped = trees.filter(tree => tree.terrainSpread >= 2)
|
||||
context.expect(trees.length >= 8, 'Expected at least eight generated spindle trunk bases', { pack, trees })
|
||||
context.expect(sloped.length > 0, 'Expected a grounded tree on uneven terrain', { pack, trees })
|
||||
context.report.treeGrounding.push({ pack, trees: trees.length, sloped: sloped.length, samples: trees })
|
||||
})
|
||||
await context.step(`${pack} overlapping canopy preserves the seeded main trunk`, async () => {
|
||||
await context.command('/minecraft:tp @s 0.5 88 0.5', /Teleported/i, 30000)
|
||||
await context.waitUntil(() => {
|
||||
const center = context.bot.entity.position.floored()
|
||||
return Math.abs(center.x) < 2 && Math.abs(center.z) < 2
|
||||
&& context.bot.blockAt(center.offset(-7 - center.x, 69 - center.y, 6 - center.z)) !== null
|
||||
}, { label: 'Seeded trunk neighborhood', timeoutMs: 90000, intervalMs: 250 })
|
||||
const center = context.bot.entity.position.floored()
|
||||
const blocks = []
|
||||
for (let y = 69; y <= 83; y++) {
|
||||
const block = context.bot.blockAt(center.offset(-7 - center.x, y - center.y, 6 - center.z))
|
||||
context.expect(isWood(block), 'Overlapping canopy interrupted the seeded main trunk',
|
||||
{ pack, x: -7, y, z: 6, block: block?.name ?? null })
|
||||
blocks.push({ y, block: block.name })
|
||||
}
|
||||
context.report.treeContinuity.push({ pack, x: -7, z: 6, blocks })
|
||||
})
|
||||
}
|
||||
await context.step('paired packs retain matching sampled tree geometry', async () => {
|
||||
const geometry = context.report.treeGrounding.map(result => result.samples.map(tree => ({
|
||||
base: tree.base, woodBlocks: tree.woodBlocks, height: tree.height, terrainSpread: tree.terrainSpread
|
||||
})).sort((first, second) => positionKey(first.base).localeCompare(positionKey(second.base))))
|
||||
context.expect(JSON.stringify(geometry[0]) === JSON.stringify(geometry[1]),
|
||||
'Paired spindle packs produced different sampled geometry', { geometry })
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,7 @@ apply from: file('../../gradle/volmlib-stamp.gradle')
|
||||
|
||||
Properties rootProperties = new Properties()
|
||||
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
|
||||
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.3.4'))
|
||||
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.3.5'))
|
||||
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.3'))
|
||||
String fabricLoaderVersion = providers.gradleProperty('fabricLoaderVersion').getOrElse(irisModTarget.fabricLoader.toString())
|
||||
Closure<String> irisArtifactName = { String platform, String targetVersion ->
|
||||
|
||||
@@ -33,6 +33,8 @@ plugins {
|
||||
|
||||
rootProject.name = 'iris-fabric'
|
||||
|
||||
apply from: new File(settingsDir, '../../gradle/runtime-cache.settings.gradle').canonicalFile
|
||||
|
||||
dependencyResolutionManagement {
|
||||
versionCatalogs {
|
||||
libs {
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
|
||||
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
||||
import net.minecraftforge.gradle.shadow.net.minecraftforge.gradleutils.shared.EnhancedPlugin
|
||||
import org.gradle.api.file.DeleteSpec
|
||||
import org.gradle.api.file.FileCopyDetails
|
||||
import org.gradle.api.file.FileSystemOperations
|
||||
@@ -59,9 +60,20 @@ apply from: file('../../gradle/volmlib-stamp.gradle')
|
||||
|
||||
Properties rootProperties = new Properties()
|
||||
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
|
||||
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.3.4'))
|
||||
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.3.5'))
|
||||
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.3'))
|
||||
String forgeVersion = providers.gradleProperty('forgeVersion').getOrElse(irisModTarget.forge.toString())
|
||||
String buildLane = gradle.extensions.extraProperties.has('irisBuildLane')
|
||||
? gradle.extensions.extraProperties.get('irisBuildLane').toString() : null
|
||||
if (buildLane != null) {
|
||||
File laneCache = providers.gradleProperty('multiplexorSandbox').getOrElse('false').toBoolean()
|
||||
? new File(gradle.gradleUserHomeDir, "caches/iris-forge/${buildLane}")
|
||||
: file(".gradle/iris-lanes/${buildLane}")
|
||||
plugins.withType(EnhancedPlugin).configureEach { EnhancedPlugin<?> forgePlugin ->
|
||||
forgePlugin.globalCaches().set(new File(laneCache, 'forgegradle'))
|
||||
}
|
||||
minecraft.mavenizerOutput.set(new File(laneCache, 'mavenizer/repo'))
|
||||
}
|
||||
Closure<String> loaderDisplayVersion = { String loaderVersion ->
|
||||
String coordinatePrefix = "${minecraftVersion}-"
|
||||
if (loaderVersion.startsWith(coordinatePrefix)) {
|
||||
@@ -161,7 +173,9 @@ configurations.testRuntimeClasspath.extendsFrom(configurations.runtimeClasspath,
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation minecraft.dependency("net.minecraftforge:forge:${forgeVersion}")
|
||||
implementation(buildLane == null
|
||||
? minecraft.dependency("net.minecraftforge:forge:${forgeVersion}")
|
||||
: minecraft.dependency("iris-${buildLane}", "net.minecraftforge:forge:${forgeVersion}"))
|
||||
testImplementation('junit:junit:4.13.2')
|
||||
testImplementation('org.mockito:mockito-core:5.23.0')
|
||||
testImplementation(testFixtures('art.arcane:core'))
|
||||
|
||||
@@ -33,6 +33,8 @@ plugins {
|
||||
|
||||
rootProject.name = 'iris-forge'
|
||||
|
||||
apply from: new File(settingsDir, '../../gradle/runtime-cache.settings.gradle').canonicalFile
|
||||
|
||||
dependencyResolutionManagement {
|
||||
versionCatalogs {
|
||||
libs {
|
||||
|
||||
+4
-2
@@ -57,6 +57,8 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedChunkGenera
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeGeneratorContext;
|
||||
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedStructureStage;
|
||||
import art.arcane.iris.world.safeguard.GenerationRefusalNotice;
|
||||
@@ -829,12 +831,12 @@ public final class IrisModdedChunkGenerator implements NativeGeneratorOwner, Nat
|
||||
importedFeatures.invalidate();
|
||||
}
|
||||
|
||||
public void updateRegeneratedChunk(int chunkX, int chunkZ) {
|
||||
public void updateRegeneratedChunk(Mantle<Matter> mantle, int chunkX, int chunkZ) {
|
||||
ModdedChunkUpdateService service = ModdedEngineBootstrap.services().service(ModdedChunkUpdateService.class);
|
||||
if (service == null) {
|
||||
throw new IllegalStateException("Iris chunk update service is unavailable during regeneration");
|
||||
}
|
||||
service.updateRegeneratedChunk(engine(), boundLevel(), chunkX, chunkZ);
|
||||
service.updateRegeneratedChunk(engine(), boundLevel(), mantle, chunkX, chunkZ);
|
||||
}
|
||||
|
||||
public NativeBiomeResolver regenBiomeResolver() {
|
||||
|
||||
+12
@@ -2,6 +2,7 @@ package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.volmlib.nativelib.terrain.structure.StructureStagePolicy;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.mantle.DeferredPlacementRecords;
|
||||
import art.arcane.iris.generation.terrain.IrisMaterialPalette;
|
||||
import art.arcane.iris.structure.nativegen.GenerationWritePolicy;
|
||||
import art.arcane.iris.structure.nativegen.IrisStructurePolicy;
|
||||
@@ -13,6 +14,7 @@ import art.arcane.iris.structure.nativegen.StructureLocateSearch;
|
||||
import art.arcane.iris.structure.placement.IrisStructureLocator;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedStructureStage;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureVolumeSource;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockChangeSet;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockPositionPredicate;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.nativelib.terrain.structure.StructureLocateSearchAccess;
|
||||
@@ -107,6 +109,16 @@ final class ModdedNativeStructurePolicy implements NativeModdedStructureStage.Po
|
||||
context.getDimensionStackContext(), context.getMinHeight());
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeBlockChangeSet trackBlockChanges(Engine context, NativeBlockChangeSet.Bounds bounds) {
|
||||
return DeferredPlacementRecords.track(context, bounds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidateBlockChanges(Engine context, NativeBlockChangeSet changes) {
|
||||
DeferredPlacementRecords.invalidate(context, changes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IntBinaryOperator surfaceHeight(Engine context) {
|
||||
return (x, z) -> Engine.hostHeight(context, x, z, true) + context.getMinHeight();
|
||||
|
||||
+25
-4
@@ -19,6 +19,8 @@
|
||||
package art.arcane.iris.modded.api;
|
||||
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.mantle.CustomBlockOwnership;
|
||||
import art.arcane.iris.integration.Identifier;
|
||||
import art.arcane.iris.modded.ModdedIrisLog;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.ModdedBlockState;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeBlockPlacement;
|
||||
@@ -297,11 +299,11 @@ public final class ModdedCustomContentRegistry {
|
||||
* consulted for that position. An unparseable key or no matching provider is logged and skipped. Called on the
|
||||
* server thread with the chunk loaded.
|
||||
*/
|
||||
public static void processBlockPlacement(Engine engine, String key, Supplier<NativeBlockPlacement> placement) {
|
||||
public static CustomBlockOwnership.Entry processBlockPlacement(Engine engine, String key, Supplier<NativeBlockPlacement> placement) {
|
||||
String base = parseIdentifier(key);
|
||||
if (base == null) {
|
||||
ModdedIrisLog.warn("Iris deferred custom block placement rejected invalid id {}", key);
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
Map<String, String> state = parseState(key);
|
||||
for (ModdedDataProvider provider : PROVIDERS) {
|
||||
@@ -309,14 +311,33 @@ public final class ModdedCustomContentRegistry {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
provider.processBlockPlacement(new ModdedBlockPlacementContext(
|
||||
String token = provider.processBlockPlacement(new ModdedBlockPlacementContext(
|
||||
engine, placement.get(), base, state));
|
||||
int properties = key.indexOf('[');
|
||||
String qualifiedKey = base.replace(':', '/') + (properties < 0 ? "" : key.substring(properties));
|
||||
return token == null ? null : new CustomBlockOwnership.Entry(
|
||||
new Identifier(provider.modId(), qualifiedKey), token, false);
|
||||
} catch (Throwable error) {
|
||||
ModdedIrisLog.error("Iris custom content provider '{}' failed post-placement for {}", provider.modId(), key, error);
|
||||
}
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
ModdedIrisLog.warn("Iris deferred custom block placement has no provider for {}", key);
|
||||
return null;
|
||||
}
|
||||
|
||||
public static boolean removeGenerated(Engine engine, CustomBlockOwnership.Entry entry,
|
||||
Supplier<NativeBlockPlacement> placement) {
|
||||
for (ModdedDataProvider provider : PROVIDERS) {
|
||||
if (provider.modId().equals(entry.identifier().namespace()) && provider.isReady()) {
|
||||
String storedKey = entry.identifier().key();
|
||||
int separator = storedKey.indexOf('/');
|
||||
String key = storedKey.substring(0, separator) + ":" + storedKey.substring(separator + 1);
|
||||
return provider.removeGenerated(new ModdedBlockPlacementContext(
|
||||
engine, placement.get(), parseIdentifier(key), parseState(key)), entry.token());
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+6
-1
@@ -95,7 +95,12 @@ public interface ModdedDataProvider {
|
||||
* Called on the server thread, once per deferred position, by the first provider that claims the identifier -
|
||||
* later providers are not consulted for that position. Only fires for states returned as deferred.
|
||||
*/
|
||||
default void processBlockPlacement(ModdedBlockPlacementContext context) {
|
||||
default String processBlockPlacement(ModdedBlockPlacementContext context) {
|
||||
return null;
|
||||
}
|
||||
|
||||
default boolean removeGenerated(ModdedBlockPlacementContext context, String token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+12
-4
@@ -30,6 +30,7 @@ import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.modded.ModdedGenPool;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Semaphore;
|
||||
@@ -39,6 +40,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.LongSupplier;
|
||||
|
||||
public final class ModdedPregenMethod implements PregeneratorMethod {
|
||||
private static final int ADAPTIVE_TIMEOUT_STEP = 3;
|
||||
@@ -68,8 +70,14 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
||||
private final int timeoutSeconds;
|
||||
private final PregenMantleBackpressure backpressure;
|
||||
private final PauseWhenEmptyGuard pauseGuard;
|
||||
private final LongSupplier clock;
|
||||
|
||||
public ModdedPregenMethod(Configuration configuration) {
|
||||
this(configuration, System::nanoTime);
|
||||
}
|
||||
|
||||
ModdedPregenMethod(Configuration configuration, LongSupplier clock) {
|
||||
this.clock = Objects.requireNonNull(clock);
|
||||
runtime = configuration.runtime();
|
||||
engine = configuration.engine();
|
||||
sync = configuration.sync();
|
||||
@@ -178,7 +186,7 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
||||
if (sync || runtime.isServerThread()) {
|
||||
return;
|
||||
}
|
||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(timeoutSeconds)
|
||||
long deadline = clock.getAsLong() + TimeUnit.SECONDS.toNanos(timeoutSeconds)
|
||||
+ TimeUnit.MILLISECONDS.toNanos(FINAL_SAVE_TIMEOUT_MILLIS);
|
||||
boolean interrupted = false;
|
||||
try {
|
||||
@@ -186,7 +194,7 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
||||
if (finalSaveOnServerThread.get() || !runtime.isRunning()) {
|
||||
return;
|
||||
}
|
||||
if (System.nanoTime() >= deadline) {
|
||||
if (clock.getAsLong() >= deadline) {
|
||||
throw new IllegalStateException("Iris pregen chunk callbacks did not finish for "
|
||||
+ runtime.worldIdentity() + ": " + inFlight.get() + " pending");
|
||||
}
|
||||
@@ -263,14 +271,14 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
||||
}
|
||||
|
||||
private void awaitFinalSave(FinalSaveRequest request) {
|
||||
long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(FINAL_SAVE_TIMEOUT_MILLIS);
|
||||
long deadline = clock.getAsLong() + TimeUnit.MILLISECONDS.toNanos(FINAL_SAVE_TIMEOUT_MILLIS);
|
||||
boolean interrupted = false;
|
||||
try {
|
||||
while (true) {
|
||||
if (deferFinalSaveIfRequested()) {
|
||||
return;
|
||||
}
|
||||
long remainingNanos = deadline - System.nanoTime();
|
||||
long remainingNanos = deadline - clock.getAsLong();
|
||||
if (remainingNanos <= 0L) {
|
||||
ModdedIrisLog.warn("Iris pregen level save did not complete in time for {}", runtime.worldIdentity());
|
||||
return;
|
||||
|
||||
+19
-2
@@ -22,6 +22,7 @@ import art.arcane.iris.modded.ModdedIrisLog;
|
||||
import art.arcane.iris.world.WorldMaintenance;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.mantle.EngineMantle;
|
||||
import art.arcane.iris.generation.mantle.DeferredPlacementRecords;
|
||||
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||
import art.arcane.iris.modded.ModdedBlockBuffer;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
@@ -36,6 +37,9 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeCommandSource;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeChunkRegeneration;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeProtocolPlayer;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
@@ -110,10 +114,12 @@ public final class ModdedRegen {
|
||||
|
||||
private void resetMantleMargin() {
|
||||
EngineMantle engineMantle = engine.getMantle();
|
||||
Mantle<Matter> mantle = engineMantle.getMantle();
|
||||
int margin = radius + Math.max(engineMantle.getRadius(), engineMantle.getRealRadius()) + 1;
|
||||
for (int dx = -margin; dx <= margin; dx++) {
|
||||
for (int dz = -margin; dz <= margin; dz++) {
|
||||
engineMantle.getMantle().deleteChunk(centerX + dx, centerZ + dz);
|
||||
DeferredPlacementRecords.resetMantleChunk(mantle, centerX + dx, centerZ + dz,
|
||||
Math.abs(dx) <= radius && Math.abs(dz) <= radius);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -199,10 +205,21 @@ public final class ModdedRegen {
|
||||
}
|
||||
|
||||
private void apply(int chunkX, int chunkZ, ModdedBlockBuffer blocks, Hunk<NativeBiome> biomes) {
|
||||
Mantle<Matter> mantle = engine.getMantle().getMantle();
|
||||
regeneration.apply(new NativeChunkRegeneration.Replacement(chunkX, chunkZ,
|
||||
engine.getMinHeight(), engine.getMaxHeight() - engine.getMinHeight(),
|
||||
blocks::rawOrNull, generator.regenBiomeResolver()),
|
||||
() -> generator.updateRegeneratedChunk(chunkX, chunkZ));
|
||||
() -> completeReplacement(mantle, chunkX, chunkZ));
|
||||
}
|
||||
|
||||
private void completeReplacement(Mantle<Matter> mantle, int chunkX, int chunkZ) {
|
||||
MantleChunk<Matter> records = mantle.useChunk(chunkX, chunkZ);
|
||||
try {
|
||||
DeferredPlacementRecords.invalidateCustomBlockOwnership(records);
|
||||
} finally {
|
||||
records.release();
|
||||
}
|
||||
generator.updateRegeneratedChunk(mantle, chunkX, chunkZ);
|
||||
}
|
||||
|
||||
private void ok(String message) {
|
||||
|
||||
+88
-23
@@ -28,13 +28,17 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeBlockProperties;
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.studio.view.PregeneratorJob;
|
||||
import art.arcane.iris.integration.Identifier;
|
||||
import art.arcane.iris.generation.mantle.CustomBlockOwnership;
|
||||
import art.arcane.iris.generation.mantle.DeferredPlacementRecords;
|
||||
import art.arcane.iris.generation.cache.Cache;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.block.TileData;
|
||||
import art.arcane.iris.generation.decoration.IrisProceduralBlocks;
|
||||
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||
import art.arcane.iris.modded.ModdedBlockResolution;
|
||||
import art.arcane.iris.modded.ModdedLootApplier;
|
||||
import art.arcane.iris.modded.ModdedTileData;
|
||||
import art.arcane.iris.modded.api.ModdedBlockData;
|
||||
import art.arcane.iris.modded.api.ModdedCustomContentRegistry;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeBlockPlacement;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
@@ -50,6 +54,8 @@ import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
@@ -162,8 +168,8 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
|
||||
updatePassChunk(engine, level, (int) chunkKey, (int) (chunkKey >> 32)));
|
||||
}
|
||||
|
||||
public void updateRegeneratedChunk(Engine engine, NativeWorld level, int chunkX, int chunkZ) {
|
||||
updateChunk(engine, level, chunkX, chunkZ, false);
|
||||
public void updateRegeneratedChunk(Engine engine, NativeWorld level, Mantle<Matter> mantle, int chunkX, int chunkZ) {
|
||||
updateChunk(engine, level, mantle, chunkX, chunkZ, false);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -172,7 +178,7 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
|
||||
*/
|
||||
private void updatePassChunk(Engine engine, NativeWorld level, int chunkX, int chunkZ) {
|
||||
try {
|
||||
updateChunk(engine, level, chunkX, chunkZ, true);
|
||||
updateChunk(engine, level, engine.getMantle().getMantle(), chunkX, chunkZ, true);
|
||||
} catch (Throwable failure) {
|
||||
if (engine.isClosed() || engine.getMantle().getMantle().isClosed()) {
|
||||
throw failure;
|
||||
@@ -184,7 +190,8 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
|
||||
}
|
||||
}
|
||||
|
||||
private void updateChunk(Engine engine, NativeWorld level, int chunkX, int chunkZ, boolean requireNeighbors) {
|
||||
private void updateChunk(Engine engine, NativeWorld level, Mantle<Matter> mantle,
|
||||
int chunkX, int chunkZ, boolean requireNeighbors) {
|
||||
if (requireNeighbors) {
|
||||
for (int x = -1; x <= 1; x++) {
|
||||
for (int z = -1; z <= 1; z++) {
|
||||
@@ -195,11 +202,11 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
|
||||
}
|
||||
}
|
||||
|
||||
Mantle<Matter> mantle = engine.getMantle().getMantle();
|
||||
if (!mantle.isChunkLoaded(chunkX, chunkZ)) {
|
||||
warmupMantleChunk(mantle, chunkX, chunkZ);
|
||||
return;
|
||||
}
|
||||
DeferredPlacementRecords.removeInvalidated(engine, mantle, chunkX, chunkZ);
|
||||
if (mantle.hasFlag(chunkX, chunkZ, MantleFlag.ETCHED)) {
|
||||
return;
|
||||
}
|
||||
@@ -225,30 +232,51 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
|
||||
int baseX = chunkX << 4;
|
||||
int baseZ = chunkZ << 4;
|
||||
int minHeight = engine.getWorld().minHeight();
|
||||
materializeDeferredSlice(chunk, TileWrapper.class, () ->
|
||||
chunk.iterate(TileWrapper.class, (Integer x, Integer yf, Integer z, TileWrapper v) -> {
|
||||
int y = yf + minHeight;
|
||||
if (y < level.minHeight() || y >= level.maxHeight() - 1) {
|
||||
return;
|
||||
}
|
||||
applyTile(level, baseX + (x & 15), y, baseZ + (z & 15), v.getData());
|
||||
}));
|
||||
materializeDeferredSlice(chunk, TileWrapper.class, () -> {
|
||||
for (DeferredPlacementRecords.Pending<TileWrapper> record : DeferredPlacementRecords.snapshot(chunk, TileWrapper.class)) {
|
||||
if (!DeferredPlacementRecords.isPending(chunk, TileWrapper.class, record)) {
|
||||
continue;
|
||||
}
|
||||
NativeBlockPoint position = record.position();
|
||||
int y = position.y() + minHeight;
|
||||
if (y < level.minHeight() || y >= level.maxHeight() - 1) {
|
||||
continue;
|
||||
}
|
||||
applyTile(level, baseX + (position.x() & 15), y, baseZ + (position.z() & 15), record.value().getData());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void runCustomPass(Engine engine, NativeWorld level, int chunkX, int chunkZ, MantleChunk<Matter> chunk) {
|
||||
int baseX = chunkX << 4;
|
||||
int baseZ = chunkZ << 4;
|
||||
int minHeight = engine.getWorld().minHeight();
|
||||
materializeDeferredSlice(chunk, Identifier.class, () ->
|
||||
chunk.iterate(Identifier.class, (Integer x, Integer yf, Integer z, Identifier identifier) -> {
|
||||
int y = yf + minHeight;
|
||||
if (y < level.minHeight() || y >= level.maxHeight() - 1) {
|
||||
return;
|
||||
}
|
||||
NativeBlockPoint position = new NativeBlockPoint(baseX + (x & 15), y, baseZ + (z & 15));
|
||||
ModdedCustomContentRegistry.processBlockPlacement(engine, identifier.toString(),
|
||||
() -> NativeWorldMaintenance.placement(level, position));
|
||||
}));
|
||||
StaleCarriers stale = new StaleCarriers();
|
||||
materializeDeferredSlice(chunk, Identifier.class, () -> {
|
||||
for (DeferredPlacementRecords.Pending<Identifier> record : DeferredPlacementRecords.snapshot(chunk, Identifier.class)) {
|
||||
if (!DeferredPlacementRecords.isPending(chunk, Identifier.class, record)) {
|
||||
continue;
|
||||
}
|
||||
NativeBlockPoint local = record.position();
|
||||
int y = local.y() + minHeight;
|
||||
if (y < level.minHeight() || y >= level.maxHeight() - 1) {
|
||||
continue;
|
||||
}
|
||||
int blockX = baseX + (local.x() & 15);
|
||||
int blockZ = baseZ + (local.z() & 15);
|
||||
String key = record.value().toString();
|
||||
if (stale.carrierReplaced(key, level.getBlock(blockX, y, blockZ))) {
|
||||
continue;
|
||||
}
|
||||
NativeBlockPoint position = new NativeBlockPoint(blockX, y, blockZ);
|
||||
CustomBlockOwnership.Entry ownership = ModdedCustomContentRegistry.processBlockPlacement(engine, key,
|
||||
() -> NativeWorldMaintenance.placement(level, position));
|
||||
if (ownership != null) {
|
||||
DeferredPlacementRecords.remember(engine, chunk, local, ownership);
|
||||
}
|
||||
}
|
||||
});
|
||||
stale.report(level.name(), chunkX, chunkZ);
|
||||
}
|
||||
|
||||
static void materializeDeferredSlice(MantleChunk<Matter> chunk, Class<?> sliceType, Runnable materializer) {
|
||||
@@ -386,4 +414,41 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
|
||||
NativeWorldMaintenance.refresh(level, pos);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class StaleCarriers {
|
||||
private static final String UNCHECKED = "";
|
||||
|
||||
private final Map<String, String> carriers = new HashMap<>();
|
||||
private int skipped;
|
||||
|
||||
private boolean carrierReplaced(String key, NativeBlockState live) {
|
||||
String carrier = carriers.get(key);
|
||||
if (carrier == null) {
|
||||
carrier = carrierKey(key);
|
||||
carriers.put(key, carrier);
|
||||
}
|
||||
if (carrier.equals(UNCHECKED) || carrier.equals(IrisProceduralBlocks.materialKey(live))
|
||||
|| ((carrier.equals("minecraft:fire") || carrier.equals("minecraft:soul_fire")) && live.isAir())) {
|
||||
return false;
|
||||
}
|
||||
skipped++;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void report(String levelName, int chunkX, int chunkZ) {
|
||||
if (skipped == 0) {
|
||||
return;
|
||||
}
|
||||
IrisLogging.debug("Skipped %d stale custom block records at chunk %d,%d in %s",
|
||||
skipped, chunkX, chunkZ, levelName);
|
||||
}
|
||||
|
||||
private static String carrierKey(String key) {
|
||||
ModdedBlockData resolved = ModdedCustomContentRegistry.resolveBlock(key);
|
||||
if (resolved == null || resolved.state().isAir()) {
|
||||
return UNCHECKED;
|
||||
}
|
||||
return IrisProceduralBlocks.materialKey(resolved.state());
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
@@ -28,6 +28,10 @@ import art.arcane.iris.modded.ModdedSavedTerrainCapture;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import art.arcane.iris.generation.runtime.EnginePlatformHooks;
|
||||
import art.arcane.iris.generation.mantle.CustomBlockOwnership;
|
||||
import art.arcane.iris.modded.api.ModdedCustomContentRegistry;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockPoint;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldMaintenance;
|
||||
import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.generation.terrain.IrisDimensionRuntimeContract;
|
||||
@@ -72,6 +76,12 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
|
||||
private volatile ExecutorService executor;
|
||||
private long lastPollAt;
|
||||
|
||||
@Override
|
||||
public boolean removeCustomBlock(Engine engine, NativeBlockPoint position, CustomBlockOwnership.Entry entry) {
|
||||
return ModdedCustomContentRegistry.removeGenerated(engine, entry,
|
||||
() -> NativeWorldMaintenance.placement(engine.getWorld().platformWorld(), position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> flushSavedTerrainCapture(Engine engine) {
|
||||
return ModdedSavedTerrainCapture.flush(engine);
|
||||
|
||||
+41
-6
@@ -3,6 +3,7 @@ package art.arcane.iris.modded;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.ModdedNativeStructureWorldgenAccess;
|
||||
|
||||
import art.arcane.iris.structure.placement.IrisStructureTerrain;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockChangeSet;
|
||||
import art.arcane.volmlib.util.structure.StructureTerrainMode;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructurePostProcessor;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureTerrainIntegrator;
|
||||
@@ -230,8 +231,9 @@ public class ModdedNativeStructureWorldgenAccessTest {
|
||||
recording.world(), generationCenter,
|
||||
(x, z) -> SURFACE_FIRST_FREE_Y,
|
||||
(x, z) -> FLOOR_FIRST_FREE_Y,
|
||||
true,
|
||||
position -> false);
|
||||
true, true,
|
||||
position -> false,
|
||||
NativeBlockChangeSet.disabled());
|
||||
int x = generationCenter.getMiddleBlockX();
|
||||
int z = generationCenter.getMiddleBlockZ();
|
||||
|
||||
@@ -248,8 +250,9 @@ public class ModdedNativeStructureWorldgenAccessTest {
|
||||
recording.world(), generationCenter(),
|
||||
(x, z) -> SURFACE_FIRST_FREE_Y,
|
||||
(x, z) -> FLOOR_FIRST_FREE_Y,
|
||||
false,
|
||||
position::equals);
|
||||
false, true,
|
||||
position::equals,
|
||||
NativeBlockChangeSet.disabled());
|
||||
|
||||
assertSame(Blocks.DIRT, access.getBlockState(position).getBlock());
|
||||
assertFalse(access.ensureCanWrite(position));
|
||||
@@ -270,6 +273,37 @@ public class ModdedNativeStructureWorldgenAccessTest {
|
||||
assertEquals(1, recording.mutations);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptedWritesAndReportedChangesAreTrackedOutsideProtectedCells() {
|
||||
RecordingDelegate recording = new RecordingDelegate();
|
||||
BlockPos protectedCell = generationCenter().getMiddleBlockPosition(70);
|
||||
WorldGenLevel delegate = recording.world();
|
||||
NativeBlockChangeSet changes = NativeBlockChangeSet.create(
|
||||
ModdedNativeStructureWorldgenAccess.changeBounds(delegate, generationCenter()));
|
||||
ModdedNativeStructureWorldgenAccess access = ModdedNativeStructureWorldgenAccess.create(
|
||||
delegate, generationCenter(),
|
||||
(x, z) -> SURFACE_FIRST_FREE_Y,
|
||||
(x, z) -> FLOOR_FIRST_FREE_Y,
|
||||
false, true,
|
||||
protectedCell::equals,
|
||||
changes);
|
||||
BlockPos written = protectedCell.above();
|
||||
BlockPos removed = protectedCell.east();
|
||||
BlockPos reported = protectedCell.above(2);
|
||||
|
||||
assertFalse(access.setBlock(protectedCell, Blocks.STONE.defaultBlockState(), 2));
|
||||
assertTrue(access.setBlock(written, Blocks.STONE.defaultBlockState(), 2));
|
||||
assertTrue(access.removeBlock(removed, false));
|
||||
access.recordBlockChange(reported.getX(), reported.getY(), reported.getZ());
|
||||
access.recordBlockChange(protectedCell.getX(), protectedCell.getY(), protectedCell.getZ());
|
||||
|
||||
assertEquals(3, changes.size());
|
||||
assertTrue(changes.contains(written.getX(), written.getY(), written.getZ()));
|
||||
assertTrue(changes.contains(removed.getX(), removed.getY(), removed.getZ()));
|
||||
assertTrue(changes.contains(reported.getX(), reported.getY(), reported.getZ()));
|
||||
assertFalse(changes.contains(protectedCell.getX(), protectedCell.getY(), protectedCell.getZ()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void statuslessChunkReadsPreserveWorldgenDelegateSemantics() {
|
||||
RecordingDelegate recording = new RecordingDelegate();
|
||||
@@ -319,8 +353,9 @@ public class ModdedNativeStructureWorldgenAccessTest {
|
||||
recording.world(), generationCenter(),
|
||||
(x, z) -> SURFACE_FIRST_FREE_Y,
|
||||
(x, z) -> FLOOR_FIRST_FREE_Y,
|
||||
false,
|
||||
position -> false);
|
||||
false, true,
|
||||
position -> false,
|
||||
NativeBlockChangeSet.disabled());
|
||||
}
|
||||
|
||||
private static ChunkPos generationCenter() {
|
||||
|
||||
+32
@@ -1,5 +1,9 @@
|
||||
package art.arcane.iris.modded.api;
|
||||
|
||||
import art.arcane.iris.generation.mantle.CustomBlockOwnership;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.integration.Identifier;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeBlockPlacement;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeEntityRuntime;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeSpawnedEntity;
|
||||
import net.minecraft.SharedConstants;
|
||||
@@ -9,6 +13,7 @@ import org.junit.Test;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
@@ -20,6 +25,33 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ModdedCustomContentRegistryTest {
|
||||
@Test
|
||||
public void removalReturnsToOriginalProviderWithTokenAndPlacementState() {
|
||||
ModdedDataProvider provider = mock(ModdedDataProvider.class);
|
||||
Engine engine = mock(Engine.class);
|
||||
NativeBlockPlacement placement = mock(NativeBlockPlacement.class);
|
||||
ModdedBlockPlacementContext context = new ModdedBlockPlacementContext(
|
||||
engine, placement, "provider_test:furniture/chair", Map.of("variant", "oak"));
|
||||
when(provider.modId()).thenReturn("ownership_test");
|
||||
when(provider.isReady()).thenReturn(true);
|
||||
when(provider.isValidProvider("provider_test:furniture/chair", ModdedDataType.BLOCK)).thenReturn(true);
|
||||
when(provider.processBlockPlacement(context)).thenReturn("entity-token");
|
||||
when(provider.removeGenerated(context, "entity-token")).thenReturn(true);
|
||||
ModdedCustomContentRegistry.Discovery discovery = ModdedCustomContentRegistry.discover(List.of(provider));
|
||||
try {
|
||||
CustomBlockOwnership.Entry ownership = ModdedCustomContentRegistry.processBlockPlacement(engine,
|
||||
"provider_test:furniture/chair[variant=oak]", () -> placement);
|
||||
|
||||
assertEquals(new CustomBlockOwnership.Entry(
|
||||
Identifier.fromString("ownership_test:provider_test/furniture/chair[variant=oak]"),
|
||||
"entity-token", false), ownership);
|
||||
assertTrue(ModdedCustomContentRegistry.removeGenerated(engine, ownership, () -> placement));
|
||||
verify(provider).removeGenerated(context, "entity-token");
|
||||
} finally {
|
||||
discovery.rollback();
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeClass
|
||||
public static void bootstrapRegistries() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
|
||||
+17
-7
@@ -21,6 +21,7 @@ import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
@@ -97,6 +98,9 @@ public class ModdedPregenMethodLifecycleTest {
|
||||
|
||||
@Test
|
||||
public void finalSaveWaitsForEveryCallbackBeyondFiveSeconds() throws Exception {
|
||||
AtomicLong clock = new AtomicLong();
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
CountDownLatch advanced = new CountDownLatch(2);
|
||||
CompletableFuture<ChunkLoad> load = new CompletableFuture<>();
|
||||
CompletableFuture<ChunkLoad> failedLoad = new CompletableFuture<>();
|
||||
PregenListener listener = mock(PregenListener.class);
|
||||
@@ -110,16 +114,22 @@ public class ModdedPregenMethodLifecycleTest {
|
||||
}).when(runtime).execute(any());
|
||||
try (MockedStatic<MantleHeapPressure> pressure = mockStatic(MantleHeapPressure.class)) {
|
||||
pressure.when(MantleHeapPressure::memoryBudgetBytes).thenReturn(512L << 20);
|
||||
method = new ModdedPregenMethod(new ModdedPregenMethod.Configuration(runtime, engine, false));
|
||||
method = new ModdedPregenMethod(new ModdedPregenMethod.Configuration(runtime, engine, false), () -> {
|
||||
long now = clock.get();
|
||||
if (now >= TimeUnit.SECONDS.toNanos(6)) {
|
||||
advanced.countDown();
|
||||
} else {
|
||||
started.countDown();
|
||||
}
|
||||
return now;
|
||||
});
|
||||
method.generateChunk(2, 3, listener);
|
||||
method.generateChunk(4, 5, listener);
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
Future<?> closing = executor.submit(() -> {
|
||||
started.countDown();
|
||||
method.close();
|
||||
});
|
||||
Future<?> closing = executor.submit(method::close);
|
||||
assertTrue(started.await(5, TimeUnit.SECONDS));
|
||||
assertThrows(TimeoutException.class, () -> closing.get(6, TimeUnit.SECONDS));
|
||||
clock.set(TimeUnit.SECONDS.toNanos(6));
|
||||
assertTrue(advanced.await(5, TimeUnit.SECONDS));
|
||||
assertFalse(closing.isDone());
|
||||
verify(runtime, never()).save();
|
||||
verify(engine, never()).syncGenerationHistory();
|
||||
load.complete(new ChunkLoad(true, null));
|
||||
|
||||
+244
@@ -0,0 +1,244 @@
|
||||
package art.arcane.iris.modded.service;
|
||||
|
||||
import art.arcane.iris.generation.mantle.EngineMantle;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.EngineMetrics;
|
||||
import art.arcane.iris.integration.Identifier;
|
||||
import art.arcane.iris.modded.api.ModdedBlockData;
|
||||
import art.arcane.iris.modded.api.ModdedBlockPlacementContext;
|
||||
import art.arcane.iris.modded.api.ModdedCustomContentRegistry;
|
||||
import art.arcane.iris.modded.api.ModdedDataProvider;
|
||||
import art.arcane.iris.modded.api.ModdedDataType;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.world.IrisWorld;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.ModdedBlockState;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||
import art.arcane.volmlib.util.function.Consumer4;
|
||||
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleDataAdapter;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleHooks;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.NoteBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ModdedChunkUpdateServiceCarrierTest {
|
||||
private static final String MOD_ID = "iris_carrier_guard_test";
|
||||
private static final String LAMP = MOD_ID + ":lamp";
|
||||
private static final String CHAIR = MOD_ID + ":chair";
|
||||
private static final String FIRE = MOD_ID + ":fire";
|
||||
private static final String SOUL_FIRE = MOD_ID + ":soul_fire";
|
||||
private static final List<BlockPos> PLACED = new CopyOnWriteArrayList<>();
|
||||
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraftRegistries() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
ModdedCustomContentRegistry.register(new CarrierProvider());
|
||||
}
|
||||
|
||||
@Before
|
||||
public void clearPlacements() {
|
||||
PLACED.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void staleRecordsAreSkippedWhileIntactAndAirCarrierRecordsArePlaced() {
|
||||
RecordMantleChunk chunk = new RecordMantleChunk(List.of(
|
||||
new Placed(1, LAMP),
|
||||
new Placed(2, LAMP),
|
||||
new Placed(3, LAMP),
|
||||
new Placed(4, CHAIR)));
|
||||
NativeWorld level = level(Map.of(
|
||||
1, Blocks.NOTE_BLOCK.defaultBlockState().setValue(NoteBlock.NOTE, 7),
|
||||
2, Blocks.AIR.defaultBlockState(),
|
||||
3, Blocks.COBBLESTONE.defaultBlockState(),
|
||||
4, Blocks.COBBLESTONE.defaultBlockState()));
|
||||
List<String> debug = new ArrayList<>();
|
||||
Engine engine = engine(chunk);
|
||||
|
||||
try (MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
|
||||
logging.when(() -> IrisLogging.debug(anyString(), any(Object[].class))).thenAnswer((InvocationOnMock invocation) -> {
|
||||
Object[] arguments = invocation.getArguments();
|
||||
debug.add(String.format((String) arguments[0], Arrays.copyOfRange(arguments, 1, arguments.length)));
|
||||
return null;
|
||||
});
|
||||
|
||||
new ModdedChunkUpdateService().updateRegeneratedChunk(engine, level, engine.getMantle().getMantle(), 0, 0);
|
||||
}
|
||||
|
||||
assertEquals(List.of(new BlockPos(1, 6, 1), new BlockPos(4, 6, 4)), PLACED);
|
||||
assertTrue(chunk.isFlagged(MantleFlag.ETCHED));
|
||||
assertTrue(chunk.customDeleted());
|
||||
assertEquals(1, debug.size());
|
||||
assertTrue(debug.getFirst(), debug.getFirst().contains("2 stale"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void extinguishedFireCarriersArePlacedWhileCoveredCarriersAreSkipped() {
|
||||
RecordMantleChunk chunk = new RecordMantleChunk(List.of(
|
||||
new Placed(1, FIRE), new Placed(2, SOUL_FIRE),
|
||||
new Placed(3, FIRE), new Placed(4, SOUL_FIRE)));
|
||||
NativeWorld level = level(Map.of(
|
||||
1, Blocks.AIR.defaultBlockState(),
|
||||
2, Blocks.CAVE_AIR.defaultBlockState(),
|
||||
3, Blocks.COBBLESTONE.defaultBlockState(),
|
||||
4, Blocks.COBBLESTONE.defaultBlockState()));
|
||||
Engine engine = engine(chunk);
|
||||
|
||||
new ModdedChunkUpdateService().updateRegeneratedChunk(engine, level, engine.getMantle().getMantle(), 0, 0);
|
||||
|
||||
assertEquals(List.of(new BlockPos(1, 6, 1), new BlockPos(2, 6, 2)), PLACED);
|
||||
assertTrue(chunk.customDeleted());
|
||||
}
|
||||
|
||||
private static NativeWorld level(Map<Integer, BlockState> live) {
|
||||
ServerLevel serverLevel = mock(ServerLevel.class);
|
||||
when(serverLevel.getBlockState(any(BlockPos.class))).thenReturn(Blocks.STONE.defaultBlockState());
|
||||
NativeWorld level = mock(NativeWorld.class);
|
||||
when(level.name()).thenReturn("example:carriers");
|
||||
when(level.minHeight()).thenReturn(-64);
|
||||
when(level.maxHeight()).thenReturn(320);
|
||||
when(level.nativeHandle()).thenReturn(serverLevel);
|
||||
for (Map.Entry<Integer, BlockState> entry : live.entrySet()) {
|
||||
int offset = entry.getKey();
|
||||
when(level.getBlock(offset, 6, offset)).thenReturn(ModdedBlockState.of(entry.getValue(), null));
|
||||
}
|
||||
return level;
|
||||
}
|
||||
|
||||
private static Engine engine(RecordMantleChunk chunk) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Mantle<Matter> mantle = mock(Mantle.class);
|
||||
when(mantle.isChunkLoaded(0, 0)).thenReturn(true);
|
||||
when(mantle.hasFlag(0, 0, MantleFlag.ETCHED)).thenAnswer((InvocationOnMock invocation) -> chunk.isFlagged(MantleFlag.ETCHED));
|
||||
when(mantle.useChunk(0, 0)).thenAnswer((InvocationOnMock invocation) -> chunk.use());
|
||||
IrisWorld world = mock(IrisWorld.class);
|
||||
when(world.minHeight()).thenReturn(-64);
|
||||
EngineMantle engineMantle = mock(EngineMantle.class);
|
||||
when(engineMantle.getMantle()).thenReturn(mantle);
|
||||
Engine engine = mock(Engine.class);
|
||||
when(engine.getWorld()).thenReturn(world);
|
||||
when(engine.getMetrics()).thenReturn(new EngineMetrics(8));
|
||||
when(engine.getMantle()).thenReturn(engineMantle);
|
||||
return engine;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static MantleDataAdapter<Matter> emptyAdapter() {
|
||||
return (MantleDataAdapter<Matter>) Proxy.newProxyInstance(
|
||||
MantleDataAdapter.class.getClassLoader(),
|
||||
new Class<?>[]{MantleDataAdapter.class},
|
||||
(proxy, method, arguments) -> {
|
||||
throw new UnsupportedOperationException(method.getName());
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private record Placed(int offset, String key) {
|
||||
}
|
||||
|
||||
private static final class RecordMantleChunk extends MantleChunk<Matter> {
|
||||
private final List<Placed> records;
|
||||
private boolean customDeleted;
|
||||
|
||||
private RecordMantleChunk(List<Placed> records) {
|
||||
super(1, 0, 0, emptyAdapter(), MantleHooks.NONE);
|
||||
this.records = records;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> void iterate(Class<T> type, Consumer4<Integer, Integer, Integer, T> iterator) {
|
||||
if (type != Identifier.class) {
|
||||
return;
|
||||
}
|
||||
for (Placed record : records) {
|
||||
iterator.accept(record.offset(), 70, record.offset(), type.cast(Identifier.fromString(record.key())));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T get(int x, int y, int z, Class<T> type) {
|
||||
if (type == Identifier.class && y == 70 && x == z) {
|
||||
for (Placed record : records) {
|
||||
if (record.offset() == x) {
|
||||
return type.cast(Identifier.fromString(record.key()));
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteSlices(Class<?> type) {
|
||||
if (type == Identifier.class) {
|
||||
customDeleted = true;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean customDeleted() {
|
||||
return customDeleted;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class CarrierProvider implements ModdedDataProvider {
|
||||
@Override
|
||||
public String modId() {
|
||||
return MOD_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> getTypes(ModdedDataType type) {
|
||||
return type == ModdedDataType.BLOCK ? List.of(LAMP, CHAIR, FIRE, SOUL_FIRE) : List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValidProvider(String id, ModdedDataType type) {
|
||||
return type == ModdedDataType.BLOCK && getTypes(type).contains(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModdedBlockData getBlockData(String blockId, Map<String, String> state) {
|
||||
BlockState carrier = switch (blockId) {
|
||||
case CHAIR -> Blocks.AIR.defaultBlockState();
|
||||
case FIRE -> Blocks.FIRE.defaultBlockState();
|
||||
case SOUL_FIRE -> Blocks.SOUL_FIRE.defaultBlockState();
|
||||
default -> Blocks.NOTE_BLOCK.defaultBlockState();
|
||||
};
|
||||
return ModdedBlockData.deferred(ModdedBlockState.of(carrier, null));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String processBlockPlacement(ModdedBlockPlacementContext context) {
|
||||
PLACED.add(context.placement().position());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
-5
@@ -68,6 +68,29 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ModdedChunkUpdateServiceTest {
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void regeneratedCleanupUsesSuppliedMantleAfterCurrentRuntimeChanges() {
|
||||
Mantle<Matter> selected = mock(Mantle.class);
|
||||
Mantle<Matter> replacement = mock(Mantle.class);
|
||||
MantleDataAdapter<Matter> adapter = mock(MantleDataAdapter.class);
|
||||
MantleChunk<Matter> chunk = new MantleChunk<>(1, 0, 0, adapter, MantleHooks.NONE);
|
||||
when(selected.isChunkLoaded(0, 0)).thenReturn(true);
|
||||
when(selected.useChunk(0, 0)).thenAnswer(invocation -> chunk.use());
|
||||
when(selected.hasFlag(0, 0, MantleFlag.ETCHED)).thenReturn(true);
|
||||
NativeWorld level = mock(NativeWorld.class);
|
||||
when(level.isChunkLoaded(0, 0)).thenReturn(true);
|
||||
Engine engine = engineWith(replacement);
|
||||
when(engine.getWorld().platformWorld()).thenReturn(level);
|
||||
|
||||
new ModdedChunkUpdateService().updateRegeneratedChunk(engine, level, selected, 0, 0);
|
||||
|
||||
verify(selected).useChunk(0, 0);
|
||||
verify(replacement, never()).isChunkLoaded(anyInt(), anyInt());
|
||||
verify(replacement, never()).useChunk(anyInt(), anyInt());
|
||||
assertFalse(chunk.inUse());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fallingFluidUpdateRetainsLevelEightStateAndRequestsPhysics() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
@@ -154,7 +177,7 @@ public class ModdedChunkUpdateServiceTest {
|
||||
return null;
|
||||
});
|
||||
|
||||
service.updateRegeneratedChunk(engine, level, 0, 0);
|
||||
service.updateRegeneratedChunk(engine, level, engine.getMantle().getMantle(), 0, 0);
|
||||
|
||||
assertFalse(chunk.isFlagged(MantleFlag.ETCHED));
|
||||
assertFalse(chunk.isFlagged(MantleFlag.UPDATE));
|
||||
@@ -162,7 +185,7 @@ public class ModdedChunkUpdateServiceTest {
|
||||
logging.verify(() -> IrisLogging.reportError(any(Throwable.class)), never());
|
||||
logging.verify(() -> IrisLogging.reportError(anyString(), any(Throwable.class)), never());
|
||||
|
||||
service.updateRegeneratedChunk(engine, level, 0, 0);
|
||||
service.updateRegeneratedChunk(engine, level, engine.getMantle().getMantle(), 0, 0);
|
||||
|
||||
assertTrue(chunk.isFlagged(MantleFlag.ETCHED));
|
||||
assertTrue(chunk.deleted());
|
||||
@@ -187,7 +210,7 @@ public class ModdedChunkUpdateServiceTest {
|
||||
});
|
||||
|
||||
assertThrows(SavedBiomeUnavailableException.class,
|
||||
() -> new ModdedChunkUpdateService().updateRegeneratedChunk(engine, level, 0, 0));
|
||||
() -> new ModdedChunkUpdateService().updateRegeneratedChunk(engine, level, engine.getMantle().getMantle(), 0, 0));
|
||||
|
||||
assertFalse(chunk.isFlagged(MantleFlag.ETCHED));
|
||||
assertFalse(chunk.isFlagged(MantleFlag.UPDATE));
|
||||
@@ -215,13 +238,13 @@ public class ModdedChunkUpdateServiceTest {
|
||||
});
|
||||
|
||||
assertSame(failure, assertThrows(IllegalStateException.class,
|
||||
() -> service.updateRegeneratedChunk(engine, level, 0, 0)));
|
||||
() -> service.updateRegeneratedChunk(engine, level, engine.getMantle().getMantle(), 0, 0)));
|
||||
assertFalse(chunk.isFlagged(MantleFlag.UPDATE));
|
||||
assertFalse(chunk.isFlagged(MantleFlag.ETCHED));
|
||||
assertFalse(chunk.deleted());
|
||||
verify(chunk.slice, never()).set(2, 5, 2, null);
|
||||
|
||||
service.updateRegeneratedChunk(engine, level, 0, 0);
|
||||
service.updateRegeneratedChunk(engine, level, engine.getMantle().getMantle(), 0, 0);
|
||||
|
||||
assertTrue(chunk.isFlagged(MantleFlag.ETCHED));
|
||||
assertTrue(chunk.deleted());
|
||||
|
||||
@@ -136,6 +136,18 @@
|
||||
"replacement": "when(level.getMaxBuildHeight()).thenReturn(320);"
|
||||
}
|
||||
],
|
||||
"art/arcane/iris/modded/api/ModdedCustomContentRegistryTest.java": [
|
||||
{
|
||||
"pattern": "\\bResourceLocation\\b",
|
||||
"replacement": "Identifier"
|
||||
}
|
||||
],
|
||||
"art/arcane/iris/modded/service/ModdedChunkUpdateServiceCarrierTest.java": [
|
||||
{
|
||||
"pattern": "\\bResourceLocation\\b",
|
||||
"replacement": "Identifier"
|
||||
}
|
||||
],
|
||||
"art/arcane/volmlib/nativelib/minecraft26_2/modded/NativeSpawnPopulationTest.java": [
|
||||
{
|
||||
"pattern": "when\\(level\\.getMaxY\\(\\)\\)\\.thenReturn\\(320\\);",
|
||||
|
||||
@@ -59,7 +59,7 @@ apply from: file('../../gradle/volmlib-stamp.gradle')
|
||||
|
||||
Properties rootProperties = new Properties()
|
||||
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
|
||||
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.3.4'))
|
||||
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.3.5'))
|
||||
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.3'))
|
||||
String neoForgeVersion = providers.gradleProperty('neoForgeVersion').getOrElse(irisModTarget.neoForge.toString())
|
||||
Closure<String> irisArtifactName = { String platform, String targetVersion ->
|
||||
|
||||
@@ -33,6 +33,8 @@ plugins {
|
||||
|
||||
rootProject.name = 'iris-neoforge'
|
||||
|
||||
apply from: new File(settingsDir, '../../gradle/runtime-cache.settings.gradle').canonicalFile
|
||||
|
||||
dependencyResolutionManagement {
|
||||
versionCatalogs {
|
||||
libs {
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
./gradlew buildAllToOut --no-parallel -PuseLocalVolmLib=false "$@"
|
||||
./gradlew buildAll "$@"
|
||||
+103
-228
@@ -9,9 +9,6 @@ import org.gradle.api.plugins.JavaPluginExtension
|
||||
import org.gradle.api.provider.Provider
|
||||
import org.gradle.jvm.toolchain.JavaLauncher
|
||||
import org.gradle.jvm.toolchain.JavaToolchainService
|
||||
import org.gradle.api.file.CopySpec
|
||||
import org.gradle.api.file.DeleteSpec
|
||||
import org.gradle.api.file.FileSystemOperations
|
||||
import org.gradle.api.file.FileTree
|
||||
import org.gradle.api.plugins.JavaPlugin
|
||||
import org.gradle.api.initialization.IncludedBuild
|
||||
@@ -20,8 +17,6 @@ import org.gradle.api.tasks.compile.JavaCompile
|
||||
import org.gradle.jvm.tasks.Jar
|
||||
import org.gradle.jvm.toolchain.JavaLanguageVersion
|
||||
|
||||
import javax.inject.Inject
|
||||
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2021 Arcane Arts (Volmit Software)
|
||||
@@ -59,15 +54,8 @@ apply plugin: 'art.arcane.volmit-packaging'
|
||||
apply from: file('gradle/runtime-relocations.gradle')
|
||||
apply from: file('gradle/volmlib-stamp.gradle')
|
||||
|
||||
interface IrisFileOperations {
|
||||
@Inject
|
||||
FileSystemOperations getFileSystemOperations()
|
||||
}
|
||||
|
||||
IrisFileOperations irisFileOperations = objects.newInstance(IrisFileOperations)
|
||||
|
||||
group = 'art.arcane'
|
||||
version = providers.gradleProperty('irisVersion').getOrElse('4.3.4')
|
||||
version = providers.gradleProperty('irisVersion').getOrElse('4.3.5')
|
||||
|
||||
project(':core') {
|
||||
group = 'art.arcane'
|
||||
@@ -99,41 +87,11 @@ Closure<String> irisArtifactName = { String platform, String targetVersion ->
|
||||
return "Iris v${project.version} [${platform}] ${targetVersion}.jar"
|
||||
}
|
||||
String bukkitArtifactName = irisArtifactName('CraftBukkit', bukkitMinecraftRange)
|
||||
String fabricArtifactName = irisArtifactName('Fabric', "${minecraftVersion}+${loaderDisplayVersion(fabricLoaderVersion)}")
|
||||
String forgeArtifactName = irisArtifactName('Forge', "${minecraftVersion}+${loaderDisplayVersion(forgeVersion)}")
|
||||
String neoForgeArtifactName = irisArtifactName('NeoForge', "${minecraftVersion}+${loaderDisplayVersion(neoForgeVersion)}")
|
||||
Map<String, String> runtimeDownloadRelocations = (Map<String, String>) ext.runtimeDownloadRelocations
|
||||
List<String> runtimeDownloadedPrefixes = (List<String>) ext.runtimeDownloadedPrefixes
|
||||
File pluginOutDirectory = file(providers.gradleProperty('pluginOutDirectory').getOrElse('../PluginOuts'))
|
||||
File legacyBukkitJar = layout.buildDirectory.file("libs/Iris-${project.version}.jar").get().asFile
|
||||
File bukkitArtifact = layout.buildDirectory.file("libs/${bukkitArtifactName}").get().asFile
|
||||
File consumerBukkitArtifact = layout.buildDirectory.file("consumer/${bukkitArtifactName}").get().asFile
|
||||
File fabricArtifact = layout.projectDirectory.file("adapters/fabric/build/${minecraftVersion}/libs/${fabricArtifactName}").asFile
|
||||
File forgeArtifact = layout.projectDirectory.file("adapters/forge/build/${minecraftVersion}/libs/${forgeArtifactName}").asFile
|
||||
File neoForgeArtifact = layout.projectDirectory.file("adapters/neoforge/build/${minecraftVersion}/libs/${neoForgeArtifactName}").asFile
|
||||
File consumerDirectory = layout.buildDirectory.dir('consumer').get().asFile
|
||||
FileTree staleConsumerBukkitJars = fileTree(consumerDirectory) {
|
||||
include('Iris v* [CraftBukkit] *.jar')
|
||||
}
|
||||
FileTree staleBukkitOutJars = fileTree(pluginOutDirectory) {
|
||||
include('Iris v* [CraftBukkit] *.jar')
|
||||
}
|
||||
String selectedModdedSuffix = minecraftVersion.replace('.', '_')
|
||||
apply plugin: ApiGenerator
|
||||
|
||||
// Where `buildAll` drops the per-platform jars for a local test server. Use the approved sibling
|
||||
// Multiplexor workspace when it exists, with a repo-local fallback for fresh clones.
|
||||
File multiplexorConsumerLocation = new File(rootDir, '../../[Minecraft Server]/consumers').canonicalFile
|
||||
String defaultConsumerLocation = multiplexorConsumerLocation.isDirectory()
|
||||
? multiplexorConsumerLocation.absolutePath
|
||||
: layout.buildDirectory.dir('consumers').get().asFile.absolutePath
|
||||
String consumerLocation = providers.gradleProperty('location')
|
||||
.getOrElse(defaultConsumerLocation)
|
||||
String bukkitConsumerPath = "${consumerLocation}/plugin-consumers/dropins/plugins"
|
||||
String fabricConsumerPath = "${consumerLocation}/fabric-mod-consumers/dropins/mods"
|
||||
String forgeConsumerPath = "${consumerLocation}/forge-mod-consumers/dropins/mods"
|
||||
String neoForgeConsumerPath = "${consumerLocation}/neoforge-mod-consumers/dropins/mods"
|
||||
|
||||
List<String> nmsBindings = ['v26_2_R1', 'v26_3_R1']
|
||||
ext.nativeDevelopmentAdapters = nmsBindings
|
||||
nmsBindings.each { String key ->
|
||||
@@ -240,65 +198,14 @@ tasks.named('jar', Jar).configure {
|
||||
exclude('com/fren_gor/**')
|
||||
|
||||
}
|
||||
doFirst {
|
||||
irisFileOperations.fileSystemOperations.delete { DeleteSpec spec -> spec.delete(legacyBukkitJar) }
|
||||
}
|
||||
archiveFileName.set(bukkitArtifactName)
|
||||
}
|
||||
|
||||
tasks.register('iris', Copy) {
|
||||
group = 'iris'
|
||||
dependsOn('packedJar')
|
||||
from({ tasks.named('packedJar').get().outputJar })
|
||||
rename { String ignored -> bukkitArtifactName }
|
||||
into(layout.buildDirectory.dir('consumer'))
|
||||
doFirst {
|
||||
irisFileOperations.fileSystemOperations.delete { DeleteSpec spec -> spec.delete(staleConsumerBukkitJars) }
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('buildSwiftSwamp') {
|
||||
group = 'iris'
|
||||
description = 'Builds every platform jar and drops them in the local BUILDS output directory.'
|
||||
outputs.upToDateWhen { false }
|
||||
dependsOn('iris', "verifyFabric${selectedModdedSuffix}", "verifyForge${selectedModdedSuffix}",
|
||||
"verifyNeoForge${selectedModdedSuffix}")
|
||||
File swiftSwampOut = file('C://VolmitSoftware/BUILDS')
|
||||
Map<String, File> swiftSwampSources = [
|
||||
'Iris.jar' : consumerBukkitArtifact,
|
||||
'Iris-Fabric.jar' : fabricArtifact,
|
||||
'Iris-Forge.jar' : forgeArtifact,
|
||||
'Iris-Neoforge.jar': neoForgeArtifact
|
||||
]
|
||||
doLast {
|
||||
swiftSwampSources.each { String target, File source ->
|
||||
irisFileOperations.fileSystemOperations.copy { CopySpec spec ->
|
||||
spec.from(source)
|
||||
spec.into(swiftSwampOut)
|
||||
spec.rename { String ignored -> target }
|
||||
}
|
||||
}
|
||||
println('')
|
||||
println('=== Iris buildSwiftSwamp -> ' + swiftSwampOut + ' ===')
|
||||
['Iris.jar', 'Iris-Fabric.jar', 'Iris-Forge.jar', 'Iris-Neoforge.jar'].each { String jar ->
|
||||
File produced = new File(swiftSwampOut, jar)
|
||||
println(String.format(' %-24s %8d KB', jar, (long) (produced.length() / 1024)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('buildBukkit', Copy) {
|
||||
group = 'iris'
|
||||
dependsOn('packedJar')
|
||||
from({ tasks.named('packedJar').get().outputJar })
|
||||
rename { String ignored -> bukkitArtifactName }
|
||||
into(pluginOutDirectory)
|
||||
File legacyOutJar = new File(pluginOutDirectory, "Iris-${project.version}+mc${minecraftVersion}.jar")
|
||||
doFirst {
|
||||
irisFileOperations.fileSystemOperations.delete { DeleteSpec spec ->
|
||||
spec.delete(staleBukkitOutJars, legacyOutJar)
|
||||
}
|
||||
}
|
||||
tasks.register('buildBukkit') {
|
||||
group = 'build'
|
||||
description = 'Builds and verifies the packed Bukkit plugin.'
|
||||
dependsOn('packedJar', 'check', ':core:check', ':spi:check', ':adapters:bukkit:plugin:check')
|
||||
dependsOn(nmsBindings.collect { String binding -> ":adapters:bukkit:nms:${binding}:check".toString() })
|
||||
}
|
||||
|
||||
// The three loader jars come out of nested standalone Gradle builds driven through Exec. The outer
|
||||
@@ -310,6 +217,7 @@ List<Object> moddedSharedInputs = []
|
||||
'adapters/modded-common/src',
|
||||
'adapters/client-common/src',
|
||||
'core/src/main',
|
||||
'core/src/testFixtures',
|
||||
'core/build.gradle',
|
||||
'core/agent/src',
|
||||
'core/agent/build.gradle',
|
||||
@@ -327,7 +235,13 @@ List<Object> moddedSharedInputs = []
|
||||
'gradle/native-minecraft-sources.gradle',
|
||||
'gradle/mod-target.gradle',
|
||||
'gradle/mod-targets.json',
|
||||
'gradle/modded-version-sources.gradle'
|
||||
'gradle/runtime-cache.settings.gradle',
|
||||
'gradle/nested-build.init.gradle',
|
||||
'gradle/modded-version-sources.gradle',
|
||||
'gradle/wrapper/gradle-wrapper.properties',
|
||||
'gradle/wrapper/gradle-wrapper.jar',
|
||||
'gradlew',
|
||||
'gradlew.bat'
|
||||
].each { String path -> moddedSharedInputs.add(layout.projectDirectory.file(path).asFile) }
|
||||
if (localVolmLibCheckout != null) {
|
||||
moddedSharedInputs.add(fileTree(localVolmLibCheckout) {
|
||||
@@ -341,11 +255,11 @@ if (localVolmLibCheckout != null) {
|
||||
|
||||
// The published master-SNAPSHOT is resolved inside the nested build, so no outer input changes when it
|
||||
// moves. Without a local checkout the Exec tasks always run and the nested build decides what is stale.
|
||||
Closure<Void> declareVolmLibInputs = { Exec task ->
|
||||
Closure<Void> declareVolmLibInputs = { IrisModdedBuild task ->
|
||||
task.inputs.property('volmLibStamp', volmLibStamp)
|
||||
String nativeBillow = providers.gradleProperty('includeNativeBillow').getOrElse('false')
|
||||
task.inputs.property('includeNativeBillow', nativeBillow)
|
||||
task.environment('ORG_GRADLE_PROJECT_includeNativeBillow', nativeBillow)
|
||||
task.environment.put('ORG_GRADLE_PROJECT_includeNativeBillow', nativeBillow)
|
||||
if (localVolmLibCheckout == null) {
|
||||
task.outputs.upToDateWhen { false }
|
||||
}
|
||||
@@ -410,10 +324,40 @@ TaskProvider<Task> verifyModdedMatrix = tasks.register('verifyModdedMatrix') {
|
||||
group = 'verification'
|
||||
description = 'Builds, tests and verifies the Fabric, Forge and NeoForge jars of every supported Minecraft mod target.'
|
||||
}
|
||||
List<TaskProvider<Copy>> moddedExports = []
|
||||
Map<String, TaskProvider<Exec>> selectedModdedBuilds = [:]
|
||||
Map<String, TaskProvider<Copy>> selectedModdedExports = [:]
|
||||
TaskProvider<Exec> previousMatrixBuild = null
|
||||
Map<String, TaskProvider<IrisModdedBuild>> selectedModdedBuilds = [:]
|
||||
int matrixParallelism = Math.min(gradle.startParameter.maxWorkerCount, Math.max(1,
|
||||
providers.gradleProperty('moddedBuildParallelism').getOrElse(
|
||||
Math.min(12, Math.max(1, Runtime.runtime.availableProcessors() - 4)).toString()).toInteger()))
|
||||
Provider<IrisModdedBuildService> matrixWorkers = gradle.sharedServices.registerIfAbsent(
|
||||
'irisModdedBuilds', IrisModdedBuildService) {
|
||||
maxParallelUsages.set(matrixParallelism)
|
||||
}
|
||||
List<String> nestedBuildArguments = [gradle.startParameter.buildCacheEnabled ? '--build-cache' : '--no-build-cache']
|
||||
String paperweightSources = providers.gradleProperty('paperweight.experimental.genSources').orNull
|
||||
if (paperweightSources != null) {
|
||||
nestedBuildArguments.add("-Ppaperweight.experimental.genSources=${paperweightSources}".toString())
|
||||
}
|
||||
boolean multiplexorSandbox = providers.gradleProperty('multiplexorSandbox').getOrElse('false').toBoolean()
|
||||
int nestedWorkers = multiplexorSandbox
|
||||
? Math.max(1, gradle.startParameter.maxWorkerCount.intdiv(matrixParallelism))
|
||||
: Math.min(2, gradle.startParameter.maxWorkerCount)
|
||||
int nestedProcessors = multiplexorSandbox ? nestedWorkers : 2
|
||||
if (multiplexorSandbox) {
|
||||
nestedBuildArguments.addAll(['--no-daemon', '-PmultiplexorSandbox=true'])
|
||||
}
|
||||
String nestedJvmArguments = providers.gradleProperty('org.gradle.jvmargs').getOrElse('') +
|
||||
" -XX:ActiveProcessorCount=${nestedProcessors}"
|
||||
if (gradle.startParameter.rerunTasks) {
|
||||
nestedBuildArguments.add('--rerun-tasks')
|
||||
}
|
||||
if (gradle.startParameter.refreshDependencies) {
|
||||
nestedBuildArguments.add('--refresh-dependencies')
|
||||
}
|
||||
for (File initScript : gradle.startParameter.initScripts) {
|
||||
nestedBuildArguments.addAll(['--init-script', initScript.absolutePath])
|
||||
}
|
||||
Map<Integer, TaskProvider<IrisModdedBuild>> daemonSlots = [:]
|
||||
int nextDaemonSlot = 0
|
||||
irisModTargets.each { String targetMinecraft, Map<String, Object> target ->
|
||||
String suffix = targetMinecraft.replace('.', '_')
|
||||
boolean selected = targetMinecraft == minecraftVersion
|
||||
@@ -424,67 +368,68 @@ irisModTargets.each { String targetMinecraft, Map<String, Object> target ->
|
||||
String artifactName = irisArtifactName(label, "${targetMinecraft}+${displayedLoader}")
|
||||
File artifact = file("adapters/${loader}/build/${targetMinecraft}/libs/${artifactName}")
|
||||
boolean unitTests = ((List<String>) (target.testLoaders ?: moddedLoaderLabels.keySet().toList())).contains(loader)
|
||||
TaskProvider<Exec> buildTarget = tasks.register("verify${label}${suffix}", Exec) {
|
||||
int daemonSlot = nextDaemonSlot++ % matrixParallelism
|
||||
TaskProvider<IrisModdedBuild> buildTarget = project(':adapters').tasks.register("verify${label}${suffix}", IrisModdedBuild) {
|
||||
group = 'verification'
|
||||
description = "Builds, tests and verifies the ${label} ${targetMinecraft} jar in its own Gradle build."
|
||||
workingDir = file("adapters/${loader}")
|
||||
List<String> command = [file(moddedWrapperScript).absolutePath, 'assemble', 'compileTestJava',
|
||||
usesService(matrixWorkers)
|
||||
workingDirectory.set(rootProject.file("adapters/${loader}"))
|
||||
getLabel().set("${label} ${targetMinecraft}".toString())
|
||||
artifactFile.set(artifact)
|
||||
logFile.set(rootProject.layout.buildDirectory.file("reports/modded/${loader}-${targetMinecraft}.log"))
|
||||
requiredEntries.set(commonModdedArtifactEntries + moddedLoaderEntries[loader])
|
||||
forbiddenCoreClasses.set(bukkitCoupledCoreClasses)
|
||||
javaRuntime.set((int) target.java)
|
||||
List<String> command = [rootProject.file(moddedWrapperScript).absolutePath, 'assemble', 'compileTestJava',
|
||||
'verifyLoggingPolicy', 'verifyNativeClassBoundary', '--console=plain',
|
||||
"-PirisVersion=${project.version}", "-PminecraftVersion=${targetMinecraft}",
|
||||
"--max-workers=${nestedWorkers}",
|
||||
"-Dorg.gradle.jvmargs=${nestedJvmArguments.trim()}",
|
||||
"-Dorg.gradle.daemon.registry.base=${rootProject.file(".gradle/iris-daemons/slot-${daemonSlot}").absolutePath}",
|
||||
'-Dorg.gradle.daemon.idletimeout=3600000',
|
||||
"-PirisBuildLane=${loader}-${targetMinecraft}",
|
||||
'--project-cache-dir', rootProject.file("adapters/${loader}/.gradle/iris-lanes/${loader}-${targetMinecraft}").absolutePath,
|
||||
'--init-script', rootProject.file('gradle/nested-build.init.gradle').absolutePath,
|
||||
"-PirisVersion=${rootProject.version}", "-PminecraftVersion=${targetMinecraft}",
|
||||
"-P${moddedLoaderProperties[loader]}=${loaderPin}",
|
||||
"-PuseLocalVolmLib=${localVolmLibCheckout != null}"]
|
||||
if (unitTests) {
|
||||
command.add('test')
|
||||
}
|
||||
commandLine(command)
|
||||
command.add("-PtestForks=${providers.gradleProperty('moddedTestForks').getOrElse('1')}")
|
||||
command.addAll(nestedBuildArguments)
|
||||
commandLine.set(command.collect { Object argument -> argument.toString() })
|
||||
if (localVolmLibCheckout != null) {
|
||||
environment('ORG_GRADLE_PROJECT_localVolmLibDirectory', localVolmLibCheckout.absolutePath)
|
||||
environment.put('ORG_GRADLE_PROJECT_localVolmLibDirectory', localVolmLibCheckout.absolutePath)
|
||||
}
|
||||
inputs.files(moddedSharedInputs).withPropertyName('moddedSharedSources')
|
||||
.withPathSensitivity(PathSensitivity.RELATIVE)
|
||||
inputs.files(file("adapters/${loader}/src"), file("adapters/${loader}/build.gradle"),
|
||||
file("adapters/${loader}/settings.gradle")).withPropertyName('adapterSources')
|
||||
inputs.files(rootProject.file("adapters/${loader}/src"), rootProject.file("adapters/${loader}/build.gradle"),
|
||||
rootProject.file("adapters/${loader}/settings.gradle"), rootProject.file("adapters/${loader}/gradle.properties"))
|
||||
.withPropertyName('adapterSources')
|
||||
inputs.files(gradle.startParameter.allInitScripts).withPropertyName('initScripts')
|
||||
.withPathSensitivity(PathSensitivity.NONE)
|
||||
outputs.upToDateWhen { !gradle.startParameter.refreshDependencies }
|
||||
inputs.properties(target)
|
||||
inputs.property('loaderVersion', loaderPin)
|
||||
inputs.property('irisVersion', project.version.toString())
|
||||
inputs.property('irisVersion', rootProject.version.toString())
|
||||
declareVolmLibInputs(it)
|
||||
outputs.file(artifact)
|
||||
if (unitTests) {
|
||||
outputs.dir(file("adapters/${loader}/build/${targetMinecraft}/test-results/test"))
|
||||
}
|
||||
doLast {
|
||||
ModdedArtifactVerifier.verify(artifact, commonModdedArtifactEntries + moddedLoaderEntries[loader],
|
||||
bukkitCoupledCoreClasses)
|
||||
ModdedArtifactVerifier.verifyJavaRuntime(artifact, (int) target.java)
|
||||
logger.lifecycle("Verified ${artifact.name}")
|
||||
outputs.dir(rootProject.file("adapters/${loader}/build/${targetMinecraft}/test-results/test"))
|
||||
}
|
||||
}
|
||||
if (previousMatrixBuild != null) {
|
||||
TaskProvider<Exec> preceding = previousMatrixBuild
|
||||
buildTarget.configure { mustRunAfter(preceding) }
|
||||
TaskProvider<IrisModdedBuild> previousSlotBuild = daemonSlots[daemonSlot]
|
||||
if (previousSlotBuild != null) {
|
||||
buildTarget.configure { mustRunAfter(previousSlotBuild) }
|
||||
}
|
||||
previousMatrixBuild = buildTarget
|
||||
verifyModdedMatrix.configure { dependsOn(buildTarget) }
|
||||
File legacyOutJar = new File(pluginOutDirectory, "Iris-${project.version}+mc${targetMinecraft}-${loader}.jar")
|
||||
FileTree staleTargetJars = fileTree(pluginOutDirectory) {
|
||||
include("Iris v* [${label}] ${targetMinecraft}+*.jar")
|
||||
}
|
||||
TaskProvider<Copy> export = tasks.register("build${label}${suffix}", Copy) {
|
||||
group = 'iris'
|
||||
description = "Exports the verified ${label} ${targetMinecraft} jar to the plugin output directory."
|
||||
daemonSlots[daemonSlot] = buildTarget
|
||||
tasks.register("verify${label}${suffix}") {
|
||||
group = 'verification'
|
||||
description = "Builds, tests and verifies the ${label} ${targetMinecraft} jar."
|
||||
dependsOn(buildTarget)
|
||||
from(artifact)
|
||||
into(pluginOutDirectory)
|
||||
doFirst {
|
||||
irisFileOperations.fileSystemOperations.delete { DeleteSpec spec ->
|
||||
spec.delete(staleTargetJars, legacyOutJar)
|
||||
}
|
||||
}
|
||||
}
|
||||
moddedExports.add(export)
|
||||
verifyModdedMatrix.configure { dependsOn(buildTarget) }
|
||||
if (selected) {
|
||||
selectedModdedBuilds[loader] = buildTarget
|
||||
selectedModdedExports[loader] = export
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -494,36 +439,20 @@ if (selectedModdedBuilds.isEmpty()) {
|
||||
|
||||
tasks.register('buildFabric') {
|
||||
group = 'iris'
|
||||
description = "Exports the verified Fabric ${minecraftVersion} jar."
|
||||
dependsOn(selectedModdedExports.fabric)
|
||||
description = "Builds and verifies the Fabric ${minecraftVersion} jar."
|
||||
dependsOn(selectedModdedBuilds.fabric)
|
||||
}
|
||||
|
||||
tasks.register('buildForge') {
|
||||
group = 'iris'
|
||||
description = "Exports the verified Forge ${minecraftVersion} jar."
|
||||
dependsOn(selectedModdedExports.forge)
|
||||
description = "Builds and verifies the Forge ${minecraftVersion} jar."
|
||||
dependsOn(selectedModdedBuilds.forge)
|
||||
}
|
||||
|
||||
tasks.register('buildNeoforge') {
|
||||
group = 'iris'
|
||||
description = "Exports the verified NeoForge ${minecraftVersion} jar."
|
||||
dependsOn(selectedModdedExports.neoforge)
|
||||
}
|
||||
|
||||
gradle.projectsEvaluated {
|
||||
List<Task> sharedOutputTasks = allprojects.collectMany { Project candidate ->
|
||||
candidate.tasks.findAll { Task task ->
|
||||
task instanceof JavaCompile || task instanceof Jar || task instanceof Test
|
||||
}
|
||||
}
|
||||
tasks.withType(Exec).matching { Task task -> task.name.startsWith('verify') }.configureEach {
|
||||
mustRunAfter(sharedOutputTasks)
|
||||
mustRunAfter(tasks.named('buildBukkit'))
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named('check').configure {
|
||||
dependsOn(selectedModdedBuilds.fabric)
|
||||
description = "Builds and verifies the NeoForge ${minecraftVersion} jar."
|
||||
dependsOn(selectedModdedBuilds.neoforge)
|
||||
}
|
||||
|
||||
tasks.register('checkAll') {
|
||||
@@ -608,67 +537,10 @@ tasks.named('buildBukkit').configure {
|
||||
dependsOn('verifyBukkitArtifact')
|
||||
}
|
||||
|
||||
tasks.named('iris').configure {
|
||||
dependsOn('verifyBukkitArtifact')
|
||||
}
|
||||
|
||||
tasks.register('buildAllToOut') {
|
||||
group = 'iris'
|
||||
description = 'Builds and verifies the Bukkit jar and every mod target jar into the plugin output directory.'
|
||||
dependsOn('buildBukkit', ':spi:jar')
|
||||
dependsOn(moddedExports)
|
||||
doLast {
|
||||
File[] jars = pluginOutDirectory.listFiles((java.io.FileFilter) { File f -> f.name.startsWith('Iris v') && f.name.endsWith('.jar') })
|
||||
println('')
|
||||
println('=== Iris buildAllToOut -> ' + pluginOutDirectory + ' ===')
|
||||
if (jars != null) {
|
||||
jars.sort { it.name }.each { File f ->
|
||||
println(String.format(' %-60s %8d KB', f.name, (long) (f.length() / 1024)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('buildAll') {
|
||||
group = 'iris'
|
||||
outputs.upToDateWhen { false }
|
||||
dependsOn('iris', 'buildAllToOut')
|
||||
Map<String, List<Object>> consumerDrops = [
|
||||
(bukkitConsumerPath) : ['Iris.jar', consumerBukkitArtifact],
|
||||
(fabricConsumerPath) : ['Iris-Fabric.jar', fabricArtifact],
|
||||
(forgeConsumerPath) : ['Iris-Forge.jar', forgeArtifact],
|
||||
(neoForgeConsumerPath): ['Iris-Neoforge.jar', neoForgeArtifact]
|
||||
]
|
||||
doLast {
|
||||
consumerDrops.each { String consumerPath, List<Object> drop ->
|
||||
String keep = (String) drop.get(0)
|
||||
File source = (File) drop.get(1)
|
||||
irisFileOperations.fileSystemOperations.delete { DeleteSpec spec ->
|
||||
consumerDrops.values().each { List<Object> other ->
|
||||
String name = (String) other.get(0)
|
||||
if (name != keep) {
|
||||
spec.delete(new File(consumerPath, name))
|
||||
}
|
||||
}
|
||||
}
|
||||
irisFileOperations.fileSystemOperations.copy { CopySpec spec ->
|
||||
spec.from(source)
|
||||
spec.into(consumerPath)
|
||||
spec.rename { String ignored -> keep }
|
||||
}
|
||||
}
|
||||
println('')
|
||||
println("=== Iris buildAll -> ${consumerLocation} ===")
|
||||
println(" ${bukkitConsumerPath}/Iris.jar")
|
||||
println(" ${fabricConsumerPath}/Iris-Fabric.jar")
|
||||
println(" ${forgeConsumerPath}/Iris-Forge.jar")
|
||||
println(" ${neoForgeConsumerPath}/Iris-Neoforge.jar")
|
||||
}
|
||||
}
|
||||
tasks.register('buildPsychoLT') {
|
||||
group = 'development'
|
||||
description = 'Builds all verified Iris platform jars and stages them in the local test-server dropins and PluginOuts.'
|
||||
dependsOn('buildAll')
|
||||
group = 'build'
|
||||
description = 'Builds and verifies the Bukkit plugin, API, SPI and every supported mod target.'
|
||||
dependsOn('buildBukkit', 'irisApi', ':spi:jar', verifyModdedMatrix)
|
||||
}
|
||||
|
||||
tasks.register('irisDev', Copy) {
|
||||
@@ -688,12 +560,13 @@ if (!(irisRuntimeJava in [21, 25])) {
|
||||
throw new GradleException('irisRuntimeJava must be 21 or 25')
|
||||
}
|
||||
Set<String> sharedRuntimeProjects = [':core', ':core:agent', ':spi'] as Set<String>
|
||||
String runtimeOutputVariant = gradle.extensions.extraProperties.get('irisRuntimeOutputVariant').toString()
|
||||
|
||||
allprojects {
|
||||
apply plugin: 'java'
|
||||
boolean java21Runtime = irisRuntimeJava == 21 && sharedRuntimeProjects.contains(project.path)
|
||||
if (java21Runtime) {
|
||||
layout.buildDirectory.set(layout.projectDirectory.dir('build/java21'))
|
||||
if (!runtimeOutputVariant.isEmpty() && sharedRuntimeProjects.contains(project.path)) {
|
||||
layout.buildDirectory.set(layout.projectDirectory.dir("build/${runtimeOutputVariant}"))
|
||||
}
|
||||
|
||||
tasks.withType(Test).configureEach {
|
||||
@@ -1038,3 +911,5 @@ tasks.withType(PackedJar).configureEach {
|
||||
NativeRuntimeArtifacts.verifyPacked(task.outputJar.get().asFile, nativeRuntimeModules, embeddedNatives())
|
||||
}
|
||||
}
|
||||
|
||||
apply from: file('gradle/multiplexor.gradle')
|
||||
@@ -21,11 +21,14 @@ repositories {
|
||||
// Nothing else asks buildSrc for `test`: Gradle only requests the jar that carries the build logic.
|
||||
// Gating the jar on the tests is what makes them run at all, and it costs nothing once they are green.
|
||||
tasks.named('jar').configure {
|
||||
dependsOn(tasks.named('test'))
|
||||
if (!gradle.extensions.extraProperties.has('irisBuildLane')) {
|
||||
dependsOn(tasks.named('test'))
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named('test').configure {
|
||||
jvmArgs('--add-opens=java.base/java.lang=ALL-UNNAMED')
|
||||
jvmArgs('-XX:ParallelGCThreads=2', '-XX:ConcGCThreads=1', '-XX:CICompilerCount=2')
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
@@ -37,11 +37,6 @@ public class ApiGenerator implements Plugin<Project> {
|
||||
throw new GradleException("Publishing extension not found");
|
||||
}
|
||||
|
||||
publishing.getRepositories().maven(repository -> {
|
||||
repository.setName("deployDir");
|
||||
repository.setUrl(targetDirectory(target).toURI());
|
||||
});
|
||||
|
||||
publishing.getPublications().create("maven", MavenPublication.class, publication -> {
|
||||
publication.setGroupId(target.getGroup().toString());
|
||||
publication.setArtifactId("iris");
|
||||
@@ -49,27 +44,19 @@ public class ApiGenerator implements Plugin<Project> {
|
||||
publication.artifact(task);
|
||||
});
|
||||
}
|
||||
|
||||
public static File targetDirectory(Project project) {
|
||||
String dir = System.getenv("DEPLOY_DIR");
|
||||
if (dir == null) {
|
||||
return project.getLayout().getBuildDirectory().dir("api").get().getAsFile();
|
||||
}
|
||||
return new File(dir);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class GenerateApiTask extends DefaultTask {
|
||||
public GenerateApiTask() {
|
||||
setGroup("iris");
|
||||
dependsOn("jar");
|
||||
finalizedBy("publishMavenPublicationToDeployDirRepository");
|
||||
doLast(task -> getLogger().lifecycle("The API is located at " + getOutputFile().get().getAsFile().getAbsolutePath()));
|
||||
|
||||
TaskProvider<Jar> jarTask = getProject().getTasks().named("jar", Jar.class);
|
||||
getInputFile().convention(jarTask.flatMap(Jar::getArchiveFile));
|
||||
getOutputFile().convention(getProject().getLayout().file(getInputFile().getLocationOnly().map(input ->
|
||||
new File(ApiGenerator.targetDirectory(getProject()), input.getAsFile().getName()))));
|
||||
new File(getProject().getLayout().getBuildDirectory().dir("api").get().getAsFile(),
|
||||
input.getAsFile().getName()))));
|
||||
}
|
||||
|
||||
@InputFile
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import org.gradle.api.DefaultTask;
|
||||
import org.gradle.api.file.DirectoryProperty;
|
||||
import org.gradle.api.file.RegularFileProperty;
|
||||
import org.gradle.api.provider.ListProperty;
|
||||
import org.gradle.api.provider.MapProperty;
|
||||
import org.gradle.api.provider.Property;
|
||||
import org.gradle.api.provider.SetProperty;
|
||||
import org.gradle.api.tasks.Input;
|
||||
import org.gradle.api.tasks.Internal;
|
||||
import org.gradle.api.tasks.LocalState;
|
||||
import org.gradle.api.tasks.OutputFile;
|
||||
import org.gradle.api.tasks.TaskAction;
|
||||
import org.gradle.workers.WorkerExecutor;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.Map;
|
||||
|
||||
public abstract class IrisModdedBuild extends DefaultTask {
|
||||
public IrisModdedBuild() {
|
||||
getEnvironment().convention(Map.of());
|
||||
}
|
||||
|
||||
@Input
|
||||
public abstract Property<String> getLabel();
|
||||
|
||||
@Internal
|
||||
public abstract DirectoryProperty getWorkingDirectory();
|
||||
|
||||
@Input
|
||||
public abstract ListProperty<String> getCommandLine();
|
||||
|
||||
@Input
|
||||
public abstract MapProperty<String, String> getEnvironment();
|
||||
|
||||
@Input
|
||||
public abstract ListProperty<String> getRequiredEntries();
|
||||
|
||||
@Input
|
||||
public abstract SetProperty<String> getForbiddenCoreClasses();
|
||||
|
||||
@Input
|
||||
public abstract Property<Integer> getJavaRuntime();
|
||||
|
||||
@OutputFile
|
||||
public abstract RegularFileProperty getArtifactFile();
|
||||
|
||||
@LocalState
|
||||
public abstract RegularFileProperty getLogFile();
|
||||
|
||||
@Inject
|
||||
protected abstract WorkerExecutor getWorkerExecutor();
|
||||
|
||||
@TaskAction
|
||||
public void build() {
|
||||
getWorkerExecutor().noIsolation().submit(IrisModdedBuildAction.class, parameters -> {
|
||||
parameters.getLabel().set(getLabel());
|
||||
parameters.getWorkingDirectory().set(getWorkingDirectory());
|
||||
parameters.getCommandLine().set(getCommandLine());
|
||||
parameters.getEnvironment().set(getEnvironment());
|
||||
parameters.getRequiredEntries().set(getRequiredEntries());
|
||||
parameters.getForbiddenCoreClasses().set(getForbiddenCoreClasses());
|
||||
parameters.getJavaRuntime().set(getJavaRuntime());
|
||||
parameters.getArtifactFile().set(getArtifactFile());
|
||||
parameters.getLogFile().set(getLogFile());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import org.gradle.api.GradleException;
|
||||
import org.gradle.api.logging.Logger;
|
||||
import org.gradle.api.logging.Logging;
|
||||
import org.gradle.process.ExecOperations;
|
||||
import org.gradle.workers.WorkAction;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public abstract class IrisModdedBuildAction implements WorkAction<IrisModdedBuildParameters> {
|
||||
private static final Logger LOGGER = Logging.getLogger(IrisModdedBuildAction.class);
|
||||
private static final int LOG_TAIL_BYTES = 16 * 1024;
|
||||
private static final int LOG_TAIL_LINES = 40;
|
||||
|
||||
@Inject
|
||||
protected abstract ExecOperations getExecOperations();
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
IrisModdedBuildParameters parameters = getParameters();
|
||||
String label = parameters.getLabel().get();
|
||||
Path log = parameters.getLogFile().get().getAsFile().toPath();
|
||||
long started = System.nanoTime();
|
||||
LOGGER.lifecycle("{} started; log: {}", label, log);
|
||||
try {
|
||||
Files.createDirectories(log.getParent());
|
||||
try (OutputStream output = Files.newOutputStream(log)) {
|
||||
getExecOperations().exec(spec -> {
|
||||
spec.setWorkingDir(parameters.getWorkingDirectory().get().getAsFile());
|
||||
spec.commandLine(parameters.getCommandLine().get());
|
||||
spec.environment(parameters.getEnvironment().get());
|
||||
spec.setStandardOutput(output);
|
||||
spec.setErrorOutput(output);
|
||||
}).assertNormalExitValue();
|
||||
}
|
||||
ModdedArtifactVerifier.verify(parameters.getArtifactFile().get().getAsFile(),
|
||||
parameters.getRequiredEntries().get(), parameters.getForbiddenCoreClasses().get());
|
||||
ModdedArtifactVerifier.verifyJavaRuntime(parameters.getArtifactFile().get().getAsFile(),
|
||||
parameters.getJavaRuntime().get());
|
||||
LOGGER.lifecycle("{} succeeded in {} ms", label,
|
||||
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - started));
|
||||
} catch (Exception failure) {
|
||||
appendFailure(log, failure);
|
||||
String details;
|
||||
try {
|
||||
details = logTail(log);
|
||||
} catch (IOException unreadable) {
|
||||
failure.addSuppressed(unreadable);
|
||||
details = "Log could not be read.";
|
||||
}
|
||||
LOGGER.error(label + " failed; log: " + log + System.lineSeparator() + details, failure);
|
||||
throw new GradleException(label + " failed; log: " + log, failure);
|
||||
}
|
||||
}
|
||||
|
||||
static String logTail(Path log) throws IOException {
|
||||
try (RandomAccessFile input = new RandomAccessFile(log.toFile(), "r")) {
|
||||
long offset = Math.max(0L, input.length() - LOG_TAIL_BYTES);
|
||||
input.seek(offset);
|
||||
byte[] bytes = new byte[(int) (input.length() - offset)];
|
||||
input.readFully(bytes);
|
||||
String[] lines = new String(bytes, StandardCharsets.UTF_8).split("\\R");
|
||||
return String.join(System.lineSeparator(), Arrays.copyOfRange(lines,
|
||||
Math.max(0, lines.length - LOG_TAIL_LINES), lines.length));
|
||||
}
|
||||
}
|
||||
|
||||
private static void appendFailure(Path log, Exception failure) {
|
||||
try (PrintWriter output = new PrintWriter(Files.newBufferedWriter(log, StandardCharsets.UTF_8,
|
||||
StandardOpenOption.CREATE, StandardOpenOption.APPEND))) {
|
||||
failure.printStackTrace(output);
|
||||
} catch (IOException unwritable) {
|
||||
failure.addSuppressed(unwritable);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import org.gradle.api.file.DirectoryProperty;
|
||||
import org.gradle.api.file.RegularFileProperty;
|
||||
import org.gradle.api.provider.ListProperty;
|
||||
import org.gradle.api.provider.MapProperty;
|
||||
import org.gradle.api.provider.Property;
|
||||
import org.gradle.api.provider.SetProperty;
|
||||
import org.gradle.workers.WorkParameters;
|
||||
|
||||
public interface IrisModdedBuildParameters extends WorkParameters {
|
||||
Property<String> getLabel();
|
||||
|
||||
DirectoryProperty getWorkingDirectory();
|
||||
|
||||
ListProperty<String> getCommandLine();
|
||||
|
||||
MapProperty<String, String> getEnvironment();
|
||||
|
||||
ListProperty<String> getRequiredEntries();
|
||||
|
||||
SetProperty<String> getForbiddenCoreClasses();
|
||||
|
||||
Property<Integer> getJavaRuntime();
|
||||
|
||||
RegularFileProperty getArtifactFile();
|
||||
|
||||
RegularFileProperty getLogFile();
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import org.gradle.api.services.BuildService;
|
||||
import org.gradle.api.services.BuildServiceParameters;
|
||||
|
||||
public abstract class IrisModdedBuildService implements BuildService<BuildServiceParameters.None> {
|
||||
}
|
||||
@@ -231,11 +231,11 @@ public final class GenerationBuildRevision {
|
||||
throw new IOException("Duplicate dependency artifact entry: " + name);
|
||||
}
|
||||
}
|
||||
byte[] buffer = new byte[16 * 1024];
|
||||
for (Map.Entry<String, ZipEntry> entry : entries.entrySet()) {
|
||||
update(digest, entry.getKey());
|
||||
try (InputStream input = zip.getInputStream(entry.getValue())) {
|
||||
MessageDigest content = digest();
|
||||
byte[] buffer = new byte[16 * 1024];
|
||||
int read;
|
||||
while ((read = input.read(buffer)) != -1) {
|
||||
content.update(buffer, 0, read);
|
||||
|
||||
@@ -50,9 +50,12 @@ public class ApiGeneratorTest {
|
||||
|
||||
assertEquals(jar.getArchiveFile().get().getAsFile(), api.getInputFile().get().getAsFile());
|
||||
assertEquals(jar.getArchiveFileName().get(), api.getOutputFile().get().getAsFile().getName());
|
||||
assertEquals(ApiGenerator.targetDirectory(project), api.getOutputFile().get().getAsFile().getParentFile());
|
||||
assertEquals(project.getLayout().getBuildDirectory().dir("api").get().getAsFile(),
|
||||
api.getOutputFile().get().getAsFile().getParentFile());
|
||||
assertTrue(api.getFinalizedBy().getDependencies(api).isEmpty());
|
||||
|
||||
PublishingExtension publishing = project.getExtensions().getByType(PublishingExtension.class);
|
||||
assertTrue(publishing.getRepositories().isEmpty());
|
||||
MavenPublication publication = (MavenPublication) publishing.getPublications().getByName("maven");
|
||||
assertEquals("art.arcane", publication.getGroupId());
|
||||
assertEquals("iris", publication.getArtifactId());
|
||||
|
||||
@@ -58,6 +58,22 @@ public class GenerationBuildRevisionTest {
|
||||
GenerationBuildRevision.capture(fixture.options(), dependencies)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hashesLargeShortAndEmptyEntriesIndependently() throws Exception {
|
||||
Path dependency = temporary.newFile("multiple-entries.jar").toPath();
|
||||
Map<String, String> contents = Map.of("example/Large.class", "abc".repeat(11_000),
|
||||
"example/Short.class", "tail", "example/Zero.class", "");
|
||||
try (ZipOutputStream output = new ZipOutputStream(Files.newOutputStream(dependency))) {
|
||||
for (String name : List.of("example/Zero.class", "example/Short.class", "example/Large.class")) {
|
||||
output.putNextEntry(new ZipEntry(name));
|
||||
output.write(contents.get(name).getBytes(StandardCharsets.UTF_8));
|
||||
output.closeEntry();
|
||||
}
|
||||
}
|
||||
assertEquals("49217af891afa3111d1f70a390acacc8bad700528b61b1ecc5637c060aeb52cd",
|
||||
GenerationBuildRevision.artifactHash(dependency));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsChangedAddedAndDeletedGenerationSources() throws Exception {
|
||||
Fixture fixture = fixture();
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.testfixtures.ProjectBuilder;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IrisModdedBuildTest {
|
||||
@Rule
|
||||
public TemporaryFolder temporary = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void capturesTheCompleteTargetConfiguration() throws Exception {
|
||||
Project project = ProjectBuilder.builder().withProjectDir(temporary.newFolder()).build();
|
||||
IrisModdedBuild task = project.getTasks().create("verifyFabric", IrisModdedBuild.class);
|
||||
task.getLabel().set("Fabric 26.3");
|
||||
task.getWorkingDirectory().set(project.getLayout().getProjectDirectory());
|
||||
task.getCommandLine().set(List.of("gradlew", "assemble", "test"));
|
||||
task.getRequiredEntries().set(List.of("fabric.mod.json"));
|
||||
task.getForbiddenCoreClasses().set(Set.of("org/bukkit/Test"));
|
||||
task.getJavaRuntime().set(25);
|
||||
task.getArtifactFile().set(project.getLayout().getProjectDirectory().file("build/mod.jar"));
|
||||
task.getLogFile().set(project.getLayout().getProjectDirectory().file("build/reports/matrix/fabric.log"));
|
||||
|
||||
assertEquals(Map.of(), task.getEnvironment().get());
|
||||
assertEquals(List.of("gradlew", "assemble", "test"), task.getCommandLine().get());
|
||||
assertEquals(List.of("fabric.mod.json"), task.getRequiredEntries().get());
|
||||
assertEquals(Set.of("org/bukkit/Test"), task.getForbiddenCoreClasses().get());
|
||||
assertEquals(Integer.valueOf(25), task.getJavaRuntime().get());
|
||||
assertEquals(Set.of(task.getArtifactFile().get().getAsFile()), task.getOutputs().getFiles().getFiles());
|
||||
assertFalse(task.getOutputs().getFiles().contains(task.getLogFile().get().getAsFile()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failureSummaryRetainsTheFinalLinesWithoutReadingAnEntireLog() throws Exception {
|
||||
Path log = temporary.newFile().toPath();
|
||||
StringBuilder content = new StringBuilder("discarded first line\n");
|
||||
for (int index = 0; index < 2000; index++) {
|
||||
content.append("task output ").append(index).append('\n');
|
||||
}
|
||||
content.append("final failure\n");
|
||||
Files.writeString(log, content);
|
||||
|
||||
String tail = IrisModdedBuildAction.logTail(log);
|
||||
|
||||
assertEquals(40, tail.lines().count());
|
||||
assertTrue(tail.endsWith("final failure"));
|
||||
assertFalse(tail.contains("discarded first line"));
|
||||
assertTrue(tail.length() <= 16 * 1024);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failureSummaryAcceptsEmptyAndUnterminatedLogs() throws Exception {
|
||||
Path log = temporary.newFile().toPath();
|
||||
assertEquals("", IrisModdedBuildAction.logTail(log));
|
||||
Files.writeString(log, "first\r\nlast");
|
||||
assertEquals("first" + System.lineSeparator() + "last", IrisModdedBuildAction.logTail(log));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failureSummaryBoundsAnUnbrokenFailureLine() throws Exception {
|
||||
Path log = temporary.newFile().toPath();
|
||||
Files.writeString(log, "x".repeat(100_000) + "final failure");
|
||||
|
||||
String tail = IrisModdedBuildAction.logTail(log);
|
||||
|
||||
assertEquals(16 * 1024, tail.length());
|
||||
assertTrue(tail.endsWith("final failure"));
|
||||
}
|
||||
}
|
||||
+24
-2
@@ -1,3 +1,5 @@
|
||||
import groovy.json.JsonSlurper
|
||||
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
||||
import io.github.slimjar.resolver.data.Mirror
|
||||
import org.gradle.api.attributes.java.TargetJvmVersion
|
||||
import org.gradle.api.file.FileTreeElement
|
||||
@@ -197,8 +199,16 @@ slimJar {
|
||||
}
|
||||
}
|
||||
|
||||
TaskProvider<Task> slimJarTask = tasks.named('slimJar')
|
||||
tasks.withType(ShadowJar).configureEach {
|
||||
dependsOn(slimJarTask)
|
||||
}
|
||||
|
||||
def embeddedAgentJar = project(':core:agent').tasks.named('jar', Jar)
|
||||
tasks.named('processResources').configure {
|
||||
tasks.named('processResources').configure { Task resources ->
|
||||
resources.setFinalizedBy(resources.finalizedBy.getDependencies(resources).findAll { Task finalizer ->
|
||||
finalizer != slimJarTask.get()
|
||||
})
|
||||
from(embeddedAgentJar.map { it.archiveFile }) {
|
||||
rename { String ignored -> 'agent.jar' }
|
||||
}
|
||||
@@ -217,6 +227,18 @@ def generateTemplates = tasks.register('generateTemplates', Copy) {
|
||||
return 'development'
|
||||
},
|
||||
commit: providers.provider {
|
||||
String sourceHashesJson = providers.environmentVariable('MULTIPLEXOR_SOURCE_HASHES').orNull
|
||||
if (sourceHashesJson != null) {
|
||||
Object sourceHashes = new JsonSlurper().parseText(sourceHashesJson)
|
||||
if (!(sourceHashes instanceof Map)) {
|
||||
throw new GradleException('MULTIPLEXOR_SOURCE_HASHES must map project paths to SHA-256 source fingerprints')
|
||||
}
|
||||
Object sourceHash = sourceHashes.get(rootProject.projectDir.canonicalPath)
|
||||
if (!(sourceHash instanceof String) || !(sourceHash ==~ /[a-f0-9]{64}/)) {
|
||||
throw new GradleException("MULTIPLEXOR_SOURCE_HASHES requires a SHA-256 source fingerprint for ${rootProject.projectDir.canonicalPath}")
|
||||
}
|
||||
return "source-${sourceHash}"
|
||||
}
|
||||
String commitId = null
|
||||
Exception failure = null
|
||||
try {
|
||||
@@ -447,7 +469,7 @@ List<String> dependencyBuildMetadata = [
|
||||
|
||||
String languageTomlSupportClassPattern = 'art/arcane/volmlib/util/config/(?:TomlCodec|ConfigJson|ConfigExposePolicy|ConfigDoc|ConfigDescription|ConfigDocumentation)(?:\\$[^/]+)?\\.class'
|
||||
|
||||
tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar).configure {
|
||||
tasks.named('shadowJar', ShadowJar).configure {
|
||||
inputs.property('languageTomlSupportClassPattern', languageTomlSupportClassPattern)
|
||||
dependsOn(embeddedAgentJar)
|
||||
mergeServiceFiles()
|
||||
|
||||
+51
@@ -15,6 +15,7 @@ import net.momirealms.craftengine.bukkit.api.CraftEngineFurniture;
|
||||
import net.momirealms.craftengine.bukkit.api.CraftEngineItems;
|
||||
import net.momirealms.craftengine.bukkit.api.event.CraftEngineReloadEvent;
|
||||
import net.momirealms.craftengine.bukkit.item.BukkitItemDefinition;
|
||||
import net.momirealms.craftengine.bukkit.entity.furniture.BukkitFurniture;
|
||||
import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine;
|
||||
import net.momirealms.craftengine.core.block.BlockDefinition;
|
||||
import net.momirealms.craftengine.core.block.ImmutableBlockState;
|
||||
@@ -24,10 +25,12 @@ import net.momirealms.craftengine.core.entity.furniture.FurnitureVariant;
|
||||
import net.momirealms.craftengine.core.registry.Holder;
|
||||
import net.momirealms.craftengine.core.util.Key;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.junit.Test;
|
||||
@@ -37,6 +40,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.MissingResourceException;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
@@ -48,12 +52,59 @@ import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class CraftEngineDataProviderTest {
|
||||
@Test
|
||||
public void furniturePlacementReturnsExactEntityTokenAndCleanupKeepsReplacement() {
|
||||
FurnitureDefinition definition = furnitureDefinition();
|
||||
Engine engine = mock(Engine.class);
|
||||
SeedManager seeds = mock(SeedManager.class);
|
||||
when(engine.getSeedManager()).thenReturn(seeds);
|
||||
Block block = mock(Block.class);
|
||||
when(block.getLocation()).thenAnswer(invocation -> new Location(null, 0, 70, 0));
|
||||
Chunk chunk = mock(Chunk.class);
|
||||
when(block.getChunk()).thenReturn(chunk);
|
||||
when(chunk.isEntitiesLoaded()).thenReturn(true);
|
||||
Entity original = mock(Entity.class);
|
||||
Entity replacement = mock(Entity.class);
|
||||
UUID originalId = UUID.randomUUID();
|
||||
when(original.getUniqueId()).thenReturn(originalId);
|
||||
when(replacement.getUniqueId()).thenReturn(UUID.randomUUID());
|
||||
when(chunk.getEntities()).thenReturn(new Entity[]{replacement, original});
|
||||
BukkitFurniture placed = mock(BukkitFurniture.class);
|
||||
when(placed.baseEntity()).thenReturn(original);
|
||||
CraftEngineDataProvider provider = new CraftEngineDataProvider();
|
||||
try (MockedStatic<CraftEngineBlocks> blocks = mockStatic(CraftEngineBlocks.class);
|
||||
MockedStatic<CraftEngineFurniture> furniture = mockStatic(CraftEngineFurniture.class)) {
|
||||
furniture.when(() -> CraftEngineFurniture.byId(Key.of("test:chair"))).thenReturn(definition);
|
||||
furniture.when(() -> CraftEngineFurniture.place(any(Location.class), eq(definition), eq("alpha"), eq(false)))
|
||||
.thenReturn(placed);
|
||||
furniture.when(() -> CraftEngineFurniture.remove(original, false, false)).thenReturn(true);
|
||||
|
||||
String token = provider.processUpdate(engine, block, Identifier.fromString("test:chair"));
|
||||
|
||||
assertEquals(originalId.toString(), token);
|
||||
assertTrue(provider.removeGenerated(block, token));
|
||||
furniture.verify(() -> CraftEngineFurniture.remove(original, false, false));
|
||||
furniture.verify(() -> CraftEngineFurniture.remove(replacement, false, false), never());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void furnitureCleanupWaitsForEntitiesToLoad() {
|
||||
Block block = mock(Block.class);
|
||||
Chunk chunk = mock(Chunk.class);
|
||||
when(block.getChunk()).thenReturn(chunk);
|
||||
|
||||
assertFalse(new CraftEngineDataProvider().removeGenerated(block, UUID.randomUUID().toString()));
|
||||
verify(chunk, never()).getEntities();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void startupWaitsForReloadEventAndEmptyLoadedRegistriesAreReady() {
|
||||
CraftEngineDataProvider provider = spy(new CraftEngineDataProvider());
|
||||
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
package art.arcane.iris.platform.generation;
|
||||
|
||||
import art.arcane.iris.generation.block.IrisCustomData;
|
||||
import art.arcane.iris.generation.mantle.CustomBlockOwnership;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.SeedManager;
|
||||
import art.arcane.iris.integration.ExternalDataProvider;
|
||||
import art.arcane.iris.integration.ExternalDataSVC;
|
||||
import art.arcane.iris.integration.Identifier;
|
||||
import art.arcane.iris.integration.data.CraftEngineDataProvider;
|
||||
import art.arcane.iris.platform.bukkit.BukkitBlockResolution;
|
||||
import art.arcane.iris.platform.registry.RegistryUtil;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.testsupport.BukkitTestServer;
|
||||
import art.arcane.iris.world.IrisWorld;
|
||||
import art.arcane.volmlib.util.function.Consumer4;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.matter.MatterSlice;
|
||||
import net.momirealms.craftengine.bukkit.api.CraftEngineBlocks;
|
||||
import net.momirealms.craftengine.bukkit.api.CraftEngineFurniture;
|
||||
import net.momirealms.craftengine.bukkit.api.CraftEngineItems;
|
||||
import net.momirealms.craftengine.bukkit.api.event.CraftEngineReloadEvent;
|
||||
import net.momirealms.craftengine.bukkit.entity.furniture.BukkitFurniture;
|
||||
import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine;
|
||||
import net.momirealms.craftengine.core.block.BlockDefinition;
|
||||
import net.momirealms.craftengine.core.block.ImmutableBlockState;
|
||||
import net.momirealms.craftengine.core.entity.furniture.FurnitureDefinition;
|
||||
import net.momirealms.craftengine.core.entity.furniture.FurnitureVariant;
|
||||
import net.momirealms.craftengine.core.registry.Holder;
|
||||
import net.momirealms.craftengine.core.util.Key;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class CraftEngineStaleRecordTest {
|
||||
@Test
|
||||
public void stalePlacementRecordsAreSkippedAndIntactOnesPlaced() throws Exception {
|
||||
BukkitTestServer.install();
|
||||
BlockDefinition lampDefinition = mock(BlockDefinition.class);
|
||||
ImmutableBlockState lamp = mock(ImmutableBlockState.class);
|
||||
when(lamp.owner()).thenReturn(Holder.direct(lampDefinition));
|
||||
when(lamp.toString()).thenReturn("test:lamp");
|
||||
when(lampDefinition.defaultState()).thenReturn(lamp);
|
||||
FurnitureDefinition chairDefinition = mock(FurnitureDefinition.class);
|
||||
when(chairDefinition.id()).thenReturn(Key.of("test:chair"));
|
||||
when(chairDefinition.variants()).thenReturn(Map.of("alpha", mock(FurnitureVariant.class)));
|
||||
BlockData noteBlock = blockData(Material.NOTE_BLOCK);
|
||||
BlockData air = blockData(Material.AIR);
|
||||
CraftEngineDataProvider provider = new CraftEngineDataProvider();
|
||||
ExternalDataSVC service = new ExternalDataSVC();
|
||||
activate(service, provider);
|
||||
|
||||
Engine engine = mock(Engine.class);
|
||||
when(engine.getWorld()).thenReturn(mock(IrisWorld.class));
|
||||
SeedManager seeds = mock(SeedManager.class);
|
||||
when(engine.getSeedManager()).thenReturn(seeds);
|
||||
World world = mock(World.class);
|
||||
when(world.getMinHeight()).thenReturn(0);
|
||||
when(world.getMaxHeight()).thenReturn(256);
|
||||
Chunk chunk = mock(Chunk.class);
|
||||
when(chunk.getWorld()).thenReturn(world);
|
||||
block(chunk, 1, Material.NOTE_BLOCK);
|
||||
block(chunk, 2, Material.AIR);
|
||||
block(chunk, 3, Material.COBBLESTONE);
|
||||
block(chunk, 4, Material.COBBLESTONE);
|
||||
|
||||
try (MockedStatic<BukkitCraftEngine> craftEngine = mockStatic(BukkitCraftEngine.class);
|
||||
MockedStatic<CraftEngineBlocks> blocks = mockStatic(CraftEngineBlocks.class);
|
||||
MockedStatic<CraftEngineFurniture> furniture = mockStatic(CraftEngineFurniture.class);
|
||||
MockedStatic<CraftEngineItems> items = mockStatic(CraftEngineItems.class);
|
||||
MockedStatic<RegistryUtil> registry = materialRegistry();
|
||||
MockedStatic<BukkitBlockResolution> resolution = mockStatic(BukkitBlockResolution.class);
|
||||
MockedStatic<IrisServices> services = mockStatic(IrisServices.class)) {
|
||||
resolution.when(BukkitBlockResolution::getAir).thenReturn(air);
|
||||
services.when(() -> IrisServices.get(ExternalDataSVC.class)).thenReturn(service);
|
||||
blocks.when(CraftEngineBlocks::loadedBlocks).thenReturn(Map.of(Key.of("test:lamp"), lampDefinition));
|
||||
furniture.when(CraftEngineFurniture::loadedFurniture).thenReturn(Map.of(Key.of("test:chair"), chairDefinition));
|
||||
items.when(CraftEngineItems::loadedItems).thenReturn(Map.of());
|
||||
blocks.when(() -> CraftEngineBlocks.byId(Key.of("test:lamp"))).thenReturn(lampDefinition);
|
||||
blocks.when(() -> CraftEngineBlocks.getBukkitBlockData(lamp)).thenReturn(noteBlock);
|
||||
blocks.when(() -> CraftEngineBlocks.place(any(Location.class), eq(lamp), eq(false))).thenReturn(true);
|
||||
furniture.when(() -> CraftEngineFurniture.byId(Key.of("test:chair"))).thenReturn(chairDefinition);
|
||||
BukkitFurniture placed = mock(BukkitFurniture.class);
|
||||
Entity entity = mock(Entity.class);
|
||||
when(placed.baseEntity()).thenReturn(entity);
|
||||
when(entity.getUniqueId()).thenReturn(UUID.randomUUID());
|
||||
furniture.when(() -> CraftEngineFurniture.place(any(Location.class), eq(chairDefinition), eq("alpha"), eq(false)))
|
||||
.thenReturn(placed);
|
||||
provider.onReload(mock(CraftEngineReloadEvent.class));
|
||||
Identifier lampRecord = generatedRecord(service, "test:lamp");
|
||||
Identifier chairRecord = generatedRecord(service, "test:chair");
|
||||
assertEquals("craftengine:test/lamp", lampRecord.toString());
|
||||
assertEquals("craftengine:test/chair[variant=alpha]", chairRecord.toString());
|
||||
|
||||
EngineBukkitOps.materializeCustomBlocks(engine, chunk, records(
|
||||
new Placed(1, lampRecord),
|
||||
new Placed(2, lampRecord),
|
||||
new Placed(3, lampRecord),
|
||||
new Placed(4, chairRecord)));
|
||||
|
||||
blocks.verify(() -> CraftEngineBlocks.place(any(Location.class), eq(lamp), eq(false)), times(1));
|
||||
blocks.verify(() -> CraftEngineBlocks.place(location(1), lamp, false));
|
||||
blocks.verify(() -> CraftEngineBlocks.place(location(2), lamp, false), never());
|
||||
blocks.verify(() -> CraftEngineBlocks.place(location(3), lamp, false), never());
|
||||
furniture.verify(() -> CraftEngineFurniture.place(location(4), chairDefinition, "alpha", false));
|
||||
}
|
||||
}
|
||||
|
||||
private static MockedStatic<RegistryUtil> materialRegistry() {
|
||||
MockedStatic<RegistryUtil> registry = mockStatic(RegistryUtil.class);
|
||||
registry.when(() -> RegistryUtil.find(Material.class, "grass", "short_grass")).thenReturn(Material.SHORT_GRASS);
|
||||
return registry;
|
||||
}
|
||||
|
||||
private static Identifier generatedRecord(ExternalDataSVC service, String key) {
|
||||
BlockData resolved = service.getBlockData(Identifier.fromString(key)).orElseThrow();
|
||||
return ((IrisCustomData) resolved).getCustom();
|
||||
}
|
||||
|
||||
private static BlockData blockData(Material material) {
|
||||
BlockData data = mock(BlockData.class);
|
||||
when(data.getMaterial()).thenReturn(material);
|
||||
return data;
|
||||
}
|
||||
|
||||
private static Block block(Chunk chunk, int offset, Material type) {
|
||||
Block block = mock(Block.class);
|
||||
when(block.getType()).thenReturn(type);
|
||||
when(block.getX()).thenReturn(offset);
|
||||
when(block.getY()).thenReturn(10);
|
||||
when(block.getZ()).thenReturn(offset);
|
||||
when(block.getLocation()).thenAnswer((InvocationOnMock invocation) -> location(offset));
|
||||
when(chunk.getBlock(offset, 10, offset)).thenReturn(block);
|
||||
return block;
|
||||
}
|
||||
|
||||
private static Location location(int offset) {
|
||||
return new Location(null, offset, 10, offset);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static MantleChunk<Matter> records(Placed... records) {
|
||||
MantleChunk<Matter> chunk = mock(MantleChunk.class);
|
||||
Matter section = mock(Matter.class);
|
||||
MatterSlice<CustomBlockOwnership> ownership = mock(MatterSlice.class);
|
||||
MatterSlice<Identifier> deferred = mock(MatterSlice.class);
|
||||
when(chunk.getOrCreate(anyInt())).thenReturn(section);
|
||||
when(section.<CustomBlockOwnership>slice(CustomBlockOwnership.class)).thenReturn(ownership);
|
||||
when(section.getSlice(Identifier.class)).thenReturn(deferred);
|
||||
for (Placed record : records) {
|
||||
when(chunk.get(record.offset(), 10, record.offset(), Identifier.class)).thenReturn(record.identifier());
|
||||
when(deferred.get(record.offset(), 10, record.offset())).thenReturn(record.identifier());
|
||||
}
|
||||
doAnswer((InvocationOnMock invocation) -> {
|
||||
Consumer4<Integer, Integer, Integer, Identifier> iterator = invocation.getArgument(1);
|
||||
for (Placed record : records) {
|
||||
iterator.accept(record.offset(), 10, record.offset(), record.identifier());
|
||||
}
|
||||
return null;
|
||||
}).when(chunk).iterate(eq(Identifier.class), any());
|
||||
return chunk;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static void activate(ExternalDataSVC service, ExternalDataProvider provider) throws Exception {
|
||||
Field field = ExternalDataSVC.class.getDeclaredField("activeProviders");
|
||||
field.setAccessible(true);
|
||||
((List<ExternalDataProvider>) field.get(service)).add(provider);
|
||||
}
|
||||
|
||||
private record Placed(int offset, Identifier identifier) {
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -242,11 +242,11 @@ public class IrisProceduralTree implements IrisProceduralPlacement {
|
||||
private IrisTreeRootStyle rootStyle = IrisTreeRootStyle.BUTTRESS;
|
||||
|
||||
@MinNumber(0)
|
||||
@Description("Override the root depth/reach in blocks. 0 means auto (scales with tree height).")
|
||||
@Description("Root depth in blocks, also used for ground-contact extension up to 16 blocks. 0 scales with tree height.")
|
||||
private int rootDepth = 0;
|
||||
|
||||
@MinNumber(0)
|
||||
@Description("Override the root flare radius in blocks. 0 means auto.")
|
||||
@Description("Horizontal reach of buttress and stilt roots. 0 uses a compact radius based on trunk width. Taproots ignore this field.")
|
||||
private double rootFlare = 0;
|
||||
|
||||
@MinNumber(1)
|
||||
|
||||
+30
-9
@@ -40,6 +40,12 @@ public final class ProceduralTreeGenerator {
|
||||
long baseSeed = rng.getSeed();
|
||||
|
||||
List<TreeTrunkBuilder.Limb> limbs = TreeTrunkBuilder.build(canvas, tree, height);
|
||||
Set<TreeBlockCanvas.Vec> supportPositions = new HashSet<>();
|
||||
for (TreeBlockCanvas.Vec position : canvas.getTrunk()) {
|
||||
if (position.y() == 0) {
|
||||
supportPositions.add(position);
|
||||
}
|
||||
}
|
||||
|
||||
boolean wantEndpoints = tree.getDecorators() != null && !tree.getDecorators().isEmpty();
|
||||
List<int[]> branchEndpoints = wantEndpoints ? new ArrayList<>() : null;
|
||||
@@ -55,7 +61,13 @@ public final class ProceduralTreeGenerator {
|
||||
}
|
||||
|
||||
if (tree.isRoots()) {
|
||||
Set<TreeBlockCanvas.Vec> existingWood = new HashSet<>(canvas.getTrunk());
|
||||
TreeRootBuilder.build(canvas, tree, height, baseSeed);
|
||||
for (TreeBlockCanvas.Vec position : canvas.getTrunk()) {
|
||||
if (position.y() <= 0 && !existingWood.contains(position)) {
|
||||
supportPositions.add(position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tree.isPlausible()) {
|
||||
@@ -82,10 +94,14 @@ public final class ProceduralTreeGenerator {
|
||||
|
||||
TreePlausibility.apply(resolved, trunkPositions, leafPositions, tree);
|
||||
|
||||
return assemble(resolved);
|
||||
int rootReach = tree.isRoots() ? tree.getRootDepth() > 0 ? tree.getRootDepth()
|
||||
: Math.max(2, (int) Math.round(height * 0.18)) : 0;
|
||||
return assemble(resolved, supportPositions, leafPositions, rootReach);
|
||||
}
|
||||
|
||||
private static IrisObject assemble(Map<TreeBlockCanvas.Vec, NativeBlockState> resolved) {
|
||||
private static IrisObject assemble(Map<TreeBlockCanvas.Vec, NativeBlockState> resolved,
|
||||
Set<TreeBlockCanvas.Vec> supportPositions,
|
||||
Set<TreeBlockCanvas.Vec> leafPositions, int rootReach) {
|
||||
if (resolved.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
@@ -105,21 +121,26 @@ public final class ProceduralTreeGenerator {
|
||||
maxZ = Math.max(maxZ, v.z());
|
||||
}
|
||||
|
||||
int w = maxX - minX + 1;
|
||||
int w = 2 * Math.max(Math.abs(minX), Math.abs(maxX)) + 1;
|
||||
int h = maxY - minY + 1;
|
||||
int d = maxZ - minZ + 1;
|
||||
int cx = w / 2;
|
||||
int d = 2 * Math.max(Math.abs(minZ), Math.abs(maxZ)) + 1;
|
||||
int cy = h / 2;
|
||||
int cz = d / 2;
|
||||
|
||||
IrisObject object = new IrisObject(w, h, d);
|
||||
List<TreeGrounding.Root> roots = new ArrayList<>(supportPositions.size());
|
||||
Set<TreeBlockCanvas.Vec> canopy = new HashSet<>(leafPositions.size());
|
||||
for (Map.Entry<TreeBlockCanvas.Vec, NativeBlockState> entry : resolved.entrySet()) {
|
||||
TreeBlockCanvas.Vec v = entry.getKey();
|
||||
int nx = v.x() - minX - cx;
|
||||
int ny = v.y() - cy + 1;
|
||||
int nz = v.z() - minZ - cz;
|
||||
object.getBlocks().put(new IrisBlockVector(nx, ny, nz), entry.getValue());
|
||||
object.getBlocks().put(new IrisBlockVector(v.x(), ny, v.z()), entry.getValue());
|
||||
if (supportPositions.contains(v)) {
|
||||
roots.add(new TreeGrounding.Root(v.x(), ny, v.z(), entry.getValue()));
|
||||
}
|
||||
if (leafPositions.contains(v)) {
|
||||
canopy.add(new TreeBlockCanvas.Vec(v.x(), ny, v.z()));
|
||||
}
|
||||
}
|
||||
object.setTreeGrounding(TreeGrounding.create(roots, rootReach, canopy));
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
+3
-1
@@ -335,7 +335,9 @@ public final class TreeCanopyBuilder {
|
||||
int x = (int) Math.round(ox + dx * t);
|
||||
int y = (int) Math.round(oy + dy * t - sagBlocks * t * t);
|
||||
int z = (int) Math.round(oz + dz * t);
|
||||
if (!canvas.has(x, y, z)) {
|
||||
TreeBlockCanvas.Cell existing = canvas.get(x, y, z);
|
||||
if (existing == null || existing.role() == TreeBlockCanvas.Role.LEAF
|
||||
|| existing.role() == TreeBlockCanvas.Role.SECONDARY_LEAF) {
|
||||
canvas.setTrunk(x, y, z, TreeBlockCanvas.Role.TRUNK, axis);
|
||||
}
|
||||
tip = new int[]{x, y, z};
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
package art.arcane.iris.generation.decoration.tree;
|
||||
|
||||
import art.arcane.iris.generation.geometry.IrisBlockVector;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.structure.object.IObjectPlacer;
|
||||
import art.arcane.iris.structure.object.IrisObjectPlacement;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.BlockPosition;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public record TreeGrounding(List<Root> roots, int maxExtension, Set<TreeBlockCanvas.Vec> canopy) {
|
||||
public TreeGrounding {
|
||||
roots = List.copyOf(roots);
|
||||
maxExtension = Math.max(0, Math.min(16, maxExtension));
|
||||
canopy = Set.copyOf(canopy);
|
||||
}
|
||||
|
||||
public static TreeGrounding create(List<Root> roots, int maxExtension, Set<TreeBlockCanvas.Vec> canopy) {
|
||||
Map<Long, Root> columns = new HashMap<>();
|
||||
for (Root root : roots) {
|
||||
long key = ((long) root.x() << 32) | (root.z() & 0xffffffffL);
|
||||
Root previous = columns.get(key);
|
||||
if (previous == null || root.y() < previous.y()) {
|
||||
columns.put(key, root);
|
||||
}
|
||||
}
|
||||
List<Root> lowest = new ArrayList<>(columns.values());
|
||||
lowest.sort(Comparator.comparingInt(Root::x).thenComparingInt(Root::z));
|
||||
return new TreeGrounding(lowest, maxExtension, canopy);
|
||||
}
|
||||
|
||||
public boolean isCanopy(int x, int y, int z) {
|
||||
return canopy.contains(new TreeBlockCanvas.Vec(x, y, z));
|
||||
}
|
||||
|
||||
public KList<IrisBlockVector> supportOffsets() {
|
||||
KList<IrisBlockVector> offsets = new KList<>();
|
||||
for (Root root : roots) {
|
||||
offsets.add(new IrisBlockVector(root.x(), root.y(), root.z()));
|
||||
}
|
||||
return offsets;
|
||||
}
|
||||
|
||||
public Plan plan(IObjectPlacer placer, Placement placement) {
|
||||
List<Extension> extensions = new ArrayList<>();
|
||||
IrisObjectPlacement config = placement.config();
|
||||
BlockPosition origin = placement.origin();
|
||||
BlockPosition spin = placement.spin();
|
||||
int solidDepth = Math.max(1, config.getSurfaceSupportDepth());
|
||||
for (Root root : roots) {
|
||||
IrisBlockVector offset = config.getRotation().rotate(
|
||||
new IrisBlockVector(root.x(), root.y(), root.z()), spin.getX(), spin.getY(), spin.getZ());
|
||||
offset = config.getTranslate().translate(offset, config.getRotation(), spin.getX(), spin.getY(), spin.getZ());
|
||||
int x = origin.getX() + (int) Math.round(offset.getX());
|
||||
int y = origin.getY() + (int) Math.round(offset.getY());
|
||||
int z = origin.getZ() + (int) Math.round(offset.getZ());
|
||||
int surface = placer.getHighest(x, z, placement.data(), true);
|
||||
int contact = Math.min(surface, y - 1);
|
||||
if (y - contact - 1 > maxExtension) {
|
||||
return Plan.UNSUPPORTED;
|
||||
}
|
||||
for (int below = 0; below < solidDepth; below++) {
|
||||
if (placer.isCarved(x, contact - below, z) || !placer.isSurfaceSolid(x, contact - below, z)) {
|
||||
return Plan.UNSUPPORTED;
|
||||
}
|
||||
}
|
||||
if (contact < y - 1) {
|
||||
NativeBlockState state = config.getRotation().rotate(root.state(), spin.getX(), spin.getY(), spin.getZ());
|
||||
if (state == null) {
|
||||
return Plan.UNSUPPORTED;
|
||||
}
|
||||
Extension extension = new Extension(x, y - 1, contact, z, state);
|
||||
if (intersectsStructure(placer, extension)) {
|
||||
return Plan.UNSUPPORTED;
|
||||
}
|
||||
extensions.add(extension);
|
||||
}
|
||||
}
|
||||
return new Plan(!roots.isEmpty(), List.copyOf(extensions));
|
||||
}
|
||||
|
||||
private static boolean intersectsStructure(IObjectPlacer placer, Extension extension) {
|
||||
Engine engine = placer.getEngine();
|
||||
if (engine == null) {
|
||||
return false;
|
||||
}
|
||||
KList<NativeStructureVolume> volumes = engine.getNativeStructureVolumes(
|
||||
extension.x(), extension.z(), extension.x(), extension.z());
|
||||
if (volumes == null) {
|
||||
return false;
|
||||
}
|
||||
for (NativeStructureVolume volume : volumes) {
|
||||
if (volume.intersects(extension.x(), extension.bottomY() + engine.getMinHeight(), extension.z(),
|
||||
extension.x(), extension.topY() + engine.getMinHeight(), extension.z())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public record Root(int x, int y, int z, NativeBlockState state) {
|
||||
}
|
||||
|
||||
public record Placement(IrisData data, IrisObjectPlacement config, BlockPosition origin, BlockPosition spin) {
|
||||
}
|
||||
|
||||
public record Extension(int x, int topY, int bottomY, int z, NativeBlockState state) {
|
||||
}
|
||||
|
||||
public record Plan(boolean supported, List<Extension> extensions) {
|
||||
private static final Plan UNSUPPORTED = new Plan(false, List.of());
|
||||
}
|
||||
}
|
||||
+104
-68
@@ -28,120 +28,152 @@ public final class TreeRootBuilder {
|
||||
}
|
||||
|
||||
public static void build(TreeBlockCanvas canvas, IrisProceduralTree tree, int height, long baseSeed) {
|
||||
List<int[]> cells = new ArrayList<>();
|
||||
List<TreeBlockCanvas.Vec> cells = new ArrayList<>();
|
||||
for (TreeBlockCanvas.Vec v : canvas.getTrunk()) {
|
||||
if (v.y() == 0) {
|
||||
cells.add(new int[]{v.x(), v.z()});
|
||||
cells.add(v);
|
||||
}
|
||||
}
|
||||
if (cells.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
cells.sort(TreeBlockCanvas.Vec::compareTo);
|
||||
|
||||
int depth = tree.getRootDepth() > 0 ? tree.getRootDepth() : autoDepth(height);
|
||||
RNG rng = new RNG(baseSeed ^ 0x5009L);
|
||||
|
||||
double cx = 0;
|
||||
double cz = 0;
|
||||
for (int[] c : cells) {
|
||||
cx += c[0];
|
||||
cz += c[1];
|
||||
for (TreeBlockCanvas.Vec cell : cells) {
|
||||
cx += cell.x();
|
||||
cz += cell.z();
|
||||
}
|
||||
cx /= cells.size();
|
||||
cz /= cells.size();
|
||||
|
||||
double baseRadius = 1.0;
|
||||
for (int[] c : cells) {
|
||||
baseRadius = Math.max(baseRadius, Math.hypot(c[0] - cx, c[1] - cz) + 0.5);
|
||||
for (TreeBlockCanvas.Vec cell : cells) {
|
||||
baseRadius = Math.max(baseRadius, Math.hypot(cell.x() - cx, cell.z() - cz) + 0.5);
|
||||
}
|
||||
double flare = tree.getRootFlare() > 0 ? tree.getRootFlare() : baseRadius + Math.max(2.0, depth * 0.6);
|
||||
double flare = tree.getRootFlare() > 0 ? tree.getRootFlare() : baseRadius + Math.max(0.75, baseRadius * 0.35);
|
||||
RootShape shape = new RootShape(cells, cx, cz, baseRadius, depth, flare);
|
||||
|
||||
IrisTreeRootStyle style = tree.getRootStyle();
|
||||
switch (style) {
|
||||
case TAPROOT -> taproot(canvas, cells, cx, cz, baseRadius, depth);
|
||||
case TAPROOT -> taproot(canvas, shape);
|
||||
case BUTTRESS -> {
|
||||
taproot(canvas, cells, cx, cz, baseRadius, depth);
|
||||
buttress(canvas, cells, cx, cz, baseRadius, depth, flare, rng);
|
||||
taproot(canvas, shape);
|
||||
buttress(canvas, shape, rng);
|
||||
}
|
||||
case STILT -> {
|
||||
taproot(canvas, cells, cx, cz, baseRadius, Math.min(depth, 3));
|
||||
stilts(canvas, cx, cz, baseRadius, flare, height, cells.size(), rng);
|
||||
stilts(canvas, shape, height, rng);
|
||||
taproot(canvas, new RootShape(cells, cx, cz, baseRadius, Math.min(depth, 3), flare));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void taproot(TreeBlockCanvas canvas, List<int[]> cells, double cx, double cz, double baseRadius, int depth) {
|
||||
int icx = (int) Math.round(cx);
|
||||
int icz = (int) Math.round(cz);
|
||||
for (int k = 1; k <= depth; k++) {
|
||||
double frac = k / (double) depth;
|
||||
double keepR = baseRadius * (1.0 - 0.6 * frac);
|
||||
for (int[] c : cells) {
|
||||
if (Math.hypot(c[0] - cx, c[1] - cz) <= keepR + 1e-6) {
|
||||
placeRoot(canvas, c[0], -k, c[1], TreeBlockCanvas.Axis.Y);
|
||||
private static void taproot(TreeBlockCanvas canvas, RootShape shape) {
|
||||
TreeBlockCanvas.Vec center = nearestCell(shape.cells(), shape.centerX(), shape.centerZ());
|
||||
for (int k = 1; k <= shape.depth(); k++) {
|
||||
double fraction = k / (double) shape.depth();
|
||||
double keepRadius = shape.radius() * (1.0 - fraction);
|
||||
for (TreeBlockCanvas.Vec cell : shape.cells()) {
|
||||
if (Math.hypot(cell.x() - shape.centerX(), cell.z() - shape.centerZ()) <= keepRadius) {
|
||||
placeRoot(canvas, cell.x(), -k, cell.z(), TreeBlockCanvas.Axis.Y);
|
||||
}
|
||||
}
|
||||
placeRoot(canvas, icx, -k, icz, TreeBlockCanvas.Axis.Y);
|
||||
placeRoot(canvas, center.x(), -k, center.z(), TreeBlockCanvas.Axis.Y);
|
||||
}
|
||||
}
|
||||
|
||||
private static void buttress(TreeBlockCanvas canvas, List<int[]> cells, double cx, double cz, double baseRadius,
|
||||
int depth, double flare, RNG rng) {
|
||||
int nLegs = Math.max(4, Math.min(12, cells.size() + 2));
|
||||
for (int li = 0; li < nLegs; li++) {
|
||||
double ang = 2.0 * Math.PI * li / nLegs + rng.d(-0.25, 0.25);
|
||||
double ca = Math.cos(ang);
|
||||
double sa = Math.sin(ang);
|
||||
int prevX = (int) Math.round(cx + baseRadius * ca);
|
||||
int prevZ = (int) Math.round(cz + baseRadius * sa);
|
||||
for (int k = 1; k <= depth; k++) {
|
||||
double frac = k / (double) depth;
|
||||
double r = baseRadius + (flare - baseRadius) * frac;
|
||||
int x = (int) Math.round(cx + r * ca);
|
||||
int z = (int) Math.round(cz + r * sa);
|
||||
placeRoot(canvas, x, -k, z, TreeBlockCanvas.Axis.Y);
|
||||
if (x != prevX || z != prevZ) {
|
||||
placeRoot(canvas, prevX, -k, prevZ, TreeBlockCanvas.Axis.Y);
|
||||
}
|
||||
prevX = x;
|
||||
prevZ = z;
|
||||
private static void buttress(TreeBlockCanvas canvas, RootShape shape, RNG rng) {
|
||||
int legs = Math.max(3, Math.min(8, shape.cells().size() + 2));
|
||||
for (int leg = 0; leg < legs; leg++) {
|
||||
double angle = 2.0 * Math.PI * leg / legs + rng.d(-0.25, 0.25);
|
||||
double reach = shape.flare() * rng.d(0.8, 1.0);
|
||||
int x = (int) Math.round(shape.centerX() + reach * Math.cos(angle));
|
||||
int z = (int) Math.round(shape.centerZ() + reach * Math.sin(angle));
|
||||
int depth = legDepth(shape.depth(), leg, legs);
|
||||
TreeBlockCanvas.Vec start = nearestCell(shape.cells(), x, z);
|
||||
rasterizeRoot(canvas, start, new TreeBlockCanvas.Vec(x, -depth, z));
|
||||
}
|
||||
}
|
||||
|
||||
private static void stilts(TreeBlockCanvas canvas, RootShape shape, int height, RNG rng) {
|
||||
int stiltTop = Math.min(height - 1, Math.max(2, Math.min(7, (int) Math.round(0.25 * height))));
|
||||
List<TreeBlockCanvas.Vec> attachments = new ArrayList<>();
|
||||
for (TreeBlockCanvas.Vec cell : canvas.getTrunk()) {
|
||||
if (cell.y() == stiltTop) {
|
||||
attachments.add(cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void stilts(TreeBlockCanvas canvas, double cx, double cz, double baseRadius, double flare,
|
||||
int height, int cellCount, RNG rng) {
|
||||
int stiltTop = Math.max(2, Math.min(7, (int) Math.round(0.25 * height)));
|
||||
int nProps = Math.max(4, Math.min(10, cellCount + 2));
|
||||
for (int p = 0; p < nProps; p++) {
|
||||
double ang = 2.0 * Math.PI * p / nProps + rng.d(-0.25, 0.25);
|
||||
double ca = Math.cos(ang);
|
||||
double sa = Math.sin(ang);
|
||||
int sx = (int) Math.round(cx + baseRadius * ca);
|
||||
int sz = (int) Math.round(cz + baseRadius * sa);
|
||||
int fx = (int) Math.round(cx + flare * ca);
|
||||
int fz = (int) Math.round(cz + flare * sa);
|
||||
rasterizeRoot(canvas, sx, stiltTop, sz, fx, -2, fz);
|
||||
if (attachments.isEmpty()) {
|
||||
attachments.addAll(shape.cells());
|
||||
}
|
||||
attachments.sort(TreeBlockCanvas.Vec::compareTo);
|
||||
int props = Math.max(3, Math.min(8, shape.cells().size() + 2));
|
||||
for (int prop = 0; prop < props; prop++) {
|
||||
double angle = 2.0 * Math.PI * prop / props + rng.d(-0.25, 0.25);
|
||||
double reach = shape.flare() * rng.d(0.8, 1.0);
|
||||
int x = (int) Math.round(shape.centerX() + reach * Math.cos(angle));
|
||||
int z = (int) Math.round(shape.centerZ() + reach * Math.sin(angle));
|
||||
int depth = legDepth(Math.min(shape.depth(), 3), prop, props);
|
||||
TreeBlockCanvas.Vec start = nearestCell(attachments, x, z);
|
||||
rasterizeRoot(canvas, start, new TreeBlockCanvas.Vec(x, -depth, z));
|
||||
}
|
||||
}
|
||||
|
||||
private static void rasterizeRoot(TreeBlockCanvas canvas, int ox, int oy, int oz, int ex, int ey, int ez) {
|
||||
int dx = ex - ox;
|
||||
int dy = ey - oy;
|
||||
int dz = ez - oz;
|
||||
private static int legDepth(int depth, int leg, int count) {
|
||||
return Math.max(1, (int) Math.round(depth * (0.55 + 0.45 * leg / Math.max(1, count - 1))));
|
||||
}
|
||||
|
||||
private static TreeBlockCanvas.Vec nearestCell(List<TreeBlockCanvas.Vec> cells, double x, double z) {
|
||||
TreeBlockCanvas.Vec nearest = cells.getFirst();
|
||||
double distance = Double.POSITIVE_INFINITY;
|
||||
for (TreeBlockCanvas.Vec cell : cells) {
|
||||
double dx = cell.x() - x;
|
||||
double dz = cell.z() - z;
|
||||
double candidateDistance = dx * dx + dz * dz;
|
||||
if (candidateDistance < distance) {
|
||||
nearest = cell;
|
||||
distance = candidateDistance;
|
||||
}
|
||||
}
|
||||
return nearest;
|
||||
}
|
||||
|
||||
private static void rasterizeRoot(TreeBlockCanvas canvas, TreeBlockCanvas.Vec start, TreeBlockCanvas.Vec end) {
|
||||
int dx = end.x() - start.x();
|
||||
int dy = end.y() - start.y();
|
||||
int dz = end.z() - start.z();
|
||||
int steps = Math.max(Math.max(Math.abs(dx), Math.abs(dy)), Math.max(Math.abs(dz), 1));
|
||||
TreeBlockCanvas.Axis axis = TreeFunctions.logAxis(dx, dy, dz);
|
||||
for (int i = 0; i <= steps; i++) {
|
||||
int x = start.x();
|
||||
int y = start.y();
|
||||
int z = start.z();
|
||||
for (int i = 1; i <= steps; i++) {
|
||||
double t = i / (double) steps;
|
||||
int x = (int) Math.round(ox + dx * t);
|
||||
int y = (int) Math.round(oy + dy * t);
|
||||
int z = (int) Math.round(oz + dz * t);
|
||||
placeRoot(canvas, x, y, z, axis);
|
||||
int nextX = (int) Math.round(start.x() + dx * t);
|
||||
int nextY = (int) Math.round(start.y() + dy * t);
|
||||
int nextZ = (int) Math.round(start.z() + dz * t);
|
||||
while (y != nextY) {
|
||||
y += Integer.signum(nextY - y);
|
||||
placeRoot(canvas, x, y, z, axis);
|
||||
}
|
||||
while (x != nextX) {
|
||||
x += Integer.signum(nextX - x);
|
||||
placeRoot(canvas, x, y, z, axis);
|
||||
}
|
||||
while (z != nextZ) {
|
||||
z += Integer.signum(nextZ - z);
|
||||
placeRoot(canvas, x, y, z, axis);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void placeRoot(TreeBlockCanvas canvas, int x, int y, int z, TreeBlockCanvas.Axis axis) {
|
||||
if (!canvas.has(x, y, z)) {
|
||||
TreeBlockCanvas.Cell cell = canvas.get(x, y, z);
|
||||
if (cell == null || cell.role() != TreeBlockCanvas.Role.TRUNK && cell.role() != TreeBlockCanvas.Role.SECONDARY_TRUNK) {
|
||||
canvas.setTrunk(x, y, z, TreeBlockCanvas.Role.TRUNK, axis);
|
||||
}
|
||||
}
|
||||
@@ -149,4 +181,8 @@ public final class TreeRootBuilder {
|
||||
private static int autoDepth(int height) {
|
||||
return Math.max(2, Math.min(16, (int) Math.round(0.18 * height)));
|
||||
}
|
||||
|
||||
private record RootShape(List<TreeBlockCanvas.Vec> cells, double centerX, double centerZ,
|
||||
double radius, int depth, double flare) {
|
||||
}
|
||||
}
|
||||
+55
-9
@@ -28,6 +28,7 @@ import art.arcane.iris.structure.object.IObjectPlacer;
|
||||
import art.arcane.iris.generation.block.TileData;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.math.BlockPosition;
|
||||
import art.arcane.iris.generation.block.B;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
|
||||
@@ -49,6 +50,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
||||
private final Long2IntOpenHashMap surfaceHeights;
|
||||
private final Long2IntOpenHashMap caveCeilings;
|
||||
private int blockWrites;
|
||||
private BlockPosition canopyPosition;
|
||||
|
||||
CaveObjectPlacementTransaction(IObjectPlacer delegate, int anchorY, int minDepthBelowSurface) {
|
||||
this.delegate = delegate;
|
||||
@@ -95,13 +97,36 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
||||
}
|
||||
}
|
||||
|
||||
for (BufferedMutation mutation : mutations) {
|
||||
mutation.apply(delegate);
|
||||
}
|
||||
applyMutations();
|
||||
discard();
|
||||
return CommitResult.COMMITTED;
|
||||
}
|
||||
|
||||
private void applyMutations() {
|
||||
boolean canopyOpen = false;
|
||||
boolean skipCanopy = false;
|
||||
try {
|
||||
for (BufferedMutation mutation : mutations) {
|
||||
if (mutation.value() instanceof NativeBlockState) {
|
||||
if (canopyOpen) {
|
||||
delegate.endTreeCanopy();
|
||||
canopyOpen = false;
|
||||
}
|
||||
skipCanopy = mutation.canopy()
|
||||
&& !delegate.beginTreeCanopy(new BlockPosition(mutation.x(), mutation.y(), mutation.z()));
|
||||
canopyOpen = mutation.canopy() && !skipCanopy;
|
||||
}
|
||||
if (!mutation.canopy() || !skipCanopy) {
|
||||
mutation.apply(delegate);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (canopyOpen) {
|
||||
delegate.endTreeCanopy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void discard() {
|
||||
mutations.clear();
|
||||
bufferedBlocks.clear();
|
||||
@@ -139,11 +164,30 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
||||
if (state == null) {
|
||||
return;
|
||||
}
|
||||
mutations.add(new BlockMutation(x, y, z, state));
|
||||
mutations.add(new BlockMutation(x, y, z, state, isCanopy(x, y, z)));
|
||||
bufferedBlocks.put(new PositionKey(x, y, z), state);
|
||||
blockWrites++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean beginTreeCanopy(BlockPosition position) {
|
||||
if (!IObjectPlacer.super.beginTreeCanopy(position)) {
|
||||
return false;
|
||||
}
|
||||
canopyPosition = position;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endTreeCanopy() {
|
||||
canopyPosition = null;
|
||||
}
|
||||
|
||||
private boolean isCanopy(int x, int y, int z) {
|
||||
return canopyPosition != null && canopyPosition.getX() == x
|
||||
&& canopyPosition.getY() == y && canopyPosition.getZ() == z;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeBlockState get(int x, int y, int z) {
|
||||
NativeBlockState buffered = bufferedBlocks.get(new PositionKey(x, y, z));
|
||||
@@ -198,7 +242,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
||||
@Override
|
||||
public void setTile(int x, int y, int z, TileData tile) {
|
||||
if (tile != null) {
|
||||
mutations.add(new TileMutation(x, y, z, tile));
|
||||
mutations.add(new TileMutation(x, y, z, tile, isCanopy(x, y, z)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,7 +251,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
mutations.add(new DataMutation(x, y, z, data));
|
||||
mutations.add(new DataMutation(x, y, z, data, isCanopy(x, y, z)));
|
||||
if (data instanceof NativeBlockState state) {
|
||||
bufferedBlocks.put(new PositionKey(x, y, z), state);
|
||||
blockWrites++;
|
||||
@@ -291,24 +335,26 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
||||
|
||||
Object value();
|
||||
|
||||
boolean canopy();
|
||||
|
||||
void apply(IObjectPlacer placer);
|
||||
}
|
||||
|
||||
private record BlockMutation(int x, int y, int z, NativeBlockState value) implements BufferedMutation {
|
||||
private record BlockMutation(int x, int y, int z, NativeBlockState value, boolean canopy) implements BufferedMutation {
|
||||
@Override
|
||||
public void apply(IObjectPlacer placer) {
|
||||
placer.set(x, y, z, value);
|
||||
}
|
||||
}
|
||||
|
||||
private record TileMutation(int x, int y, int z, TileData value) implements BufferedMutation {
|
||||
private record TileMutation(int x, int y, int z, TileData value, boolean canopy) implements BufferedMutation {
|
||||
@Override
|
||||
public void apply(IObjectPlacer placer) {
|
||||
placer.setTile(x, y, z, value);
|
||||
}
|
||||
}
|
||||
|
||||
private record DataMutation(int x, int y, int z, Object value) implements BufferedMutation {
|
||||
private record DataMutation(int x, int y, int z, Object value, boolean canopy) implements BufferedMutation {
|
||||
@Override
|
||||
public void apply(IObjectPlacer placer) {
|
||||
placer.setData(x, y, z, value);
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package art.arcane.iris.generation.mantle;
|
||||
|
||||
import art.arcane.iris.integration.Identifier;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public record CustomBlockOwnership(List<Entry> entries) {
|
||||
public CustomBlockOwnership {
|
||||
entries = List.copyOf(entries);
|
||||
}
|
||||
|
||||
public CustomBlockOwnership add(Entry entry) {
|
||||
if (entries.contains(entry)) {
|
||||
return this;
|
||||
}
|
||||
List<Entry> updated = new ArrayList<>(entries);
|
||||
updated.add(entry);
|
||||
return new CustomBlockOwnership(updated);
|
||||
}
|
||||
|
||||
public CustomBlockOwnership invalidate() {
|
||||
List<Entry> updated = new ArrayList<>(entries.size());
|
||||
for (Entry entry : entries) {
|
||||
updated.add(new Entry(entry.identifier(), entry.token(), true));
|
||||
}
|
||||
return new CustomBlockOwnership(updated);
|
||||
}
|
||||
|
||||
public CustomBlockOwnership remove(Entry entry) {
|
||||
List<Entry> updated = new ArrayList<>(entries);
|
||||
updated.remove(entry);
|
||||
return updated.isEmpty() ? null : new CustomBlockOwnership(updated);
|
||||
}
|
||||
|
||||
public record Entry(Identifier identifier, String token, boolean invalidated) {
|
||||
public Entry {
|
||||
Objects.requireNonNull(identifier);
|
||||
Objects.requireNonNull(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
package art.arcane.iris.generation.mantle;
|
||||
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.EnginePlatformHooks;
|
||||
import art.arcane.iris.integration.Identifier;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.world.storage.matter.TileWrapper;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockChangeSet;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockPoint;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.mantle.MantleClosedException;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.matter.MatterSlice;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
public final class DeferredPlacementRecords {
|
||||
private static final Set<MantleFlag> RETAINED_MARGIN_FLAGS = Set.of(
|
||||
MantleFlag.TILE, MantleFlag.CUSTOM, MantleFlag.UPDATE, MantleFlag.ETCHED,
|
||||
MantleFlag.INITIAL_SPAWNED, MantleFlag.INITIAL_SPAWNED_MARKER, MantleFlag.DISCOVERED);
|
||||
|
||||
private DeferredPlacementRecords() {
|
||||
}
|
||||
|
||||
public static void resetMantleChunk(Mantle<Matter> mantle, int chunkX, int chunkZ, boolean target) {
|
||||
MantleChunk<Matter> chunk = mantle.useChunk(chunkX, chunkZ);
|
||||
try {
|
||||
synchronized (chunk) {
|
||||
for (int index = 0; index < chunk.sectionCount(); index++) {
|
||||
Matter section = chunk.get(index);
|
||||
if (section == null) {
|
||||
continue;
|
||||
}
|
||||
section.getSliceTypes().removeIf(type -> type != CustomBlockOwnership.class);
|
||||
if (section.isEmpty()) {
|
||||
chunk.delete(index);
|
||||
}
|
||||
}
|
||||
for (ReservedFlag flag : ReservedFlag.values()) {
|
||||
if (target || !RETAINED_MARGIN_FLAGS.contains(flag)) {
|
||||
chunk.flag(flag, false);
|
||||
}
|
||||
}
|
||||
for (int ordinal = MantleFlag.MIN_ORDINAL; ordinal <= MantleFlag.MAX_ORDINAL; ordinal++) {
|
||||
chunk.flag(MantleFlag.of(ordinal), false);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
chunk.release();
|
||||
}
|
||||
}
|
||||
|
||||
public static void invalidateCustomBlockOwnership(MantleChunk<Matter> chunk) {
|
||||
synchronized (chunk) {
|
||||
List<Pending<CustomBlockOwnership>> records =
|
||||
snapshot(chunk, CustomBlockOwnership.class);
|
||||
for (Pending<CustomBlockOwnership> record : records) {
|
||||
NativeBlockPoint position = record.position();
|
||||
chunk.get(position.y() >> 4).slice(CustomBlockOwnership.class)
|
||||
.set(position.x() & 15, position.y() & 15, position.z() & 15, record.value().invalidate());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> List<Pending<T>> snapshot(MantleChunk<Matter> chunk, Class<T> type) {
|
||||
List<Pending<T>> records = new ArrayList<>();
|
||||
synchronized (chunk) {
|
||||
chunk.iterate(type, (x, y, z, value) -> records.add(new Pending<>(new NativeBlockPoint(x, y, z), value)));
|
||||
}
|
||||
return records;
|
||||
}
|
||||
|
||||
public static <T> boolean isPending(MantleChunk<Matter> chunk, Class<T> type, Pending<T> record) {
|
||||
NativeBlockPoint position = record.position();
|
||||
synchronized (chunk) {
|
||||
return Objects.equals(record.value(), chunk.get(position.x(), position.y(), position.z(), type));
|
||||
}
|
||||
}
|
||||
|
||||
public static NativeBlockChangeSet track(Engine engine, NativeBlockChangeSet.Bounds bounds) {
|
||||
Mantle<Matter> mantle = engine.getMantle().getMantle();
|
||||
for (int chunkX = bounds.minChunkX(); chunkX <= bounds.maxChunkX(); chunkX++) {
|
||||
for (int chunkZ = bounds.minChunkZ(); chunkZ <= bounds.maxChunkZ(); chunkZ++) {
|
||||
if (hasRecords(mantle, chunkX, chunkZ)) {
|
||||
return NativeBlockChangeSet.create(bounds);
|
||||
}
|
||||
}
|
||||
}
|
||||
return NativeBlockChangeSet.disabled();
|
||||
}
|
||||
|
||||
public static void invalidate(Engine engine, NativeBlockChangeSet changes) {
|
||||
if (changes.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Mantle<Matter> mantle = engine.getMantle().getMantle();
|
||||
int minHeight = engine.getMinHeight();
|
||||
changes.forEachChunk((chunkX, chunkZ) -> {
|
||||
MantleChunk<Matter> chunk = invalidateChunk(mantle, changes, chunkX, chunkZ, minHeight);
|
||||
if (chunk != null) {
|
||||
scheduleRemoval(engine, chunk, minHeight);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static NativeBlockChangeSet positions(Engine engine, NativeBlockChangeSet.Bounds bounds) {
|
||||
Mantle<Matter> mantle = engine.getMantle().getMantle();
|
||||
NativeBlockChangeSet positions = null;
|
||||
int minHeight = engine.getMinHeight();
|
||||
for (int chunkX = bounds.minChunkX(); chunkX <= bounds.maxChunkX(); chunkX++) {
|
||||
for (int chunkZ = bounds.minChunkZ(); chunkZ <= bounds.maxChunkZ(); chunkZ++) {
|
||||
MantleChunk<Matter> chunk = mantle.useChunk(chunkX, chunkZ);
|
||||
try {
|
||||
synchronized (chunk) {
|
||||
if (!hasRecords(chunk)) {
|
||||
continue;
|
||||
}
|
||||
if (positions == null) {
|
||||
positions = NativeBlockChangeSet.create(bounds);
|
||||
}
|
||||
collectPositions(chunk, positions, Identifier.class, minHeight);
|
||||
collectPositions(chunk, positions, TileWrapper.class, minHeight);
|
||||
collectPositions(chunk, positions, CustomBlockOwnership.class, minHeight);
|
||||
}
|
||||
} finally {
|
||||
chunk.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
return positions == null || positions.isEmpty() ? NativeBlockChangeSet.disabled() : positions;
|
||||
}
|
||||
|
||||
public static void remember(Engine engine, MantleChunk<Matter> chunk, NativeBlockPoint position,
|
||||
CustomBlockOwnership.Entry entry) {
|
||||
boolean invalidated;
|
||||
synchronized (chunk) {
|
||||
Matter section = chunk.getOrCreate(position.y() >> 4);
|
||||
MatterSlice<Identifier> deferred = section.getSlice(Identifier.class);
|
||||
invalidated = entry.invalidated() || deferred == null
|
||||
|| deferred.get(position.x() & 15, position.y() & 15, position.z() & 15) == null;
|
||||
CustomBlockOwnership.Entry completed = invalidated
|
||||
? new CustomBlockOwnership.Entry(entry.identifier(), entry.token(), true) : entry;
|
||||
MatterSlice<CustomBlockOwnership> slice = section.slice(CustomBlockOwnership.class);
|
||||
CustomBlockOwnership existing = slice.get(position.x() & 15, position.y() & 15, position.z() & 15);
|
||||
CustomBlockOwnership updated = existing == null
|
||||
? new CustomBlockOwnership(List.of(completed)) : existing.add(completed);
|
||||
slice.set(position.x() & 15, position.y() & 15, position.z() & 15, updated);
|
||||
}
|
||||
if (invalidated) {
|
||||
scheduleRemoval(engine, chunk, engine.getMinHeight());
|
||||
}
|
||||
}
|
||||
|
||||
public static void removeInvalidated(Engine engine, int chunkX, int chunkZ) {
|
||||
removeInvalidated(engine, engine.getMantle().getMantle(), chunkX, chunkZ);
|
||||
}
|
||||
|
||||
public static void removeInvalidated(Engine engine, Mantle<Matter> mantle, int chunkX, int chunkZ) {
|
||||
NativeWorld world = engine.getWorld().platformWorld();
|
||||
if (world == null || engine.isClosed() || !world.isChunkLoaded(chunkX, chunkZ)) {
|
||||
return;
|
||||
}
|
||||
if (mantle.isClosed()) {
|
||||
return;
|
||||
}
|
||||
MantleChunk<Matter> chunk = mantle.useChunk(chunkX, chunkZ);
|
||||
try {
|
||||
removeInvalidated(engine, chunk, engine.getMinHeight());
|
||||
} finally {
|
||||
chunk.release();
|
||||
}
|
||||
}
|
||||
|
||||
private static void scheduleRemoval(Engine engine, MantleChunk<Matter> chunk, int minHeight) {
|
||||
NativeWorld world = engine.getWorld().platformWorld();
|
||||
if (world != null && IrisPlatforms.isBound()) {
|
||||
IrisPlatforms.get().scheduler().region(world, chunk.getX(), chunk.getZ(),
|
||||
() -> removeScheduled(new ScheduledRemoval(engine, world, chunk, minHeight)));
|
||||
}
|
||||
}
|
||||
|
||||
private static void removeScheduled(ScheduledRemoval removal) {
|
||||
MantleChunk<Matter> chunk = removal.chunk();
|
||||
if (removal.engine().isClosed() || chunk.isClosed()
|
||||
|| !removal.world().isChunkLoaded(chunk.getX(), chunk.getZ())) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
chunk.use();
|
||||
} catch (MantleClosedException closed) {
|
||||
IrisLogging.reportError("Custom content cleanup deferred after mantle chunk "
|
||||
+ chunk.getX() + "," + chunk.getZ() + " closed.", closed);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
removeInvalidated(removal.engine(), chunk, removal.minHeight());
|
||||
} finally {
|
||||
chunk.release();
|
||||
}
|
||||
}
|
||||
|
||||
private static void removeInvalidated(Engine engine, MantleChunk<Matter> chunk, int minHeight) {
|
||||
EnginePlatformHooks hooks = IrisServices.getOrNull(EnginePlatformHooks.class);
|
||||
if (hooks == null) {
|
||||
return;
|
||||
}
|
||||
int baseX = chunk.getX() << 4;
|
||||
int baseZ = chunk.getZ() << 4;
|
||||
List<Removal> removals = new ArrayList<>();
|
||||
synchronized (chunk) {
|
||||
chunk.iterate(CustomBlockOwnership.class, (x, y, z, ownership) -> {
|
||||
for (CustomBlockOwnership.Entry entry : ownership.entries()) {
|
||||
if (entry.invalidated()) {
|
||||
removals.add(new Removal(new NativeBlockPoint(baseX + (x & 15),
|
||||
y + minHeight, baseZ + (z & 15)), entry));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
for (Removal removal : removals) {
|
||||
try {
|
||||
if (hooks.removeCustomBlock(engine, removal.position(), removal.entry())) {
|
||||
forgetRemoval(chunk, removal, minHeight);
|
||||
}
|
||||
} catch (RuntimeException | LinkageError error) {
|
||||
IrisLogging.reportError("Failed to remove generated custom content at " + removal.position() + ".", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> void collectPositions(MantleChunk<Matter> chunk, NativeBlockChangeSet positions,
|
||||
Class<T> type, int minHeight) {
|
||||
int baseX = chunk.getX() << 4;
|
||||
int baseZ = chunk.getZ() << 4;
|
||||
chunk.iterate(type, (x, y, z, value) -> positions.add(baseX + (x & 15), y + minHeight, baseZ + (z & 15)));
|
||||
}
|
||||
|
||||
private static void forgetRemoval(MantleChunk<Matter> chunk, Removal removal, int minHeight) {
|
||||
NativeBlockPoint position = removal.position();
|
||||
int y = position.y() - minHeight;
|
||||
synchronized (chunk) {
|
||||
Matter section = chunk.get(y >> 4);
|
||||
if (section == null) {
|
||||
return;
|
||||
}
|
||||
MatterSlice<CustomBlockOwnership> slice = section.getSlice(CustomBlockOwnership.class);
|
||||
if (slice == null) {
|
||||
return;
|
||||
}
|
||||
CustomBlockOwnership ownership = slice.get(position.x() & 15, y & 15, position.z() & 15);
|
||||
if (ownership != null) {
|
||||
slice.set(position.x() & 15, y & 15, position.z() & 15, ownership.remove(removal.entry()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean hasRecords(Mantle<Matter> mantle, int chunkX, int chunkZ) {
|
||||
MantleChunk<Matter> chunk = mantle.useChunk(chunkX, chunkZ);
|
||||
try {
|
||||
synchronized (chunk) {
|
||||
return hasRecords(chunk);
|
||||
}
|
||||
} finally {
|
||||
chunk.release();
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean hasRecords(MantleChunk<Matter> chunk) {
|
||||
for (int section = 0; section < chunk.sectionCount(); section++) {
|
||||
Matter matter = chunk.get(section);
|
||||
if (matter != null && (matter.hasSlice(Identifier.class) || matter.hasSlice(TileWrapper.class)
|
||||
|| matter.hasSlice(CustomBlockOwnership.class))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static MantleChunk<Matter> invalidateChunk(Mantle<Matter> mantle, NativeBlockChangeSet changes,
|
||||
int chunkX, int chunkZ, int minHeight) {
|
||||
MantleChunk<Matter> chunk = mantle.useChunk(chunkX, chunkZ);
|
||||
try {
|
||||
synchronized (chunk) {
|
||||
if (!hasRecords(chunk)) {
|
||||
return null;
|
||||
}
|
||||
boolean[] cleanup = new boolean[1];
|
||||
changes.forEachPosition(chunkX, chunkZ, (x, y, z) ->
|
||||
cleanup[0] |= clearRecords(chunk, x & 15, y - minHeight, z & 15));
|
||||
return cleanup[0] ? chunk : null;
|
||||
}
|
||||
} finally {
|
||||
chunk.release();
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean clearRecords(MantleChunk<Matter> chunk, int x, int y, int z) {
|
||||
if (y < 0 || (y >> 4) >= chunk.sectionCount()) {
|
||||
return false;
|
||||
}
|
||||
Matter section = chunk.get(y >> 4);
|
||||
if (section == null) {
|
||||
return false;
|
||||
}
|
||||
clearRecord(section.getSlice(Identifier.class), x, y & 15, z);
|
||||
clearRecord(section.getSlice(TileWrapper.class), x, y & 15, z);
|
||||
MatterSlice<CustomBlockOwnership> ownership = section.getSlice(CustomBlockOwnership.class);
|
||||
if (ownership != null) {
|
||||
CustomBlockOwnership current = ownership.get(x, y & 15, z);
|
||||
if (current != null) {
|
||||
ownership.set(x, y & 15, z, current.invalidate());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void clearRecord(MatterSlice<?> slice, int x, int y, int z) {
|
||||
if (slice != null) {
|
||||
slice.set(x, y, z, null);
|
||||
}
|
||||
}
|
||||
|
||||
private record Removal(NativeBlockPoint position, CustomBlockOwnership.Entry entry) {
|
||||
}
|
||||
|
||||
public record Pending<T>(NativeBlockPoint position, T value) {
|
||||
}
|
||||
|
||||
private record ScheduledRemoval(Engine engine, NativeWorld world, MantleChunk<Matter> chunk, int minHeight) {
|
||||
}
|
||||
}
|
||||
+63
-9
@@ -24,6 +24,7 @@ import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.structure.object.IObjectPlacer;
|
||||
import art.arcane.iris.generation.block.TileData;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.math.BlockPosition;
|
||||
import art.arcane.iris.generation.block.B;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@@ -43,6 +44,7 @@ final class FloatingObjectPlacementTransaction implements IObjectPlacer {
|
||||
private final KList<BufferedMutation> mutations;
|
||||
private final Map<PositionKey, NativeBlockState> bufferedBlocks;
|
||||
private int blockWrites;
|
||||
private BlockPosition canopyPosition;
|
||||
|
||||
FloatingObjectPlacementTransaction(IObjectPlacer delegate, ObjectContinuationBundle.PlacementKey placementKey) {
|
||||
this.delegate = Objects.requireNonNull(delegate);
|
||||
@@ -70,9 +72,7 @@ final class FloatingObjectPlacementTransaction implements IObjectPlacer {
|
||||
return CommitResult.REJECTED_SUPPORT;
|
||||
}
|
||||
}
|
||||
for (BufferedMutation mutation : mutations) {
|
||||
mutation.apply(delegate);
|
||||
}
|
||||
applyMutations();
|
||||
persistContinuation(engine);
|
||||
discard();
|
||||
return CommitResult.COMMITTED;
|
||||
@@ -94,9 +94,17 @@ final class FloatingObjectPlacementTransaction implements IObjectPlacer {
|
||||
}
|
||||
IrisMatterSupport.ensureRegistered();
|
||||
LinkedHashMap<ObjectContinuationBundle.ChunkPosition, Matter> payloads = new LinkedHashMap<>();
|
||||
boolean skipCanopy = false;
|
||||
for (BufferedMutation mutation : mutations) {
|
||||
ObjectContinuationBundle.ChunkPosition chunk = new ObjectContinuationBundle.ChunkPosition(mutation.x() >> 4, mutation.z() >> 4);
|
||||
Matter payload = payloads.computeIfAbsent(chunk, ignored -> new IrisMatter(16, engine.getHeight(), 16));
|
||||
if (mutation.value() instanceof NativeBlockState) {
|
||||
skipCanopy = !ObjectContinuationPersistence.beginBlock(payload, mutation.x() & 15,
|
||||
mutation.y(), mutation.z() & 15, mutation.canopy());
|
||||
}
|
||||
if (mutation.canopy() && skipCanopy) {
|
||||
continue;
|
||||
}
|
||||
ObjectContinuationPersistence.put(payload, mutation.x() & 15, mutation.y(), mutation.z() & 15, mutation.value());
|
||||
}
|
||||
LinkedHashMap<ObjectContinuationBundle.ChunkPosition, Supplier<Matter>> destinations = new LinkedHashMap<>();
|
||||
@@ -105,6 +113,31 @@ final class FloatingObjectPlacementTransaction implements IObjectPlacer {
|
||||
new ObjectContinuationBundle.Bounds(minimumX, minimumZ, maximumX, maximumZ), destinations);
|
||||
}
|
||||
|
||||
private void applyMutations() {
|
||||
boolean canopyOpen = false;
|
||||
boolean skipCanopy = false;
|
||||
try {
|
||||
for (BufferedMutation mutation : mutations) {
|
||||
if (mutation.value() instanceof NativeBlockState) {
|
||||
if (canopyOpen) {
|
||||
delegate.endTreeCanopy();
|
||||
canopyOpen = false;
|
||||
}
|
||||
skipCanopy = mutation.canopy()
|
||||
&& !delegate.beginTreeCanopy(new BlockPosition(mutation.x(), mutation.y(), mutation.z()));
|
||||
canopyOpen = mutation.canopy() && !skipCanopy;
|
||||
}
|
||||
if (!mutation.canopy() || !skipCanopy) {
|
||||
mutation.apply(delegate);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (canopyOpen) {
|
||||
delegate.endTreeCanopy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void discard() {
|
||||
mutations.clear();
|
||||
bufferedBlocks.clear();
|
||||
@@ -126,11 +159,30 @@ final class FloatingObjectPlacementTransaction implements IObjectPlacer {
|
||||
if (state == null) {
|
||||
return;
|
||||
}
|
||||
mutations.add(new BlockMutation(x, y, z, state));
|
||||
mutations.add(new BlockMutation(x, y, z, state, isCanopy(x, y, z)));
|
||||
bufferedBlocks.put(new PositionKey(x, y, z), state);
|
||||
blockWrites++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean beginTreeCanopy(BlockPosition position) {
|
||||
if (!IObjectPlacer.super.beginTreeCanopy(position)) {
|
||||
return false;
|
||||
}
|
||||
canopyPosition = position;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endTreeCanopy() {
|
||||
canopyPosition = null;
|
||||
}
|
||||
|
||||
private boolean isCanopy(int x, int y, int z) {
|
||||
return canopyPosition != null && canopyPosition.getX() == x
|
||||
&& canopyPosition.getY() == y && canopyPosition.getZ() == z;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeBlockState get(int x, int y, int z) {
|
||||
NativeBlockState state = bufferedBlocks.get(new PositionKey(x, y, z));
|
||||
@@ -176,7 +228,7 @@ final class FloatingObjectPlacementTransaction implements IObjectPlacer {
|
||||
@Override
|
||||
public void setTile(int x, int y, int z, TileData tile) {
|
||||
if (tile != null) {
|
||||
mutations.add(new TileMutation(x, y, z, tile));
|
||||
mutations.add(new TileMutation(x, y, z, tile, isCanopy(x, y, z)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +237,7 @@ final class FloatingObjectPlacementTransaction implements IObjectPlacer {
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
mutations.add(new DataMutation(x, y, z, data));
|
||||
mutations.add(new DataMutation(x, y, z, data, isCanopy(x, y, z)));
|
||||
if (data instanceof NativeBlockState state) {
|
||||
bufferedBlocks.put(new PositionKey(x, y, z), state);
|
||||
blockWrites++;
|
||||
@@ -228,24 +280,26 @@ final class FloatingObjectPlacementTransaction implements IObjectPlacer {
|
||||
|
||||
Object value();
|
||||
|
||||
boolean canopy();
|
||||
|
||||
void apply(IObjectPlacer placer);
|
||||
}
|
||||
|
||||
private record BlockMutation(int x, int y, int z, NativeBlockState value) implements BufferedMutation {
|
||||
private record BlockMutation(int x, int y, int z, NativeBlockState value, boolean canopy) implements BufferedMutation {
|
||||
@Override
|
||||
public void apply(IObjectPlacer placer) {
|
||||
placer.set(x, y, z, value);
|
||||
}
|
||||
}
|
||||
|
||||
private record TileMutation(int x, int y, int z, TileData value) implements BufferedMutation {
|
||||
private record TileMutation(int x, int y, int z, TileData value, boolean canopy) implements BufferedMutation {
|
||||
@Override
|
||||
public void apply(IObjectPlacer placer) {
|
||||
placer.setTile(x, y, z, value);
|
||||
}
|
||||
}
|
||||
|
||||
private record DataMutation(int x, int y, int z, Object value) implements BufferedMutation {
|
||||
private record DataMutation(int x, int y, int z, Object value, boolean canopy) implements BufferedMutation {
|
||||
@Override
|
||||
public void apply(IObjectPlacer placer) {
|
||||
placer.setData(x, y, z, value);
|
||||
|
||||
@@ -24,6 +24,7 @@ import art.arcane.iris.structure.object.IObjectPlacer;
|
||||
import art.arcane.iris.generation.block.TileData;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.math.BlockPosition;
|
||||
|
||||
final class GoldenDebugObjectPlacer implements IObjectPlacer {
|
||||
private static final int[] GOLDEN_DEBUG_TARGET = parseGoldenDebugTarget(resolveGoldenDebugSpec());
|
||||
@@ -93,6 +94,16 @@ final class GoldenDebugObjectPlacer implements IObjectPlacer {
|
||||
delegate.set(x, y, z, d);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean beginTreeCanopy(BlockPosition position) {
|
||||
return delegate.beginTreeCanopy(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endTreeCanopy() {
|
||||
delegate.endTreeCanopy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeBlockState get(int x, int y, int z) {
|
||||
return delegate.get(x, y, z);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+92
@@ -1,9 +1,11 @@
|
||||
package art.arcane.iris.generation.mantle;
|
||||
|
||||
import art.arcane.iris.generation.block.TileData;
|
||||
import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial;
|
||||
import art.arcane.iris.integration.Identifier;
|
||||
import art.arcane.iris.world.storage.matter.TileWrapper;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.hunk.Hunk;
|
||||
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
@@ -12,6 +14,7 @@ import art.arcane.volmlib.util.matter.MatterSlice;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.BitSet;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public final class ObjectContinuationPersistence {
|
||||
@@ -68,6 +71,95 @@ public final class ObjectContinuationPersistence {
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean beginBlock(Matter payload, int x, int y, int z, boolean canopy) {
|
||||
if (canopy) {
|
||||
MatterSlice<NativeBlockState> blocks = payload.getSlice(NativeBlockState.class);
|
||||
NativeBlockState existing = blocks == null ? null : blocks.get(x, y, z);
|
||||
if (existing != null && existing.isSolid()) {
|
||||
return false;
|
||||
}
|
||||
payload.slice(TreeCanopy.class).set(x, y, z, TreeCanopy.INSTANCE);
|
||||
} else {
|
||||
MatterSlice<TreeCanopy> conditions = payload.getSlice(TreeCanopy.class);
|
||||
if (conditions != null) {
|
||||
conditions.set(x, y, z, null);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void restore(Mantle<Matter> mantle, int chunkX, int chunkZ, Matter continuation,
|
||||
Hunk<NativeBlockState> blocks, boolean applyBlocks) {
|
||||
BitSet blocked = blockedCanopy(continuation, blocks, applyBlocks);
|
||||
MatterSlice<NativeBlockState> states = continuation.getSlice(NativeBlockState.class);
|
||||
MantleChunk<Matter> chunk = mantle.useChunk(chunkX, chunkZ);
|
||||
try {
|
||||
synchronized (chunk) {
|
||||
if (states != null) {
|
||||
states.iterateSync((x, y, z, state) -> {
|
||||
if (blocked.get(cellIndex(x, y, z))) {
|
||||
return;
|
||||
}
|
||||
Matter section = chunk.get(y >> 4);
|
||||
if (section != null) {
|
||||
clearMetadata(section, x, y & 15, z);
|
||||
}
|
||||
});
|
||||
}
|
||||
for (Map.Entry<Class<?>, MatterSlice<?>> entry : continuation.getSliceMap().entrySet()) {
|
||||
if (entry.getKey() == TreeCanopy.class) {
|
||||
continue;
|
||||
}
|
||||
entry.getValue().iterateSync((x, y, z, value) -> {
|
||||
if (!blocked.get(cellIndex(x, y, z))) {
|
||||
chunk.getOrCreate(y >> 4).slice(entry.getKey()).set(x, y & 15, z, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
chunk.release();
|
||||
}
|
||||
if (applyBlocks && states != null) {
|
||||
states.iterateSync((x, y, z, state) -> {
|
||||
if (!blocked.get(cellIndex(x, y, z))) {
|
||||
blocks.set(x, y, z, state);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static BitSet blockedCanopy(Matter continuation, Hunk<NativeBlockState> blocks, boolean applyBlocks) {
|
||||
BitSet blocked = new BitSet();
|
||||
MatterSlice<TreeCanopy> conditions = continuation.getSlice(TreeCanopy.class);
|
||||
if (conditions != null) {
|
||||
conditions.iterateSync((x, y, z, condition) -> {
|
||||
NativeBlockState existing = blocks.get(x, y, z);
|
||||
if (!applyBlocks || existing != null && existing.isSolid()) {
|
||||
blocked.set(cellIndex(x, y, z));
|
||||
}
|
||||
});
|
||||
}
|
||||
return blocked;
|
||||
}
|
||||
|
||||
private static int cellIndex(int x, int y, int z) {
|
||||
return (y << 8) | (z << 4) | x;
|
||||
}
|
||||
|
||||
private static void clearMetadata(Matter section, int x, int y, int z) {
|
||||
clearMetadata(section.getSlice(Identifier.class), x, y, z);
|
||||
clearMetadata(section.getSlice(String.class), x, y, z);
|
||||
clearMetadata(section.getSlice(TreeBlockMaterial.class), x, y, z);
|
||||
clearMetadata(section.getSlice(TileWrapper.class), x, y, z);
|
||||
}
|
||||
|
||||
private static void clearMetadata(MatterSlice<?> slice, int x, int y, int z) {
|
||||
if (slice != null) {
|
||||
slice.set(x, y, z, null);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean contains(ObjectContinuationBundle bundle, ObjectContinuationBundle.PlacementKey key) {
|
||||
if (bundle == null) {
|
||||
return false;
|
||||
|
||||
+35
-4
@@ -10,6 +10,7 @@ import art.arcane.iris.generation.subterrain.SubterrainRasterizer;
|
||||
import art.arcane.iris.generation.block.TileData;
|
||||
import art.arcane.iris.structure.object.IObjectPlacer;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.math.BlockPosition;
|
||||
import art.arcane.iris.generation.block.B;
|
||||
import art.arcane.iris.world.storage.matter.TileWrapper;
|
||||
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||
@@ -34,6 +35,7 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
private final List<Mutation> mutations;
|
||||
private final List<PlacementRange> placements = new ArrayList<>();
|
||||
private final Long2ObjectOpenHashMap<Int2ObjectOpenHashMap<OverlayCell>> overlay;
|
||||
private BlockPosition canopyPosition;
|
||||
private boolean hydrologyGateKnown;
|
||||
private int hydrologyGateChunkX;
|
||||
private int hydrologyGateChunkZ;
|
||||
@@ -82,7 +84,15 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
}
|
||||
|
||||
void apply(ObjectSourcePlan plan) {
|
||||
boolean skipCanopy = false;
|
||||
for (Mutation mutation : plan.mutationsFor(destinationChunkX, destinationChunkZ)) {
|
||||
if (mutation.key().type() == NativeBlockState.class) {
|
||||
NativeBlockState existing = mutation.canopy() ? get(mutation.key().x(), mutation.key().y(), mutation.key().z()) : null;
|
||||
skipCanopy = existing != null && existing.isSolid();
|
||||
}
|
||||
if (mutation.canopy() && skipCanopy) {
|
||||
continue;
|
||||
}
|
||||
switch (mutation) {
|
||||
case SetMutation set -> setData(set.key().x(), set.key().y(), set.key().z(), set.value(), set);
|
||||
case CustomBlockMutation custom -> set(custom.key().x(), custom.key().y(), custom.key().z(), custom.state(), custom);
|
||||
@@ -158,12 +168,31 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
cell.tile = CLEARED;
|
||||
cell.identifier = identifier;
|
||||
mutations.add(replayed != null
|
||||
? replayed : new CustomBlockMutation(new DataKey(x, y, z, NativeBlockState.class), state));
|
||||
? replayed : new CustomBlockMutation(new DataKey(x, y, z, NativeBlockState.class), state, isCanopy(x, y, z)));
|
||||
return;
|
||||
}
|
||||
setData(x, y, z, state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean beginTreeCanopy(BlockPosition position) {
|
||||
if (!ObjectPassPlacer.super.beginTreeCanopy(position)) {
|
||||
return false;
|
||||
}
|
||||
canopyPosition = position;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endTreeCanopy() {
|
||||
canopyPosition = null;
|
||||
}
|
||||
|
||||
private boolean isCanopy(int x, int y, int z) {
|
||||
return canopyPosition != null && canopyPosition.getX() == x
|
||||
&& canopyPosition.getY() == y && canopyPosition.getZ() == z;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeBlockState get(int x, int y, int z) {
|
||||
OverlayCell cell = overlayCell(x, y, z);
|
||||
@@ -257,7 +286,7 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
cell.put(type, data);
|
||||
}
|
||||
mutations.add(replayed != null
|
||||
? replayed : new SetMutation(new DataKey(x, y, z, type), data));
|
||||
? replayed : new SetMutation(new DataKey(x, y, z, type), data, isCanopy(x, y, z)));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -476,10 +505,12 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
|
||||
DataKey key();
|
||||
|
||||
boolean canopy();
|
||||
|
||||
void apply(IObjectPlacer placer);
|
||||
}
|
||||
|
||||
record SetMutation(DataKey key, Object value) implements Mutation {
|
||||
record SetMutation(DataKey key, Object value, boolean canopy) implements Mutation {
|
||||
@Override
|
||||
public int x() {
|
||||
return key.x();
|
||||
@@ -496,7 +527,7 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
||||
}
|
||||
}
|
||||
|
||||
record CustomBlockMutation(DataKey key, NativeBlockState state) implements Mutation {
|
||||
record CustomBlockMutation(DataKey key, NativeBlockState state, boolean canopy) implements Mutation {
|
||||
@Override
|
||||
public int x() {
|
||||
return key.x();
|
||||
|
||||
@@ -32,7 +32,8 @@ final class ObjectSourcePlan {
|
||||
private static final int PLACEMENT_DESTINATION_BYTES = 40;
|
||||
private static final int UNKNOWN_PAYLOAD_BYTES = 256;
|
||||
private static final int CUSTOM_BIT = 1 << 8;
|
||||
private static final int Y_SHIFT = 9;
|
||||
private static final int CANOPY_BIT = 1 << 9;
|
||||
private static final int Y_SHIFT = 10;
|
||||
private static final int MAXIMUM_Y = (1 << (31 - Y_SHIFT)) - 1;
|
||||
|
||||
private final long[] destinations;
|
||||
@@ -102,7 +103,7 @@ final class ObjectSourcePlan {
|
||||
custom = CUSTOM_BIT;
|
||||
}
|
||||
}
|
||||
cells[position] = (key.y() << Y_SHIFT) | custom | ((key.z() & 15) << 4) | (key.x() & 15);
|
||||
cells[position] = (key.y() << Y_SHIFT) | custom | (mutation.canopy() ? CANOPY_BIT : 0) | ((key.z() & 15) << 4) | (key.x() & 15);
|
||||
valueIndexes[position] = table.indexOf(value);
|
||||
}
|
||||
values = table.values.toArray();
|
||||
@@ -275,9 +276,9 @@ final class ObjectSourcePlan {
|
||||
Object value = values[valueIndexes[start + index]];
|
||||
decoded[index] = (cell & CUSTOM_BIT) != 0
|
||||
? new ObjectDestinationTransaction.CustomBlockMutation(
|
||||
new ObjectDestinationTransaction.DataKey(x, y, z, NativeBlockState.class), (NativeBlockState) value)
|
||||
new ObjectDestinationTransaction.DataKey(x, y, z, NativeBlockState.class), (NativeBlockState) value, (cell & CANOPY_BIT) != 0)
|
||||
: new ObjectDestinationTransaction.SetMutation(
|
||||
new ObjectDestinationTransaction.DataKey(x, y, z, typeOf(value)), value);
|
||||
new ObjectDestinationTransaction.DataKey(x, y, z, typeOf(value)), value, (cell & CANOPY_BIT) != 0);
|
||||
}
|
||||
return List.of(decoded);
|
||||
}
|
||||
@@ -285,10 +286,17 @@ final class ObjectSourcePlan {
|
||||
static byte[] encode(List<ObjectDestinationTransaction.Mutation> mutations, int height) {
|
||||
IrisMatterSupport.ensureRegistered();
|
||||
Matter payload = new IrisMatter(16, height, 16);
|
||||
boolean skipCanopy = false;
|
||||
for (ObjectDestinationTransaction.Mutation mutation : mutations) {
|
||||
ObjectDestinationTransaction.DataKey key = mutation.key();
|
||||
int x = key.x() & 15;
|
||||
int z = key.z() & 15;
|
||||
if (key.type() == NativeBlockState.class) {
|
||||
skipCanopy = !ObjectContinuationPersistence.beginBlock(payload, x, key.y(), z, mutation.canopy());
|
||||
}
|
||||
if (mutation.canopy() && skipCanopy) {
|
||||
continue;
|
||||
}
|
||||
if (mutation instanceof ObjectDestinationTransaction.CustomBlockMutation custom) {
|
||||
payload.slice(NativeBlockState.class).set(x, key.y(), z, custom.state().placementBaseState());
|
||||
payload.slice(Identifier.class).set(x, key.y(), z, Identifier.fromString(custom.state().deferredPlacementKey()));
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package art.arcane.iris.generation.mantle;
|
||||
|
||||
public enum TreeCanopy {
|
||||
INSTANCE
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
@@ -33,6 +34,7 @@ import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.LongSupplier;
|
||||
|
||||
import static art.arcane.iris.generation.runtime.EngineShutdownSequence.appendFailure;
|
||||
import static art.arcane.iris.generation.runtime.EngineShutdownSequence.propagate;
|
||||
@@ -47,9 +49,18 @@ final class EngineBackgroundTasks {
|
||||
|
||||
private final Object backgroundTaskLock = new Object();
|
||||
private final List<TrackedBackgroundTask> backgroundTasks = new ArrayList<>();
|
||||
private final LongSupplier clock;
|
||||
private boolean backgroundTaskAdmission;
|
||||
private int activeDrains;
|
||||
|
||||
EngineBackgroundTasks() {
|
||||
this(System::nanoTime);
|
||||
}
|
||||
|
||||
EngineBackgroundTasks(LongSupplier clock) {
|
||||
this.clock = Objects.requireNonNull(clock);
|
||||
}
|
||||
|
||||
boolean scheduleTrackedTask(Runnable task) {
|
||||
return scheduleTrackedTask(task, false);
|
||||
}
|
||||
@@ -106,7 +117,7 @@ final class EngineBackgroundTasks {
|
||||
}
|
||||
|
||||
BackgroundTaskDrain drainBackgroundTasks(String reason) {
|
||||
long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(BACKGROUND_TASK_TIMEOUT_MILLIS);
|
||||
long deadline = clock.getAsLong() + TimeUnit.MILLISECONDS.toNanos(BACKGROUND_TASK_TIMEOUT_MILLIS);
|
||||
Set<TrackedBackgroundTask> observed = Collections.newSetFromMap(new IdentityHashMap<>());
|
||||
Set<TrackedBackgroundTask> settledAtEntry = Collections.newSetFromMap(new IdentityHashMap<>());
|
||||
List<TrackedBackgroundTask> tasks;
|
||||
@@ -135,7 +146,7 @@ final class EngineBackgroundTasks {
|
||||
return new BackgroundTaskDrain(failure, allTasksComplete());
|
||||
}
|
||||
}
|
||||
if (System.nanoTime() >= deadline) {
|
||||
if (clock.getAsLong() >= deadline) {
|
||||
for (TrackedBackgroundTask task : tasks) {
|
||||
cancelBackgroundTask(task, reason);
|
||||
}
|
||||
@@ -170,7 +181,7 @@ final class EngineBackgroundTasks {
|
||||
long deadline, String reason) {
|
||||
Throwable failure = null;
|
||||
for (TrackedBackgroundTask task : tasks) {
|
||||
long remaining = deadline - System.nanoTime();
|
||||
long remaining = deadline - clock.getAsLong();
|
||||
if (remaining <= 0L) {
|
||||
cancelBackgroundTask(task, reason);
|
||||
failure = appendFailure(failure, new TimeoutException("Timed out waiting for Iris background tasks during " + reason + "."));
|
||||
|
||||
@@ -23,11 +23,9 @@ import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||
import art.arcane.iris.generation.context.ChunkContext;
|
||||
import art.arcane.iris.generation.context.IrisContext;
|
||||
import art.arcane.iris.generation.mantle.EngineMantle;
|
||||
import art.arcane.iris.generation.mantle.ObjectContinuationPersistence;
|
||||
import art.arcane.iris.generation.stage.IrisDimensionStackActuator;
|
||||
import art.arcane.iris.generation.stage.IrisTransitionGeometryActuator;
|
||||
import art.arcane.iris.integration.Identifier;
|
||||
import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial;
|
||||
import art.arcane.iris.world.storage.matter.TileWrapper;
|
||||
import art.arcane.iris.world.history.FloatingBiomeOverlay;
|
||||
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
|
||||
import art.arcane.iris.world.history.SavedTerrainChunk;
|
||||
@@ -36,13 +34,10 @@ import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.documentation.BlockCoordinates;
|
||||
import art.arcane.volmlib.util.hunk.Hunk;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.matter.MatterSlice;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface EngineMode extends Staged {
|
||||
KList<EngineStage> getTerrainStages();
|
||||
@@ -221,7 +216,7 @@ public interface EngineMode extends Staged {
|
||||
recordNaturalTerrain(x, z, blocks, biomes, ctx);
|
||||
IrisDimensionStackActuator.clearHostNaturalMetadata(getEngine(), x, z, biomes.getHeight(), ctx);
|
||||
ctx.beginContent();
|
||||
List<Matter> continuations = restoreObjectContinuations(x, z);
|
||||
List<Matter> continuations = restoreObjectContinuations(x, z, blocks);
|
||||
if (!continuations.isEmpty()) {
|
||||
ctx.setCaveTerrain(null);
|
||||
}
|
||||
@@ -229,16 +224,13 @@ public interface EngineMode extends Staged {
|
||||
i.generate(x, z, blocks, biomes, multicore, ctx);
|
||||
}
|
||||
for (Matter continuation : continuations) {
|
||||
restoreObjectMetadata(x, z, continuation);
|
||||
MatterSlice<NativeBlockState> states = continuation.getSlice(NativeBlockState.class);
|
||||
if (states != null) {
|
||||
states.iterateSync((localX, y, localZ, state) -> blocks.set(localX, y, localZ, state));
|
||||
}
|
||||
ObjectContinuationPersistence.restore(getMantle().getMantle(), x >> 4, z >> 4,
|
||||
continuation, blocks, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<Matter> restoreObjectContinuations(int x, int z) {
|
||||
private List<Matter> restoreObjectContinuations(int x, int z, Hunk<NativeBlockState> blocks) {
|
||||
if (!(getEngine() instanceof IrisEngine irisEngine)) {
|
||||
return List.of();
|
||||
}
|
||||
@@ -257,7 +249,8 @@ public interface EngineMode extends Staged {
|
||||
if (continuation.getHeight() != getEngine().getHeight()) {
|
||||
throw new IOException("Object continuation height does not match the world");
|
||||
}
|
||||
restoreObjectMetadata(x, z, continuation);
|
||||
ObjectContinuationPersistence.restore(getMantle().getMantle(), x >> 4, z >> 4,
|
||||
continuation, blocks, false);
|
||||
}
|
||||
return continuations;
|
||||
} catch (IOException failure) {
|
||||
@@ -265,42 +258,6 @@ public interface EngineMode extends Staged {
|
||||
}
|
||||
}
|
||||
|
||||
private void restoreObjectMetadata(int x, int z, Matter continuation) {
|
||||
MantleChunk<Matter> chunk = getMantle().getMantle().useChunk(x >> 4, z >> 4);
|
||||
try {
|
||||
synchronized (chunk) {
|
||||
MatterSlice<NativeBlockState> states = continuation.getSlice(NativeBlockState.class);
|
||||
if (states != null) {
|
||||
states.iterateSync((localX, y, localZ, state) -> {
|
||||
Matter section = chunk.get(y >> 4);
|
||||
if (section != null) {
|
||||
clearObjectMetadata(section, localX, y & 15, localZ);
|
||||
}
|
||||
});
|
||||
}
|
||||
for (Map.Entry<Class<?>, MatterSlice<?>> entry : continuation.getSliceMap().entrySet()) {
|
||||
entry.getValue().iterateSync((localX, y, localZ, value) ->
|
||||
chunk.getOrCreate(y >> 4).slice(entry.getKey()).set(localX, y & 15, localZ, value));
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
chunk.release();
|
||||
}
|
||||
}
|
||||
|
||||
private static void clearObjectMetadata(Matter section, int x, int y, int z) {
|
||||
clearObjectMetadata(section.getSlice(Identifier.class), x, y, z);
|
||||
clearObjectMetadata(section.getSlice(String.class), x, y, z);
|
||||
clearObjectMetadata(section.getSlice(TreeBlockMaterial.class), x, y, z);
|
||||
clearObjectMetadata(section.getSlice(TileWrapper.class), x, y, z);
|
||||
}
|
||||
|
||||
private static void clearObjectMetadata(MatterSlice<?> slice, int x, int y, int z) {
|
||||
if (slice != null) {
|
||||
slice.set(x, y, z, null);
|
||||
}
|
||||
}
|
||||
|
||||
private void recordNaturalTerrain(int x, int z, Hunk<NativeBlockState> blocks,
|
||||
Hunk<NativeBiome> biomes, ChunkContext context) {
|
||||
if (!(getEngine() instanceof IrisEngine irisEngine)) {
|
||||
|
||||
@@ -21,6 +21,8 @@ package art.arcane.iris.generation.runtime;
|
||||
import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume;
|
||||
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.generation.mantle.CustomBlockOwnership;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockPoint;
|
||||
import art.arcane.iris.world.history.SavedTerrainChunk;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@@ -28,6 +30,10 @@ import art.arcane.iris.generation.mantle.MantleComponent;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
|
||||
public interface EnginePlatformHooks {
|
||||
default boolean removeCustomBlock(Engine engine, NativeBlockPoint position, CustomBlockOwnership.Entry entry) {
|
||||
return false;
|
||||
}
|
||||
|
||||
default CompletableFuture<Void> flushSavedTerrainCapture(Engine engine) {
|
||||
return CompletableFuture.failedFuture(new UnsupportedOperationException(
|
||||
"This platform does not support saved terrain checkpoints."));
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -44,6 +44,7 @@ import org.bukkit.event.block.BlockBreakEvent;
|
||||
import org.bukkit.event.block.BlockDropItemEvent;
|
||||
import org.bukkit.event.block.BlockPlaceEvent;
|
||||
import org.bukkit.event.player.PlayerTeleportEvent;
|
||||
import org.bukkit.event.world.EntitiesLoadEvent;
|
||||
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@@ -249,6 +250,16 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onEntitiesLoad(EntitiesLoadEvent event) {
|
||||
runManagerTask("bukkit_world_manager_entities_load", () -> {
|
||||
if (!event.getEntities().isEmpty()
|
||||
&& event.getWorld().equals(BukkitWorldBinding.world(getEngine().getWorld()))) {
|
||||
chunkMaintenance.onEntitiesLoaded(event.getChunk());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChunkUnload(Chunk e) {
|
||||
final var future = cleanup.remove(Cache.key(e.getX(), e.getZ()));
|
||||
|
||||
@@ -104,6 +104,16 @@ final class WorldChunkMaintenance {
|
||||
}
|
||||
}
|
||||
|
||||
void onEntitiesLoaded(Chunk chunk) {
|
||||
int chunkX = chunk.getX();
|
||||
int chunkZ = chunk.getZ();
|
||||
World world = chunk.getWorld();
|
||||
invalidateMaterialization(chunkX, chunkZ);
|
||||
J.runRegion(world, chunkX, chunkZ,
|
||||
manager.managedTask("bukkit_world_manager_entity_content_cleanup",
|
||||
() -> scheduleChunkUpdate(world, chunkX, chunkZ)), 1);
|
||||
}
|
||||
|
||||
void discoverChunks() {
|
||||
World world = BukkitWorldBinding.world(manager.getEngine().getWorld());
|
||||
if (world == null) {
|
||||
|
||||
@@ -120,7 +120,13 @@ public abstract class ExternalDataProvider {
|
||||
* @param block The block where the block should be placed
|
||||
* @param blockId The blockId to place
|
||||
*/
|
||||
public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {}
|
||||
public String processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean removeGenerated(@NotNull Block block, @NotNull String token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawns a mob in the specified location using the given engine and entity identifier.
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
package art.arcane.iris.integration;
|
||||
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.generation.mantle.CustomBlockOwnership;
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.integration.data.DataType;
|
||||
import art.arcane.volmlib.nativelib.terrain.BlockProperty;
|
||||
@@ -247,13 +248,24 @@ public class ExternalDataSVC implements IrisService {
|
||||
}
|
||||
}
|
||||
|
||||
public void processUpdate(Engine engine, Block block, Identifier blockId) {
|
||||
public CustomBlockOwnership.Entry processUpdate(Engine engine, Block block, Identifier blockId) {
|
||||
Pair<Identifier, KMap<String, String>> state = parseState(blockId);
|
||||
BlockProvider match = findBlockProvider(state.getA());
|
||||
if (match == null) {
|
||||
throw new MissingResourceException("No matching provider found for external block placement.", blockId.namespace(), blockId.key());
|
||||
}
|
||||
match.provider().processUpdate(engine, block, buildState(match.identifier(), state.getB()));
|
||||
String token = match.provider().processUpdate(engine, block, buildState(match.identifier(), state.getB()));
|
||||
return token == null ? null : new CustomBlockOwnership.Entry(
|
||||
qualifyBlockId(match.provider(), buildState(match.identifier(), state.getB())), token, false);
|
||||
}
|
||||
|
||||
public boolean removeGenerated(Block block, CustomBlockOwnership.Entry entry) {
|
||||
for (ExternalDataProvider provider : activeProviders) {
|
||||
if (provider.getPluginId().equalsIgnoreCase(entry.identifier().namespace()) && provider.isReady()) {
|
||||
return provider.removeGenerated(block, entry.token());
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean placeBlock(Block block, Identifier blockId) {
|
||||
|
||||
@@ -18,6 +18,7 @@ import net.momirealms.craftengine.bukkit.api.CraftEngineFurniture;
|
||||
import net.momirealms.craftengine.bukkit.api.CraftEngineItems;
|
||||
import net.momirealms.craftengine.bukkit.api.event.CraftEngineReloadEvent;
|
||||
import net.momirealms.craftengine.bukkit.item.BukkitItemDefinition;
|
||||
import net.momirealms.craftengine.bukkit.entity.furniture.BukkitFurniture;
|
||||
import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine;
|
||||
import net.momirealms.craftengine.core.block.BlockDefinition;
|
||||
import net.momirealms.craftengine.core.block.ImmutableBlockState;
|
||||
@@ -27,9 +28,11 @@ import net.momirealms.craftengine.core.block.property.Property;
|
||||
import net.momirealms.craftengine.core.entity.furniture.FurnitureDefinition;
|
||||
import net.momirealms.craftengine.core.util.Key;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
@@ -43,6 +46,7 @@ import java.util.Map;
|
||||
import java.util.MissingResourceException;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -140,21 +144,38 @@ public class CraftEngineDataProvider extends ExternalDataProvider implements Lis
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
|
||||
public String processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
|
||||
Pair<Identifier, KMap<String, String>> statePair = ExternalDataSVC.parseState(blockId);
|
||||
Identifier baseBlockId = statePair.getA();
|
||||
KMap<String, String> state = statePair.getB();
|
||||
ImmutableBlockState blockState = findBlockState(baseBlockId);
|
||||
if (blockState != null) {
|
||||
placeBlockState(block, applyProperties(baseBlockId, blockState, state), blockId);
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
FurnitureDefinition furniture = requireFurniture(baseBlockId);
|
||||
KMap<String, String> properties = validateFurnitureProperties(baseBlockId, furniture, state);
|
||||
Location location = parseYawAndPitch(engine, block, properties);
|
||||
if (CraftEngineFurniture.place(location, furniture, properties.get("variant"), false) == null) {
|
||||
BukkitFurniture placed = CraftEngineFurniture.place(location, furniture, properties.get("variant"), false);
|
||||
if (placed == null) {
|
||||
throw missing(blockId, "Failed to place CraftEngine furniture.");
|
||||
}
|
||||
return placed.baseEntity().getUniqueId().toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeGenerated(@NotNull Block block, @NotNull String token) {
|
||||
Chunk chunk = block.getChunk();
|
||||
if (!chunk.isEntitiesLoaded()) {
|
||||
return false;
|
||||
}
|
||||
UUID entityId = UUID.fromString(token);
|
||||
for (Entity entity : chunk.getEntities()) {
|
||||
if (entityId.equals(entity.getUniqueId())) {
|
||||
return CraftEngineFurniture.remove(entity, false, false);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -84,10 +84,11 @@ public class ItemAdderDataProvider extends ExternalDataProvider implements Liste
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
|
||||
public String processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
|
||||
if (CustomBlock.place(blockId.toString(), block.getLocation()) == null) {
|
||||
throw new MissingResourceException("Failed to place ItemsAdder block.", blockId.namespace(), blockId.key());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -47,17 +47,18 @@ public class KGeneratorsDataProvider extends ExternalDataProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
|
||||
if (block.getType() != Material.STRUCTURE_VOID) return;
|
||||
public String processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
|
||||
if (block.getType() != Material.STRUCTURE_VOID) return null;
|
||||
IGeneratorLocation existing = KGeneratorsAPI.getLoadedGeneratorLocation(block.getLocation());
|
||||
if (existing != null) return;
|
||||
if (existing != null) return null;
|
||||
block.setBlockData(BukkitBlockResolution.getAir(), false);
|
||||
Generator generator = Main.getGenerators().get(blockId.key());
|
||||
if (generator == null) return;
|
||||
if (generator == null) return null;
|
||||
GeneratorLocation location = new GeneratorLocation(-1, generator, block.getLocation(), Main.getPlacedGenerators().getChunkInfo(block.getChunk()), null, null);
|
||||
Main.getDatabases().getDb().saveGenerator(location);
|
||||
Main.getPlacedGenerators().addLoaded(location);
|
||||
Main.getSchedules().schedule(location, true);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -112,15 +112,15 @@ public class MythicCrucibleDataProvider extends ExternalDataProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
|
||||
public String processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
|
||||
Pair<Identifier, KMap<String, String>> parsedState = ExternalDataSVC.parseState(blockId);
|
||||
KMap<String, String> state = parsedState.getB();
|
||||
blockId = parsedState.getA();
|
||||
|
||||
Optional<CrucibleItem> item = itemManager.getItem(blockId.key());
|
||||
if (item.isEmpty()) return;
|
||||
if (item.isEmpty()) return null;
|
||||
FurnitureItemContext furniture = item.get().getFurnitureData();
|
||||
if (furniture == null) return;
|
||||
if (furniture == null) return null;
|
||||
|
||||
Pair<Float, BlockFace> pair = parseYawAndFace(engine, block, state);
|
||||
BiomeColor type = null;
|
||||
@@ -130,10 +130,11 @@ public class MythicCrucibleDataProvider extends ExternalDataProvider {
|
||||
} catch (NullPointerException | IllegalArgumentException ignored) {}
|
||||
if (type != null) {
|
||||
Color biomeColor = INMS.get().getBiomeColor(block.getLocation(), type);
|
||||
if (biomeColor == null) return;
|
||||
if (biomeColor == null) return null;
|
||||
color = Chroma.of(biomeColor.getRGB());
|
||||
}
|
||||
furniture.place(block, pair.getB(), pair.getA(), color);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -89,24 +89,24 @@ public class NexoDataProvider extends ExternalDataProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
|
||||
public String processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
|
||||
Pair<Identifier, KMap<String, String>> statePair = ExternalDataSVC.parseState(blockId);
|
||||
KMap<String, String> state = statePair.getB();
|
||||
blockId = statePair.getA();
|
||||
|
||||
if (NexoBlocks.isCustomBlock(blockId.key())) {
|
||||
NexoBlocks.place(blockId.key(), block.getLocation());
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!NexoFurniture.isFurniture(blockId.key()))
|
||||
return;
|
||||
return null;
|
||||
|
||||
Pair<Float, BlockFace> pair = parseYawAndFace(engine, block, state);
|
||||
ItemDisplay display = NexoFurniture.place(blockId.key(), block.getLocation(), pair.getA(), pair.getB());
|
||||
if (display == null) return;
|
||||
if (display == null) return null;
|
||||
ItemStack itemStack = display.getItemStack();
|
||||
if (itemStack == null) return;
|
||||
if (itemStack == null) return null;
|
||||
|
||||
BiomeColor type = null;
|
||||
try {
|
||||
@@ -115,7 +115,7 @@ public class NexoDataProvider extends ExternalDataProvider {
|
||||
|
||||
if (type != null) {
|
||||
Color biomeColor = INMS.get().getBiomeColor(block.getLocation(), type);
|
||||
if (biomeColor == null) return;
|
||||
if (biomeColor == null) return null;
|
||||
ItemMeta meta = itemStack.getItemMeta();
|
||||
switch (meta) {
|
||||
case LeatherArmorMeta armor -> armor.setColor(fromARGB(biomeColor.getAlpha(), biomeColor.getRed(), biomeColor.getGreen(), biomeColor.getBlue()));
|
||||
@@ -126,6 +126,7 @@ public class NexoDataProvider extends ExternalDataProvider {
|
||||
itemStack.setItemMeta(meta);
|
||||
}
|
||||
display.setItemStack(itemStack);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -61,8 +61,9 @@ public final class OraxenDataProvider extends ExternalDataProvider implements Li
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
|
||||
public String processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
|
||||
OraxenBlocks.place(blockId.key(), block.getLocation());
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -45,6 +45,9 @@ import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KSet;
|
||||
import art.arcane.volmlib.util.io.AtomicFileIO;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonParseException;
|
||||
import art.arcane.iris.platform.bootstrap.ServerProperties;
|
||||
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||
import art.arcane.iris.world.task.J;
|
||||
@@ -104,6 +107,12 @@ public class ServerConfigurator {
|
||||
if (failure != null) {
|
||||
IrisLogging.reportError("Unable to persist the Spigot timeout setting.", failure);
|
||||
}
|
||||
} else {
|
||||
Throwable failure = J.attemptCatch(() -> timeoutConfiguration().restore(
|
||||
"settings.timeout-time", serverConfigurationOption("spigot-settings"), Bukkit.spigot().getConfig()));
|
||||
if (failure != null) {
|
||||
IrisLogging.reportError("Unable to restore the Spigot timeout setting.", failure);
|
||||
}
|
||||
}
|
||||
|
||||
if (s.isConfigurePaperWatchdogDelay() && BukkitPlatform.isPaperServer()) {
|
||||
@@ -111,6 +120,12 @@ public class ServerConfigurator {
|
||||
if (failure != null) {
|
||||
IrisLogging.reportError("Unable to persist the Paper watchdog setting.", failure);
|
||||
}
|
||||
} else if (!s.isConfigurePaperWatchdogDelay()) {
|
||||
Throwable failure = J.attemptCatch(() -> timeoutConfiguration().restore(
|
||||
"watchdog.early-warning-delay", null, null));
|
||||
if (failure != null) {
|
||||
IrisLogging.reportError("Unable to restore the Paper watchdog setting.", failure);
|
||||
}
|
||||
}
|
||||
|
||||
if (DefaultPackBootstrapProvisioner.wasProvisionedThisStartup()) {
|
||||
@@ -238,26 +253,26 @@ public class ServerConfigurator {
|
||||
}
|
||||
|
||||
private static void increaseKeepAliveSpigot() throws IOException, ReflectiveOperationException {
|
||||
increaseKeepAliveSpigot(serverConfigurationOption("spigot-settings"), Bukkit.spigot().getConfig());
|
||||
increaseKeepAliveSpigot(serverConfigurationOption("spigot-settings"), Bukkit.spigot().getConfig(), timeoutConfiguration());
|
||||
}
|
||||
|
||||
static void increaseKeepAliveSpigot(Path file, YamlConfiguration configuration) throws IOException {
|
||||
static void increaseKeepAliveSpigot(Path file, YamlConfiguration configuration, ServerTimeoutConfiguration timeouts) throws IOException {
|
||||
long previous = configuration.getLong("settings.timeout-time");
|
||||
long minimum = TimeUnit.MINUTES.toSeconds(5);
|
||||
if (previous >= minimum) {
|
||||
return;
|
||||
}
|
||||
persistTimeout(file, configuration, "settings.timeout-time", minimum);
|
||||
timeouts.raise(file, configuration, "settings.timeout-time", minimum);
|
||||
IrisLogging.warn("Updated " + file + " settings.timeout-time: " + previous + " -> " + minimum
|
||||
+ " seconds. Takes effect after a server restart.");
|
||||
IrisLogging.warn("Disable autoConfiguration.configureSpigotTimeoutTime in iris.json to manage this setting yourself.");
|
||||
}
|
||||
|
||||
private static void increasePaperWatchdog() throws IOException, InvalidConfigurationException, ReflectiveOperationException {
|
||||
increasePaperWatchdog(serverConfigurationOption("paper-settings-directory").resolve("paper-global.yml"));
|
||||
increasePaperWatchdog(serverConfigurationOption("paper-settings-directory").resolve("paper-global.yml"), timeoutConfiguration());
|
||||
}
|
||||
|
||||
static void increasePaperWatchdog(Path file) throws IOException, InvalidConfigurationException {
|
||||
static void increasePaperWatchdog(Path file, ServerTimeoutConfiguration timeouts) throws IOException, InvalidConfigurationException {
|
||||
YamlConfiguration configuration = new YamlConfiguration();
|
||||
configuration.load(file.toFile());
|
||||
long previous = configuration.getLong("watchdog.early-warning-delay");
|
||||
@@ -265,22 +280,14 @@ public class ServerConfigurator {
|
||||
if (previous >= minimum) {
|
||||
return;
|
||||
}
|
||||
persistTimeout(file, configuration, "watchdog.early-warning-delay", minimum);
|
||||
timeouts.raise(file, configuration, "watchdog.early-warning-delay", minimum);
|
||||
IrisLogging.warn("Updated " + file + " watchdog.early-warning-delay: " + previous + " -> " + minimum
|
||||
+ " milliseconds. Takes effect after a server restart.");
|
||||
IrisLogging.warn("Disable autoConfiguration.configurePaperWatchdogDelay in iris.json to manage this setting yourself.");
|
||||
}
|
||||
|
||||
private static void persistTimeout(Path file, YamlConfiguration configuration, String key, long minimum) throws IOException {
|
||||
Path target = file.toRealPath();
|
||||
Object previous = configuration.isSet(key) ? configuration.get(key) : null;
|
||||
configuration.set(key, minimum);
|
||||
try {
|
||||
AtomicFileIO.writeString(target, configuration.saveToString());
|
||||
} catch (IOException | RuntimeException failure) {
|
||||
configuration.set(key, previous);
|
||||
throw failure;
|
||||
}
|
||||
private static ServerTimeoutConfiguration timeoutConfiguration() {
|
||||
return new ServerTimeoutConfiguration(IrisPlatforms.get().dataFolder().toPath().resolve("server-timeouts.json"));
|
||||
}
|
||||
|
||||
static Path serverConfigurationOption(String name) throws ReflectiveOperationException, IOException {
|
||||
@@ -964,4 +971,131 @@ public class ServerConfigurator {
|
||||
}
|
||||
}
|
||||
|
||||
static final class ServerTimeoutConfiguration {
|
||||
private static final Object MUTATION_LOCK = new Object();
|
||||
private static final Gson JSON = new GsonBuilder().setPrettyPrinting().serializeNulls().create();
|
||||
|
||||
private final Path recoveryFile;
|
||||
|
||||
ServerTimeoutConfiguration(Path recoveryFile) {
|
||||
this.recoveryFile = Objects.requireNonNull(recoveryFile);
|
||||
}
|
||||
|
||||
void raise(Path file, YamlConfiguration configuration, String key, long minimum) throws IOException {
|
||||
synchronized (MUTATION_LOCK) {
|
||||
Path target = file.toRealPath();
|
||||
YamlConfiguration persisted = load(target);
|
||||
Long original = value(persisted, key);
|
||||
List<SavedTimeout> changes = readChanges();
|
||||
SavedTimeout saved = find(changes, target, key);
|
||||
if (saved == null || !Objects.equals(original, saved.original()) && !Objects.equals(original, saved.applied())) {
|
||||
changes.remove(saved);
|
||||
changes.add(new SavedTimeout(target.toString(), key, original, minimum));
|
||||
writeChanges(changes);
|
||||
}
|
||||
Object previous = configuration.isSet(key) ? configuration.get(key) : null;
|
||||
configuration.set(key, minimum);
|
||||
try {
|
||||
AtomicFileIO.writeString(target, configuration.saveToString());
|
||||
} catch (IOException | RuntimeException failure) {
|
||||
configuration.set(key, previous);
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void restore(String key, Path liveFile, YamlConfiguration live) throws IOException {
|
||||
synchronized (MUTATION_LOCK) {
|
||||
List<SavedTimeout> changes = readChanges();
|
||||
for (SavedTimeout saved : List.copyOf(changes)) {
|
||||
if (!saved.key().equals(key)) {
|
||||
continue;
|
||||
}
|
||||
Path target = Path.of(saved.file());
|
||||
YamlConfiguration configuration = load(target);
|
||||
if (Objects.equals(value(configuration, key), saved.applied())) {
|
||||
configuration.set(key, saved.original());
|
||||
AtomicFileIO.writeString(target, configuration.saveToString());
|
||||
if (live != null && target.equals(liveFile.toRealPath())
|
||||
&& Objects.equals(value(live, key), saved.applied())) {
|
||||
live.set(key, saved.original());
|
||||
}
|
||||
IrisLogging.info("Restored " + target + " " + key + " to "
|
||||
+ (saved.original() == null ? "its server default" : saved.original())
|
||||
+ ". Takes effect after a server restart.");
|
||||
} else {
|
||||
IrisLogging.info("Kept the current " + target + " " + key
|
||||
+ " because it differs from the value saved by Iris.");
|
||||
}
|
||||
changes.remove(saved);
|
||||
writeChanges(changes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<SavedTimeout> readChanges() throws IOException {
|
||||
if (!Files.exists(recoveryFile)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
try {
|
||||
SavedTimeouts saved = JSON.fromJson(Files.readString(recoveryFile), SavedTimeouts.class);
|
||||
if (saved == null) {
|
||||
throw new IOException("Empty server timeout recovery data: " + recoveryFile);
|
||||
}
|
||||
return new ArrayList<>(saved.changes());
|
||||
} catch (JsonParseException | IllegalArgumentException exception) {
|
||||
throw new IOException("Invalid server timeout recovery data: " + recoveryFile, exception);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeChanges(List<SavedTimeout> changes) throws IOException {
|
||||
AtomicFileIO.writeString(recoveryFile, JSON.toJson(new SavedTimeouts(changes)));
|
||||
}
|
||||
|
||||
private static SavedTimeout find(List<SavedTimeout> changes, Path target, String key) {
|
||||
for (SavedTimeout change : changes) {
|
||||
if (change.file().equals(target.toString()) && change.key().equals(key)) {
|
||||
return change;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static YamlConfiguration load(Path file) throws IOException {
|
||||
YamlConfiguration configuration = new YamlConfiguration();
|
||||
try {
|
||||
configuration.load(file.toFile());
|
||||
} catch (InvalidConfigurationException exception) {
|
||||
throw new IOException("Invalid server configuration: " + file, exception);
|
||||
}
|
||||
return configuration;
|
||||
}
|
||||
|
||||
private static Long value(YamlConfiguration configuration, String key) throws IOException {
|
||||
if (!configuration.isSet(key)) {
|
||||
return null;
|
||||
}
|
||||
if (!configuration.isInt(key) && !configuration.isLong(key)) {
|
||||
throw new IOException("Server timeout " + key + " must be an integer.");
|
||||
}
|
||||
return configuration.getLong(key);
|
||||
}
|
||||
|
||||
private record SavedTimeout(String file, String key, Long original, long applied) {
|
||||
private SavedTimeout {
|
||||
Objects.requireNonNull(file, "file");
|
||||
Objects.requireNonNull(key, "key");
|
||||
if (!Path.of(file).isAbsolute() || applied <= 0
|
||||
|| !key.equals("settings.timeout-time") && !key.equals("watchdog.early-warning-delay")) {
|
||||
throw new IllegalArgumentException("Invalid saved server timeout");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record SavedTimeouts(List<SavedTimeout> changes) {
|
||||
private SavedTimeouts {
|
||||
changes = List.copyOf(changes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
@@ -4,6 +4,7 @@ import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.volmlib.nativelib.terrain.structure.StructureCachePolicy;
|
||||
import art.arcane.volmlib.nativelib.terrain.structure.StructureStagePolicy;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.mantle.DeferredPlacementRecords;
|
||||
import art.arcane.iris.generation.terrain.IrisMaterialPalette;
|
||||
import art.arcane.iris.structure.nativegen.GenerationWritePolicy;
|
||||
import art.arcane.iris.structure.nativegen.IrisStructurePolicy;
|
||||
@@ -13,6 +14,7 @@ import art.arcane.iris.structure.nativegen.NativeStructureStartPlan;
|
||||
import art.arcane.iris.structure.nativegen.NativeStructureVolumeIndex;
|
||||
import art.arcane.iris.structure.nativegen.StructureLocateSearch;
|
||||
import art.arcane.iris.structure.placement.IrisStructureLocator;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockChangeSet;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockPositionPredicate;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.nativelib.terrain.structure.StructureLocateSearchAccess;
|
||||
@@ -103,6 +105,16 @@ public final class BukkitStructureStagePolicy implements StructureStagePolicy<
|
||||
context.getDimensionStackContext(), context.getMinHeight());
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeBlockChangeSet trackBlockChanges(Engine context, NativeBlockChangeSet.Bounds bounds) {
|
||||
return DeferredPlacementRecords.track(context, bounds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidateBlockChanges(Engine context, NativeBlockChangeSet changes) {
|
||||
DeferredPlacementRecords.invalidate(context, changes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IntBinaryOperator surfaceHeight(Engine context) {
|
||||
return (x, z) -> Engine.hostHeight(context, x, z, true) + context.getMinHeight();
|
||||
|
||||
@@ -23,6 +23,9 @@ import art.arcane.iris.world.history.GenerationFindCatalog;
|
||||
import art.arcane.iris.world.event.IrisLootEvent;
|
||||
import art.arcane.iris.integration.Identifier;
|
||||
import art.arcane.iris.integration.ExternalDataSVC;
|
||||
import art.arcane.iris.generation.mantle.CustomBlockOwnership;
|
||||
import art.arcane.iris.generation.mantle.DeferredPlacementRecords;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockPoint;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.GenerationSessionLease;
|
||||
import art.arcane.iris.generation.locator.Locator;
|
||||
@@ -36,6 +39,7 @@ import art.arcane.iris.world.loot.InventorySlotType;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.world.loot.IrisLootTable;
|
||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||
import art.arcane.iris.generation.block.IrisCustomData;
|
||||
import art.arcane.iris.generation.block.TileData;
|
||||
import art.arcane.iris.platform.bukkit.BukkitBlockResolution;
|
||||
import art.arcane.iris.platform.bukkit.BukkitBlockState;
|
||||
@@ -83,7 +87,10 @@ import org.bukkit.loot.Lootable;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@@ -133,48 +140,73 @@ public final class EngineBukkitOps {
|
||||
Runnable tileTask = () -> materializeTiles(engine, c, chunk);
|
||||
Runnable customTask = () -> materializeCustomBlocks(engine, c, chunk);
|
||||
Runnable updateTask = () -> materializeUpdates(engine, c, chunk);
|
||||
Runnable materialize = () -> {
|
||||
DeferredPlacementRecords.removeInvalidated(engine, mantle, c.getX(), c.getZ());
|
||||
runMaterializationPasses(chunk, tileTask, customTask, updateTask);
|
||||
};
|
||||
|
||||
if (shouldRunChunkUpdateInline(c)) {
|
||||
runMaterializationPasses(chunk, tileTask, customTask, updateTask);
|
||||
materialize.run();
|
||||
return;
|
||||
}
|
||||
|
||||
runScheduledMaterializationPasses(c, () -> runMaterializationPasses(chunk, tileTask, customTask, updateTask));
|
||||
runScheduledMaterializationPasses(c, materialize);
|
||||
} finally {
|
||||
chunk.release();
|
||||
}
|
||||
}
|
||||
|
||||
static void materializeTiles(Engine engine, Chunk chunk, MantleChunk<Matter> mantleChunk) {
|
||||
mantleChunk.iterate(TileWrapper.class, (x, y, z, value) -> {
|
||||
int worldY = y + engine.getWorld().minHeight();
|
||||
StaleRecords stale = new StaleRecords();
|
||||
for (DeferredPlacementRecords.Pending<TileWrapper> record : DeferredPlacementRecords.snapshot(mantleChunk, TileWrapper.class)) {
|
||||
if (!DeferredPlacementRecords.isPending(mantleChunk, TileWrapper.class, record)) {
|
||||
continue;
|
||||
}
|
||||
NativeBlockPoint position = record.position();
|
||||
TileWrapper value = record.value();
|
||||
int worldY = position.y() + engine.getWorld().minHeight();
|
||||
if (worldY < chunk.getWorld().getMinHeight() || worldY >= chunk.getWorld().getMaxHeight()) {
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
Block block = chunk.getBlock(position.x() & 15, worldY, position.z() & 15);
|
||||
if (stale.tileReplaced(block, value.getData())) {
|
||||
continue;
|
||||
}
|
||||
Block block = chunk.getBlock(x & 15, worldY, z & 15);
|
||||
if (!TileData.setTileState(block, value.getData())) {
|
||||
NamespacedKey blockTypeKey = KeyedType.getKey(block.getType());
|
||||
String blockType = blockTypeKey == null ? block.getType().name() : blockTypeKey.toString();
|
||||
String tileType = value.getData().getMaterialKey();
|
||||
IrisLogging.warn("Failed to set tile entity data at [%d %d %d | %s] for tile %s!", block.getX(), block.getY(), block.getZ(), blockType, tileType);
|
||||
}
|
||||
});
|
||||
}
|
||||
mantleChunk.deleteSlices(TileWrapper.class);
|
||||
stale.report("tile", chunk);
|
||||
}
|
||||
|
||||
static void materializeCustomBlocks(Engine engine, Chunk chunk, MantleChunk<Matter> mantleChunk) {
|
||||
mantleChunk.iterate(Identifier.class, (x, y, z, value) -> {
|
||||
int worldY = y + engine.getWorld().minHeight();
|
||||
if (worldY < chunk.getWorld().getMinHeight() || worldY >= chunk.getWorld().getMaxHeight()) {
|
||||
return;
|
||||
StaleRecords stale = new StaleRecords();
|
||||
for (DeferredPlacementRecords.Pending<Identifier> record : DeferredPlacementRecords.snapshot(mantleChunk, Identifier.class)) {
|
||||
if (!DeferredPlacementRecords.isPending(mantleChunk, Identifier.class, record)) {
|
||||
continue;
|
||||
}
|
||||
IrisServices.get(ExternalDataSVC.class).processUpdate(
|
||||
engine,
|
||||
chunk.getBlock(x & 15, worldY, z & 15),
|
||||
value
|
||||
);
|
||||
});
|
||||
NativeBlockPoint position = record.position();
|
||||
Identifier value = record.value();
|
||||
int worldY = position.y() + engine.getWorld().minHeight();
|
||||
if (worldY < chunk.getWorld().getMinHeight() || worldY >= chunk.getWorld().getMaxHeight()) {
|
||||
continue;
|
||||
}
|
||||
ExternalDataSVC service = IrisServices.get(ExternalDataSVC.class);
|
||||
Block block = chunk.getBlock(position.x() & 15, worldY, position.z() & 15);
|
||||
if (stale.carrierReplaced(service, block, value)) {
|
||||
continue;
|
||||
}
|
||||
CustomBlockOwnership.Entry ownership = service.processUpdate(engine, block, value);
|
||||
if (ownership != null) {
|
||||
DeferredPlacementRecords.remember(engine, mantleChunk, position, ownership);
|
||||
}
|
||||
}
|
||||
mantleChunk.deleteSlices(Identifier.class);
|
||||
stale.report("custom block", chunk);
|
||||
}
|
||||
|
||||
static void materializeUpdates(Engine engine, Chunk chunk, MantleChunk<Matter> mantleChunk) {
|
||||
@@ -855,4 +887,50 @@ public final class EngineBukkitOps {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class StaleRecords {
|
||||
private final Map<Identifier, Material> carriers = new HashMap<>();
|
||||
private int skipped;
|
||||
|
||||
private boolean carrierReplaced(ExternalDataSVC service, Block block, Identifier record) {
|
||||
Material carrier = carriers.get(record);
|
||||
if (carrier == null) {
|
||||
carrier = carrierType(service, record);
|
||||
carriers.put(record, carrier);
|
||||
}
|
||||
Material live = block.getType();
|
||||
if (carrier == Material.AIR || live == carrier
|
||||
|| ((carrier == Material.FIRE || carrier == Material.SOUL_FIRE)
|
||||
&& (live == Material.AIR || live == Material.CAVE_AIR || live == Material.VOID_AIR))) {
|
||||
return false;
|
||||
}
|
||||
skipped++;
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean tileReplaced(Block block, TileData tile) {
|
||||
if (tile.resolveMaterial() == null || tile.isApplicable(block.getBlockData())) {
|
||||
return false;
|
||||
}
|
||||
skipped++;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void report(String records, Chunk chunk) {
|
||||
if (skipped == 0) {
|
||||
return;
|
||||
}
|
||||
IrisLogging.debug("Skipped %d stale %s records at chunk %d,%d in %s",
|
||||
skipped, records, chunk.getX(), chunk.getZ(), chunk.getWorld().getName());
|
||||
}
|
||||
|
||||
private static Material carrierType(ExternalDataSVC service, Identifier record) {
|
||||
Optional<BlockData> resolved = service.getBlockData(record);
|
||||
if (resolved.isEmpty()) {
|
||||
return Material.AIR;
|
||||
}
|
||||
BlockData data = resolved.get();
|
||||
Material type = (data instanceof IrisCustomData custom ? custom.getBase() : data).getMaterial();
|
||||
return type == Material.CAVE_AIR || type == Material.VOID_AIR ? Material.AIR : type;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
package art.arcane.iris.structure.nativegen;
|
||||
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockPositionPredicate;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockChangeSet;
|
||||
|
||||
import art.arcane.iris.generation.mantle.DeferredPlacementRecords;
|
||||
import art.arcane.iris.generation.runtime.DimensionStackContext;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
@@ -48,6 +50,16 @@ public class ImportedFeaturePolicy implements NativeImportedFeaturePolicy<IrisDi
|
||||
return NativeFeatureGenerationPolicy.control(engine);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeBlockChangeSet trackedBlockPositions(NativeBlockChangeSet.Bounds bounds) {
|
||||
return DeferredPlacementRecords.positions(engine, bounds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidateBlockChanges(NativeBlockChangeSet changes) {
|
||||
DeferredPlacementRecords.invalidate(engine, changes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> visibleBiomeKeys() {
|
||||
Set<String> keys = new LinkedHashSet<>();
|
||||
|
||||
Loaded 100 of 149 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user