Compare commits

...
Author SHA1 Message Date
Brian Neumann-Fopiano c3359e0a16 Fixing a handfull of issues! 2026-10-21 18:07:10 -04:00
Brian Neumann-Fopiano 1132242c58 Reduced verification allocations
- Dependency hashing reuses one read buffer per artifact
- Hash tests cover large, short, and empty entries
2026-10-21 09:08:00 -04:00
Brian Neumann-Fopiano 43cb336b85 Reviewed verification allocations
- Dependency hashing reuses one read buffer per artifact
- Hash tests cover mixed entry sizes
2026-10-21 09:06:24 -04:00
Brian Neumann-Fopiano dc1c90f63f Reduced verification allocations
- Dependency hashing reuses one read buffer per artifact
- Hash tests cover large, short, and empty entries
2026-10-21 09:06:24 -04:00
Brian Neumann-Fopiano 463feea60b Improved Iris build scheduling
- Resolved SlimJar only for shaded core artifacts
- Applied bounded parallel Forge test execution
2026-10-21 08:27:49 -04:00
Brian Neumann-Fopiano 49f7d11442 Reviewed Iris build scheduling
- Combined dependency task wiring and Forge test limits
2026-10-21 08:25:11 -04:00
Brian Neumann-Fopiano 0eb661f14d Trimmed unused build work
- Limited SlimJar generation to shaded core artifacts
- Honored configured mod test fork limits
- Set Forge targets to four test JVMs
2026-10-21 08:24:55 -04:00
Brian Neumann-Fopiano 5e02c5efdd Forwarded nested dependency settings
- Nested mod builds inherit the Paperweight source generation property
2026-10-21 08:00:04 -04:00
Brian Neumann-Fopiano 8dea0ae3e7 Reviewed dependency source forwarding
- Mod builds retain source generation settings across nested Gradle invocations
2026-10-21 07:58:12 -04:00
Brian Neumann-Fopiano b669e04acf Forwarded dependency source settings
- Nested mod builds inherit the Paperweight source generation property
2026-10-21 07:57:41 -04:00
Brian Neumann-Fopiano 294598d5e1 Improved isolated Forge builds
- Mod verification uses its included core build without duplicate prerequisites
- Nested sandbox builds divide their reserved workers across matrix lanes
- Forge targets reserve eight workers and 16 GiB with two test forks
2026-10-21 07:18:03 -04:00
Brian Neumann-Fopiano 79f5d02a7d Reviewed Forge build budgets
- Divided nested build workers across sandbox matrix lanes
- Reserved eight workers and 16 GiB for Forge verification
2026-10-21 07:11:10 -04:00
Brian Neumann-Fopiano 826a752437 Expanded Forge build budgets
- Sandbox nested builds divide reserved workers across matrix lanes
- Sandbox nested JVM processor counts follow each lane worker budget
- Forge targets reserve eight workers and 16 GiB with two test forks
2026-10-21 07:08:26 -04:00
Brian Neumann-Fopiano 389c2c9c42 Reviewed modded prerequisites
- Modded targets build core jars and fixtures in their own included build
2026-10-21 06:46:34 -04:00
Brian Neumann-Fopiano e1ff5447ca Removed duplicate core prerequisite
- Modded builds resolve core jars and test fixtures through their included build
- Forge setup starts without waiting for unused outer core outputs
2026-10-21 06:43:07 -04:00
Brian Neumann-Fopiano 96903e9cf3 Expanded Iris test parallelism
- Bukkit builds allocate eight workers for the complete core test suite
- Bukkit builds reserve 16 GiB for concurrent test JVMs
2026-10-21 06:34:44 -04:00
Brian Neumann-Fopiano e68c93c0f1 Improved isolated Iris builds
- Bukkit builds allocate four workers and 8 GiB for tests
- Forge sandbox builds retain their private Minecraft dependency cache
2026-10-21 06:22:45 -04:00
Brian Neumann-Fopiano 97da131884 Reviewed Forge cache persistence
- Isolated Forge targets retain generated Minecraft dependencies in their private Gradle homes
2026-10-21 06:22:45 -04:00
Brian Neumann-Fopiano 20ff08aeb7 Persisted Forge build caches
- Sandboxed Forge lanes retain Mavenizer and ForgeGradle caches in their private Gradle home
- Nested mod builds receive the sandbox property
2026-10-21 06:22:20 -04:00
Brian Neumann-Fopiano 9add1a0fa6 Reviewed Bukkit build allocation
- Bukkit build targets reserve four workers and 8 GiB for the complete test suite
2026-10-21 06:18:41 -04:00
Brian Neumann-Fopiano 1b80f4055b Raised Bukkit build budget
- Bukkit source builds use four Gradle workers
- Bukkit source builds reserve 8 GiB of memory
2026-10-21 06:14:01 -04:00
Brian Neumann-Fopiano 365d6f2810 Merged project build improvements
- Reviewed build controls and validation changes are integrated locally
2026-10-21 05:33:21 -04:00
Brian Neumann-Fopiano c17c8bece1 Reviewed sandbox source stamps
- Source stamp changes are integrated for local review
2026-10-21 05:32:24 -04:00
Brian Neumann-Fopiano d84050baac Stabilized sandbox source stamps
- Build constants use the captured Iris source fingerprint
- Unrelated project changes preserve compiled Iris cache inputs
2026-10-21 05:32:24 -04:00
Brian Neumann-Fopiano 7fb684faf5 Merged sandbox build targets
- Gradle exposes Bukkit, API, SPI, and loader targets with isolated build requirements
- Build tasks retain verified artifacts inside their project directories
- API generation writes a jar without publishing or external copies
- Captured source hashes identify sandbox builds
- Build instructions describe manager deployment
2026-10-21 03:41:57 -04:00
Brian Neumann-Fopiano 1342fa6a06 Reviewed sandbox build targets
- Gradle exposes Bukkit, API, SPI, and loader targets with isolated build requirements
- Build tasks retain verified artifacts inside their project directories
- API generation writes a jar without publishing or external copies
- Captured source hashes identify sandbox builds
- Build instructions describe manager deployment
2026-10-21 03:41:57 -04:00
Brian Neumann-Fopiano 52f1a18a1b Declared sandbox build targets
- Gradle exposes Bukkit, API, SPI, and loader targets with isolated build requirements
- Build tasks retain verified artifacts inside their project directories
- API generation writes a jar without publishing or external copies
- Captured source hashes identify sandbox builds
- Build instructions describe manager deployment
2026-10-21 03:41:18 -04:00
Brian Neumann-Fopiano fc810221bb Parallelized mod builds
- Mod targets run through bounded Gradle workers with separate caches and daemon slots
- Target logs retain verification failures and bounded output summaries
- Nested tests use bounded forks and JVM thread counts
- Lifecycle tests control deadlines through injected clocks
2026-10-21 02:53:00 -04:00
Brian Neumann-Fopiano d7059d84a4 V+ 2026-10-20 17:22:54 -04:00
Brian Neumann-Fopiano b357eee181 Fixed runtime configuration
- Timeout overrides retain original values for restoration
- Timeout restoration preserves operator changes and symbolic links
- Tests cover timeout persistence and failed configuration writes
- Runtime targets use separate Gradle caches and shared module outputs
- Modded build inputs include test fixtures and runtime cache settings
2026-10-18 18:15:46 -04:00
Brian Neumann-Fopiano 64f7a7a9ac Merged generation fixes
- Procedural trees keep compact terrain-supported roots
- Canopies preserve trunks across chunk generation and replay
- CraftEngine content follows native terrain replacement and regeneration
- Regression fixtures cover tree geometry and custom content cleanup
2026-10-18 18:04:40 -04:00
Brian Neumann-Fopiano a6aeceff86 Grounded procedural trees
- Compact connected roots extend bounded gaps into supporting terrain
- Unsupported cliff placements leave no partial trees
- Root and canopy roles follow object scaling and rotation
- Canopies preserve solid trunk cells across chunk replay and saved continuations
- Branch wood replaces leaf clusters within each tree
- Regression tests cover slopes and overlapping tree placements
- Gameplay checks compare grounded spindle geometry across both packs
2026-10-18 18:00:40 -04:00
Brian Neumann-Fopiano 51c626c410 Merged custom content fixes
- Deferred placement guards and generated content ownership share the current engine
- Native terrain writes invalidate stale custom content across platforms
2026-10-18 17:59:20 -04:00
Brian Neumann-Fopiano 34417b2f5b Tracked generated content ownership
- Generated furniture keeps ownership tokens for targeted removal
- Native writes invalidate pending custom content and tile records
- Regeneration and chunk updates remove invalidated content on owning threads
- Provider callbacks return ownership tokens across server platforms
- Regression fixtures cover terrain replacement and restart cleanup
2026-10-18 17:58:30 -04:00
Brian Neumann-Fopiano ed5483d75a Merged stale placement guard
- Combine the carrier check with native write invalidation
2026-10-16 19:37:15 -04:00
Brian Neumann-Fopiano ff537f4abc Cleared records under native writes
- Added DeferredPlacementRecords to gate tracking on Identifier and TileWrapper slices
- Cleared deferred custom and tile records at positions native structure placement changed
- Implemented block change tracking in the Bukkit and modded structure policies
- Covered invalidation through the custom block materialization pass
- Covered modded wrapper change tracking and protected cells
- Passed disabled change sets in existing wrapper tests
2026-10-16 19:36:35 -04:00
Brian Neumann-Fopiano dbc9d95372 Skip stale deferred placements
- Skip deferred custom block records whose live block no longer matches the provider carrier type
- Leave air-carrier furniture and unresolvable carriers to the provider unchanged
- Resolve each carrier once per chunk pass and log skipped records once per chunk at debug level
- Skip tile records quietly when the live block no longer matches the tile material
- Apply the same carrier check to the modded custom block pass
- Add Bukkit, CraftEngine and modded regression tests for flattened, replaced, intact and furniture records
2026-10-16 19:32:01 -04:00
149 changed files with 7567 additions and 905 deletions

No files matched your search

+16 -24
View File
@@ -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
@@ -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());
@@ -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() {
@@ -9,6 +9,7 @@ import art.arcane.iris.structure.nativegen.NativeStructureVolumeIndex;
import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume;
import art.arcane.volmlib.nativelib.terrain.structure.StructureTerrainSettings;
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
import art.arcane.volmlib.util.collection.KList;
import net.minecraft.SharedConstants;
@@ -69,7 +70,8 @@ public class NativeStructureVolumeIndexTest {
StructureStart start = swampHut(0, 0, 4, 6);
BoundingBox bounds = start.getPieces().getFirst().getBoundingBox();
KList<NativeStructureVolume> volumes = NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, start);
KList<NativeStructureVolume> volumes = NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, start,
StructureTerrainSettings.SOURCE, (x, z) -> 63);
assertEquals(1, volumes.size());
NativeStructureVolume volume = volumes.getFirst();
@@ -84,8 +86,10 @@ public class NativeStructureVolumeIndexTest {
@Test
public void invalidStartsContributeNoVolumes() {
assertNull(NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, StructureStart.INVALID_START));
assertNull(NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, null));
assertNull(NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, StructureStart.INVALID_START,
StructureTerrainSettings.SOURCE, (x, z) -> 63));
assertNull(NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, null,
StructureTerrainSettings.SOURCE, (x, z) -> 63));
}
@Test
@@ -323,7 +327,7 @@ public class NativeStructureVolumeIndexTest {
BlockingResolver coordination = new BlockingResolver();
NativeStructureVolumeIndex.OriginResolver volumes = (engine, chunkX, chunkZ) ->
new KList<>(new NativeStructureVolume(chunkX + ":" + chunkZ,
-256, 0, -256, 256, 255, 256));
-256, 0, -256, 256, 255, 256, null));
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting((engine, chunkX, chunkZ) -> {
coordination.volumesAt(engine, chunkX, chunkZ);
return volumes.volumesAt(engine, chunkX, chunkZ);
@@ -369,7 +373,7 @@ public class NativeStructureVolumeIndexTest {
terrain.join();
}
return new KList<>(new NativeStructureVolume(chunkX + ":" + chunkZ,
-256, 0, -256, 256, 100, 256));
-256, 0, -256, 256, 100, 256, null));
});
ExecutorService owner = Executors.newSingleThreadExecutor();
ForkJoinPool workers = new ForkJoinPool(2);
@@ -544,7 +548,8 @@ public class NativeStructureVolumeIndexTest {
if (chunkX != originChunkX || chunkZ != originChunkZ) {
return NativeStructureVolume.NONE;
}
return NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, swampHut(chunkX, chunkZ, 4, 6));
return NativeStructureVolumeSource.appendPieces(null, STRUCTURE_KEY, swampHut(chunkX, chunkZ, 4, 6),
StructureTerrainSettings.SOURCE, (x, z) -> 63);
}
}
@@ -596,7 +601,7 @@ public class NativeStructureVolumeIndexTest {
return new KList<>(new NativeStructureVolume(
chunkX + ":" + chunkZ,
0, 0, 0,
15, 255, 15));
15, 255, 15, null));
}
}
private record ScopedEngine(IrisEngine engine, AtomicInteger scopes) {
@@ -245,9 +245,15 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
for (int y = groundY + 1; y <= groundY + 32; y++) {
assertTrue("Roof at " + x + "," + y, state(blocks, x, y, 0).isAir());
}
for (int y = 12; y < groundY; y++) {
int supportY = x < 2 ? groundY - 33 : 12;
for (int y = supportY; y < groundY; y++) {
assertTrue("Missing foundation at " + x + "," + y, state(blocks, x, y, 0).isSolid());
}
if (x < 2) {
for (int y = 13; y < supportY; y++) {
assertTrue("Cave beneath support at " + x + "," + y, state(blocks, x, y, 0).isAir());
}
}
}
}
@@ -268,6 +274,57 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
assertTrue(state(blocks, 0, groundY + 1, 0).isAir());
}
@Test
public void flattenPreservesCavesBelowIntactGroundAtTheSkirtEdge() {
StructureStart start = desertStart();
int groundY = start.getBoundingBox().minY() - 1;
int x = start.getBoundingBox().maxX() + 23;
int z = start.getBoundingBox().minZ();
BoundingBox area = new BoundingBox(x, 0, z, x, 127, z);
Map<BlockPos, BlockState> blocks = new HashMap<>();
for (int y = groundY - 35; y <= groundY; y++) {
BlockState block = y >= groundY - 34 && y <= groundY - 13
? Blocks.CAVE_AIR.defaultBlockState() : Blocks.STONE.defaultBlockState();
put(blocks, x, y, z, block);
}
put(blocks, x, groundY - 1, z, Blocks.DIRT.defaultBlockState());
put(blocks, x, groundY, z, Blocks.GRASS_BLOCK.defaultBlockState());
NativeStructureTerrainIntegrator.TerrainTarget target = surfaceTarget(start, StructureTerrainMode.FLATTEN);
((IrisStructureTerrain) target.terrain()).setHorizontalPadding(24).setFlattenRange(64);
NativeStructureSurfaceFitter.prepareSurfaceStructures(world(blocks), area, List.of(target), (px, pz) -> groundY);
for (int y = groundY - 34; y <= groundY - 13; y++) {
assertEquals("Cave at " + y, Blocks.CAVE_AIR.defaultBlockState(), state(blocks, x, y, z));
}
assertEquals(Blocks.DIRT.defaultBlockState(), state(blocks, x, groundY - 1, z));
}
@Test
public void flattenBridgesTheFoundationGapWithoutFillingACaveBelowItsSupport() {
StructureStart start = desertStart();
int groundY = start.getBoundingBox().minY() - 1;
BoundingBox area = new BoundingBox(0, 0, 0, 0, 127, 0);
Map<BlockPos, BlockState> blocks = new HashMap<>();
put(blocks, 0, groundY, 0, Blocks.GRASS_BLOCK.defaultBlockState());
for (int y = groundY - 11; y <= groundY - 9; y++) {
put(blocks, 0, y, 0, Blocks.STONE.defaultBlockState());
}
for (int y = groundY - 20; y <= groundY - 12; y++) {
put(blocks, 0, y, 0, Blocks.CAVE_AIR.defaultBlockState());
}
NativeStructureSurfaceFitter.prepareSurfaceStructures(world(blocks), area,
List.of(surfaceTarget(start, StructureTerrainMode.FLATTEN)), (x, z) -> groundY);
for (int y = groundY - 8; y < groundY; y++) {
assertEquals("Foundation at " + y, Blocks.STONE.defaultBlockState(), state(blocks, 0, y, 0));
}
for (int y = groundY - 20; y <= groundY - 12; y++) {
assertEquals("Cave at " + y, Blocks.CAVE_AIR.defaultBlockState(), state(blocks, 0, y, 0));
}
}
@Test
public void flattenLeavesBuriedAndSubmergedInstancesUntouched() {
StructureStart start = desertStart();
@@ -619,10 +676,18 @@ public class NativeStructurePostProcessorSurfaceTerrainTest {
for (int z = 0; z <= 8; z++) {
int groundY = plan.flattenedHeights().get(NativeStructureTemplateOccupancy.columnKey(x, z)) - 1;
assertTrue("Village grade at " + x + "," + z + ": " + groundY, groundY >= 64 && groundY <= 70);
for (int y = 20; y <= groundY; y++) {
boolean originalFoundation = (x == 0 || x == 8) && (z == 0 || z == 8);
int supportY = originalFoundation ? 63 : 20;
for (int y = supportY; y <= groundY; y++) {
assertTrue("Disconnected village ground at " + x + "," + y + "," + z,
state(blocks, x, y, z).isSolid());
}
if (originalFoundation) {
for (int y = 21; y < supportY; y++) {
assertTrue("Cave beneath foundation at " + x + "," + y + "," + z,
state(blocks, x, y, z).isAir());
}
}
for (int y = groundY + 1; y <= 96; y++) {
assertTrue("Roof remains above village at " + x + "," + y + "," + z,
state(blocks, x, y, z).isAir());
@@ -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() {
@@ -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) {
@@ -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
}
]
}
@@ -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'
@@ -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
}
]
}
@@ -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}
}]
}
@@ -0,0 +1,15 @@
{
"name": "Object Scaling Fixture",
"version": 1,
"mode": {"type": "OVERWORLD"},
"regions": ["flat"],
"focusRegion": "flat",
"focus": "flat",
"landChance": 1,
"environment": "NORMAL",
"dimensionHeight": {"min": 0, "max": 128},
"logicalHeight": 128,
"fluidHeight": 0,
"bedrock": false,
"caveProfile": {"enabled": false}
}
@@ -0,0 +1,5 @@
{
"seed": 310,
"interpolator": {"function": "NONE", "horizontalScale": 1},
"composite": [{"seed": 310, "style": {"style": "FLAT"}}]
}
@@ -0,0 +1,6 @@
{
"name": "Flat",
"landBiomes": ["flat"],
"seaBiomes": ["flat"],
"shoreBiomes": ["flat"]
}
@@ -0,0 +1,105 @@
import { createHash } from 'node:crypto'
import { readFile, realpath } from 'node:fs/promises'
import path from 'node:path'
export default {
name: 'iris-object-scaling',
description: 'Verify fractional mushroom scaling retains its cap and uses its stem for organic supports.',
async run(context) {
const directory = await realpath(context.server.directory)
const source = await readFile(path.join(directory, '.server-source'), 'utf8')
context.expect(/^isolated=true\s*$/m.test(source), 'Object scaling requires an isolated instance')
const pack = await realpath(path.join(directory, 'plugins', 'Iris', 'packs', 'object-scaling-fixture'))
context.expect(pack.startsWith(`${directory}${path.sep}`), 'Object scaling requires a local fixture pack')
const object = await readFile(path.join(pack, 'objects', 'smolshroom1.iob'))
const objectHash = createHash('sha256').update(object).digest('hex')
const biome = JSON.parse(await readFile(path.join(pack, 'biomes', 'flat.json'), 'utf8'))
const surfaceY = biome.generators[0].min
const originalStemY = surfaceY + biome.objects[0].translate.y
const expectedCapY = originalStemY + 1
const maximumExtension = biome.objects[0].stiltSettings.organicMaxScan
context.expect(surfaceY === 32 && expectedCapY === 39 && maximumExtension === 8,
'Expected the bounded organic support fixture', { surfaceY, expectedCapY, maximumExtension })
const world = context.options.command ?? 'object_scaling_probe'
context.expect(/^[a-z0-9_]+$/.test(world), 'Fixture world name must be a simple identifier')
await context.step('create the fractional mushroom fixture world', async () => {
const response = await context.command(`/iris create name=${world} type=object-scaling-fixture seed=1337`,
/Successfully created your world|World ready|Failed to create|Exception raised/i, 180000)
context.expect(/Successfully created your world|World ready/i.test(response), response)
await context.command(`/iris tp ${world}`)
await context.waitUntil(async () => {
const observation = await context.observe()
const player = observation.players.find(entry => entry.username === context.bot.username)
const current = observation.worlds.find(entry => entry.id === player?.world)
return current?.key === `iris:${world}` || current?.name.endsWith(world)
}, { label: 'Enter the scaling fixture world', timeoutMs: 60000, intervalMs: 100 })
await context.command('/minecraft:gamemode spectator @s')
await context.waitUntil(() => context.bot.game.gameMode === 'spectator',
{ label: 'Spectator mode', timeoutMs: 30000, intervalMs: 100 })
await context.command('/minecraft:tp @s 0.5 48 0.5', /Teleported/i, 30000)
})
const columns = []
await context.step('client sees one cap and continuous stem supports', async () => {
const position = context.bot.entity.position.clone()
await context.waitUntil(() => {
if (Math.abs(context.bot.entity.position.x - 0.5) > 1
|| Math.abs(context.bot.entity.position.z - 0.5) > 1) return false
for (const x of [-16, 0, 16, 31]) {
for (const z of [-16, 0, 16, 31]) {
for (const y of [0, 32, 63]) {
if (!context.bot.blockAt(position.set(x, y, z))) return false
}
}
}
return true
}, { label: 'Complete mushroom fixture chunks', timeoutMs: 60000, intervalMs: 100 })
for (let x = -16; x < 32; x++) {
for (let z = -16; z < 32; z++) {
const caps = []
const stems = []
for (let y = 1; y < 63; y++) {
const block = context.bot.blockAt(position.set(x, y, z))
context.expect(block !== null, 'Every sampled mushroom block must be loaded', { x, y, z })
if (block.name === 'red_mushroom_block') caps.push(y)
if (block.name === 'mushroom_stem') stems.push(y)
}
if (caps.length === 0) continue
context.expect(caps.length === 1, 'Mushroom cap was copied into its stem or support', { x, z, caps })
const capY = caps[0]
context.expect(capY === expectedCapY, 'Fractional scaling changed the expected mushroom height',
{ x, z, capY, expectedCapY })
const bottomY = stems[0]
const stemBlocks = stems.length
context.expect(bottomY >= originalStemY - maximumExtension && bottomY <= surfaceY + 1,
'Mushroom support must reach the surface within its configured extension limit',
{ x, z, capY, bottomY, surfaceY, maximumExtension, stems })
context.expect(stemBlocks === capY - bottomY
&& stems.every((y, index) => y === bottomY + index),
'Scaled mushroom must have a continuous stem from its cap to the ground', { x, z, capY, stems })
const supportY = bottomY - 1
const support = context.bot.blockAt(position.set(x, supportY, z))?.name
const above = context.bot.blockAt(position.set(x, capY + 1, z))?.name
context.expect(support === (supportY === surfaceY ? 'lime_wool' : 'stone'),
'Mushroom stem must terminate on the fixture terrain', { x, z, capY, supportY, support })
context.expect(above === 'air', 'Scaled mushroom has unexpected blocks above its cap', { x, z, capY, above })
columns.push({ x, z, surfaceY, capY, bottomY, stemBlocks,
buriedStemBlocks: Math.max(0, surfaceY - bottomY + 1) })
}
}
context.expect(columns.length >= 8, 'Expected at least eight scaled mushroom columns', { columns })
})
await context.step('server confirms the sampled stem and cap materials', async () => {
for (const [index, column] of columns.slice(0, 8).entries()) {
const { x, z, surfaceY, capY, bottomY } = column
const marker = `IRIS_SCALING_OK_${index}`
await context.command(`/minecraft:execute if block ${x} ${surfaceY + 1} ${z} minecraft:mushroom_stem`
+ ` if block ${x} ${bottomY} ${z} minecraft:mushroom_stem`
+ ` if block ${x} ${capY} ${z} minecraft:red_mushroom_block run minecraft:say ${marker}`,
new RegExp(marker), 10000)
}
})
context.report.objectScaling = { world, scale: 0.75, object: 'smolshroom1', objectHash,
surfaceY, maximumExtension, checkedColumns: columns.length,
serverConfirmedColumns: Math.min(8, columns.length), columns }
}
}
@@ -0,0 +1,82 @@
import { createHash } from 'node:crypto'
import { readFile, writeFile } from 'node:fs/promises'
import path from 'node:path'
const worlds = [
{ name: 'seed-selection-default', seed: '1337', argument: '' },
{ name: 'seed-selection-positive', seed: '78264193', argument: ' seed=78264193' },
{ name: 'seed-selection-negative', seed: '-734829104958217364', argument: ' seed=-734829104958217364' },
{ name: 'seed-selection-zero', seed: '0', argument: ' seed=0' }
]
export default {
name: 'iris-seed-selection',
description: 'Verify omitted, positive, negative, and zero creation seeds against visible terrain and a cold restart.',
async run(context) {
const source = await readFile(path.join(context.server.directory, '.server-source'), 'utf8')
context.expect(/^isolated=true\s*$/m.test(source), 'Seed selection requires an isolated instance')
const wait = (predicate, label) => context.waitUntil(predicate, { label, timeoutMs: 60000, intervalMs: 100 })
const restart = context.options.command === 'restart-check'
const stateFile = path.join(context.server.directory, '.seed-selection.json')
const saved = restart ? JSON.parse(await readFile(stateFile, 'utf8')) : undefined
const jarHash = createHash('sha256').update(await readFile(path.join(context.server.directory, 'plugins', 'Iris.jar'))).digest('hex')
const processId = (await context.observe()).processId
if (restart) {
context.expect(saved.processId !== processId, 'Restart-check requires a cold server restart')
context.expect(saved.jarHash === jarHash, 'Restart-check requires the same Iris jar')
}
const outputs = {}
for (const world of worlds) {
await context.step(`${restart ? 'Reload' : 'Create'} ${world.name} with seed ${world.seed}`, async () => {
if (!restart) {
await context.command(`/iris create name=${world.name} type=cohesion${world.argument}`,
/Successfully created your world/i, 180000)
}
await context.command(`/iris tp ${world.name}`)
await wait(async () => {
const observation = await context.observe()
const player = observation.players.find(entry => entry.username === context.bot.username)
const current = observation.worlds.find(entry => entry.id === player?.world)
return current?.key === `iris:${world.name}` || current?.name.endsWith(world.name)
}, `Enter ${world.name}`)
await context.command('/minecraft:gamemode spectator @s')
await wait(() => context.bot.game.gameMode === 'spectator', 'Spectator mode')
await context.command('/minecraft:seed', new RegExp(`Seed:\\s*\\[?${world.seed}\\]?(?:\\s|$)`))
const x = 1024
const z = -1024
await context.command(`/minecraft:tp @s ${x + 8.5} 48 ${z + 8.5}`, /Teleported/i, 30000)
await wait(() => Math.abs(context.bot.entity.position.x - x - 8.5) < 1
&& Math.abs(context.bot.entity.position.z - z - 8.5) < 1, 'Sample teleport')
const position = context.bot.entity.position.clone()
await wait(() => context.bot.blockAt(position.clone().set(x, 0, z))
&& context.bot.blockAt(position.clone().set(x + 15, 63, z + 15)), 'Complete sample chunk')
const digest = createHash('sha256')
let stone = 0
for (let dx = 0; dx < 16; dx++) {
for (let dz = 0; dz < 16; dz++) {
for (let y = 0; y < 64; y++) {
const block = context.bot.blockAt(position.set(x + dx, y, z + dz))
context.expect(block !== null, 'Missing sampled block', { x: x + dx, y, z: z + dz })
digest.update(`${block.stateId},`)
if (block.name === 'stone') stone++
}
}
}
context.expect(stone > 1000, 'Fixture terrain was not generated', { world: world.name, stone })
const blocks = digest.digest('hex')
outputs[world.name] = { seed: world.seed, blocks }
if (restart) {
context.expect(saved.worlds[world.name].seed === world.seed, 'Seed expectation changed after restart')
context.expect(saved.worlds[world.name].blocks === blocks, 'Persisted terrain changed after restart',
{ world: world.name, before: saved.worlds[world.name].blocks, after: blocks })
}
})
}
const signatures = new Set(Object.values(outputs).map(output => output.blocks))
context.expect(signatures.size === worlds.length, 'Different requested seeds generated identical sample chunks', outputs)
context.report.seedSelection = { restart, jarHash, worlds: outputs }
if (!restart) {
await writeFile(stateFile, JSON.stringify({ processId, jarHash, worlds: outputs }))
}
}
}
@@ -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 })
})
}
}
+1 -1
View File
@@ -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 ->
+2
View File
@@ -33,6 +33,8 @@ plugins {
rootProject.name = 'iris-fabric'
apply from: new File(settingsDir, '../../gradle/runtime-cache.settings.gradle').canonicalFile
dependencyResolutionManagement {
versionCatalogs {
libs {
+16 -2
View File
@@ -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'))
+2
View File
@@ -33,6 +33,8 @@ plugins {
rootProject.name = 'iris-forge'
apply from: new File(settingsDir, '../../gradle/runtime-cache.settings.gradle').canonicalFile
dependencyResolutionManagement {
versionCatalogs {
libs {
@@ -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() {
@@ -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();
@@ -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;
}
/**
@@ -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;
}
/**
@@ -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;
@@ -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) {
@@ -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());
}
}
}
@@ -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);
@@ -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() {
@@ -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();
@@ -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));
@@ -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;
}
}
}
@@ -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\\);",
+1 -1
View File
@@ -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 ->
+2
View File
@@ -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
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
./gradlew buildAllToOut --no-parallel -PuseLocalVolmLib=false "$@"
./gradlew buildAll "$@"
+103 -228
View File
@@ -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')
+4 -1
View File
@@ -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 {
+2 -15
View File
@@ -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);
+4 -1
View File
@@ -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
View File
@@ -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()
@@ -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());
@@ -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) {
}
}
@@ -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)
@@ -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;
}
@@ -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());
}
}
@@ -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) {
}
}
@@ -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) {
}
}
@@ -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 {
@@ -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;
@@ -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);
}
}
}
}
@@ -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