mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-19 23:14:10 +00:00
Merged custom content fixes
- Deferred placement guards and generated content ownership share the current engine - Native terrain writes invalidate stale custom content across platforms
This commit is contained in:
+10
-6
@@ -3,6 +3,7 @@ package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
|
||||
import art.arcane.volmlib.nativelib.v26_2_R1.terrain.NativeStructureWorldgenAccess;
|
||||
|
||||
import art.arcane.iris.structure.placement.IrisStructureTerrain;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockChangeSet;
|
||||
import art.arcane.volmlib.util.structure.StructureTerrainMode;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructurePostProcessor;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureTerrainIntegrator;
|
||||
@@ -230,8 +231,9 @@ public class NativeStructureWorldgenAccessTest {
|
||||
recording.world(), generationCenter,
|
||||
(x, z) -> SURFACE_FIRST_FREE_Y,
|
||||
(x, z) -> FLOOR_FIRST_FREE_Y,
|
||||
true,
|
||||
position -> false);
|
||||
true, true,
|
||||
position -> false,
|
||||
NativeBlockChangeSet.disabled());
|
||||
int x = generationCenter.getMiddleBlockX();
|
||||
int z = generationCenter.getMiddleBlockZ();
|
||||
|
||||
@@ -248,8 +250,9 @@ public class NativeStructureWorldgenAccessTest {
|
||||
recording.world(), generationCenter(),
|
||||
(x, z) -> SURFACE_FIRST_FREE_Y,
|
||||
(x, z) -> FLOOR_FIRST_FREE_Y,
|
||||
false,
|
||||
position::equals);
|
||||
false, true,
|
||||
position::equals,
|
||||
NativeBlockChangeSet.disabled());
|
||||
|
||||
assertSame(Blocks.DIRT, access.getBlockState(position).getBlock());
|
||||
assertFalse(access.ensureCanWrite(position));
|
||||
@@ -319,8 +322,9 @@ public class NativeStructureWorldgenAccessTest {
|
||||
recording.world(), generationCenter(),
|
||||
(x, z) -> SURFACE_FIRST_FREE_Y,
|
||||
(x, z) -> FLOOR_FIRST_FREE_Y,
|
||||
false,
|
||||
position -> false);
|
||||
false, true,
|
||||
position -> false,
|
||||
NativeBlockChangeSet.disabled());
|
||||
}
|
||||
|
||||
private static ChunkPos generationCenter() {
|
||||
|
||||
+10
-6
@@ -3,6 +3,7 @@ package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
|
||||
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureWorldgenAccess;
|
||||
|
||||
import art.arcane.iris.structure.placement.IrisStructureTerrain;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockChangeSet;
|
||||
import art.arcane.volmlib.util.structure.StructureTerrainMode;
|
||||
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructurePostProcessor;
|
||||
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeStructureTerrainIntegrator;
|
||||
@@ -230,8 +231,9 @@ public class NativeStructureWorldgenAccessTest {
|
||||
recording.world(), generationCenter,
|
||||
(x, z) -> SURFACE_FIRST_FREE_Y,
|
||||
(x, z) -> FLOOR_FIRST_FREE_Y,
|
||||
true,
|
||||
position -> false);
|
||||
true, true,
|
||||
position -> false,
|
||||
NativeBlockChangeSet.disabled());
|
||||
int x = generationCenter.getMiddleBlockX();
|
||||
int z = generationCenter.getMiddleBlockZ();
|
||||
|
||||
@@ -248,8 +250,9 @@ public class NativeStructureWorldgenAccessTest {
|
||||
recording.world(), generationCenter(),
|
||||
(x, z) -> SURFACE_FIRST_FREE_Y,
|
||||
(x, z) -> FLOOR_FIRST_FREE_Y,
|
||||
false,
|
||||
position::equals);
|
||||
false, true,
|
||||
position::equals,
|
||||
NativeBlockChangeSet.disabled());
|
||||
|
||||
assertSame(Blocks.DIRT, access.getBlockState(position).getBlock());
|
||||
assertFalse(access.ensureCanWrite(position));
|
||||
@@ -319,8 +322,9 @@ public class NativeStructureWorldgenAccessTest {
|
||||
recording.world(), generationCenter(),
|
||||
(x, z) -> SURFACE_FIRST_FREE_Y,
|
||||
(x, z) -> FLOOR_FIRST_FREE_Y,
|
||||
false,
|
||||
position -> false);
|
||||
false, true,
|
||||
position -> false,
|
||||
NativeBlockChangeSet.disabled());
|
||||
}
|
||||
|
||||
private static ChunkPos generationCenter() {
|
||||
|
||||
+11
@@ -19,6 +19,10 @@
|
||||
package art.arcane.iris.world.runtime;
|
||||
|
||||
import art.arcane.iris.api.world.IrisWorldPhase;
|
||||
import art.arcane.iris.generation.mantle.CustomBlockOwnership;
|
||||
import art.arcane.iris.integration.ExternalDataSVC;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockPoint;
|
||||
import art.arcane.iris.pack.datapack.ServerConfigurator;
|
||||
import art.arcane.iris.pack.datapack.DatapackIngestService;
|
||||
import art.arcane.iris.world.event.IrisEngineHotloadEvent;
|
||||
@@ -51,6 +55,13 @@ import org.bukkit.World;
|
||||
import org.bukkit.WorldBorder;
|
||||
|
||||
public final class BukkitEnginePlatformHooks implements EnginePlatformHooks {
|
||||
@Override
|
||||
public boolean removeCustomBlock(Engine engine, NativeBlockPoint position, CustomBlockOwnership.Entry entry) {
|
||||
ExternalDataSVC service = IrisServices.getOrNull(ExternalDataSVC.class);
|
||||
return service != null && service.removeGenerated(
|
||||
BukkitWorldBinding.world(engine.getWorld()).getBlockAt(position.x(), position.y(), position.z()), entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> flushSavedTerrainCapture(Engine engine) {
|
||||
return INMS.get().flushSavedTerrainCapture(BukkitWorldBinding.world(engine.getWorld()));
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "Flat",
|
||||
"derivative": "minecraft:snowy_plains",
|
||||
"vanillaDerivative": "minecraft:snowy_plains",
|
||||
"layers": [
|
||||
{
|
||||
"minHeight": 1,
|
||||
"maxHeight": 1,
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:lime_wool"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"minHeight": 384,
|
||||
"maxHeight": 384,
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:green_concrete"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"generators": [
|
||||
{
|
||||
"generator": "flat",
|
||||
"min": 96,
|
||||
"max": 96
|
||||
}
|
||||
],
|
||||
"decorators": [
|
||||
{
|
||||
"palette": [
|
||||
{
|
||||
"block": "craftengine:default/flower_basket",
|
||||
"data": {
|
||||
"variant": "ground"
|
||||
}
|
||||
}
|
||||
],
|
||||
"chance": 1,
|
||||
"forcePlace": true
|
||||
}
|
||||
]
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "CraftEngine Imported Feature Fixture",
|
||||
"version": 1,
|
||||
"mode": {
|
||||
"type": "OVERWORLD"
|
||||
},
|
||||
"regions": [
|
||||
"flat"
|
||||
],
|
||||
"focusRegion": "flat",
|
||||
"focus": "flat",
|
||||
"landChance": 1,
|
||||
"environment": "NORMAL",
|
||||
"dimensionHeight": {
|
||||
"min": -64,
|
||||
"max": 320
|
||||
},
|
||||
"logicalHeight": 384,
|
||||
"fluidHeight": 0,
|
||||
"bedrock": false,
|
||||
"rockPalette": {
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:red_concrete"
|
||||
}
|
||||
]
|
||||
},
|
||||
"importedStructures": {
|
||||
"disabled": [
|
||||
"minecraft:"
|
||||
]
|
||||
},
|
||||
"importedFeatures": {
|
||||
"enabled": true,
|
||||
"steps": [
|
||||
"TOP_LAYER_MODIFICATION"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"interpolator": {
|
||||
"function": "NONE",
|
||||
"horizontalScale": 1
|
||||
},
|
||||
"seed": 310,
|
||||
"composite": [
|
||||
{
|
||||
"seed": 310,
|
||||
"style": {
|
||||
"style": "FLAT"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "Flat",
|
||||
"landBiomes": [
|
||||
"flat"
|
||||
],
|
||||
"seaBiomes": [
|
||||
"flat"
|
||||
],
|
||||
"shoreBiomes": [
|
||||
"flat"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
}
|
||||
|
||||
version = '1.0.0'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven { url = 'https://repo.papermc.io/repository/maven-public/' }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly files(providers.gradleProperty('irisJar').get())
|
||||
compileOnly files(providers.gradleProperty('craftEngineJar').get())
|
||||
compileOnly 'io.papermc.paper:paper-api:26.1.2.build.74-stable'
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain.languageVersion = JavaLanguageVersion.of(25)
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
java.srcDirs = ['src']
|
||||
resources.srcDirs = ['resources']
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
options.compilerArgs += '-parameters'
|
||||
}
|
||||
|
||||
tasks.named('jar') {
|
||||
archiveBaseName = 'IrisCraftEngineNativeFixture'
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
name: IrisCraftEngineNativeFixture
|
||||
version: 1.0.0
|
||||
main: art.arcane.iris.fixture.CraftEngineNativeFixture
|
||||
api-version: '26.1.2'
|
||||
folia-supported: true
|
||||
depend: [Iris, CraftEngine]
|
||||
commands:
|
||||
iriscenative:
|
||||
permission: iriscenative.run
|
||||
permissions:
|
||||
iriscenative.run:
|
||||
default: op
|
||||
@@ -0,0 +1 @@
|
||||
rootProject.name = 'iris-craftengine-native-fixture'
|
||||
+385
@@ -0,0 +1,385 @@
|
||||
package art.arcane.iris.fixture;
|
||||
|
||||
import art.arcane.iris.generation.block.IrisCustomData;
|
||||
import art.arcane.iris.generation.block.TileData;
|
||||
import art.arcane.iris.generation.mantle.DeferredPlacementRecords;
|
||||
import art.arcane.iris.generation.mantle.CustomBlockOwnership;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.integration.ExternalDataSVC;
|
||||
import art.arcane.iris.integration.Identifier;
|
||||
import art.arcane.iris.platform.generation.EngineBukkitOps;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.world.IrisCreator;
|
||||
import art.arcane.iris.world.IrisToolbelt;
|
||||
import art.arcane.iris.world.storage.matter.TileWrapper;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockChangeSet;
|
||||
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import net.momirealms.craftengine.bukkit.api.CraftEngineBlocks;
|
||||
import net.momirealms.craftengine.bukkit.api.CraftEngineFurniture;
|
||||
import net.momirealms.craftengine.core.util.Key;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.GameMode;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public final class CraftEngineNativeFixture extends JavaPlugin {
|
||||
private static final NamespacedKey WORLD_KEY = new NamespacedKey("iris", "ce_native_probe");
|
||||
private static final NamespacedKey FEATURE_WORLD_KEY = new NamespacedKey("iris", "ce_native_feature_probe");
|
||||
private static final int TEST_Y = 180;
|
||||
private static final int TEST_Z = 8;
|
||||
private static final String BLOCK_ID = "craftengine:default/topaz_ore";
|
||||
private static final String FURNITURE_ID = "craftengine:default/flower_basket[variant=ground]";
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] arguments) {
|
||||
if (!(sender instanceof Player player) || arguments.length != 1) {
|
||||
sender.sendMessage("CE_FAILED Use /iriscenative prepare|pending|neighbor|loaded|features|restart from a player");
|
||||
return true;
|
||||
}
|
||||
if (arguments[0].equals("prepare")) {
|
||||
player.setGameMode(GameMode.CREATIVE);
|
||||
prepare(player);
|
||||
return true;
|
||||
}
|
||||
if (arguments[0].equals("features")) {
|
||||
importedFeatures(player);
|
||||
return true;
|
||||
}
|
||||
World world = Bukkit.getWorld(WORLD_KEY);
|
||||
if (world == null) {
|
||||
fail(player, new IllegalStateException("Fixture world has not been created"));
|
||||
return true;
|
||||
}
|
||||
if (arguments[0].equals("restart")) {
|
||||
loadChunks().thenCompose(ignored -> J.runRegionFuture(world, 0, 0,
|
||||
() -> later(player, world, () -> restarted(player, world))))
|
||||
.whenComplete((ignored, failure) -> {
|
||||
if (failure != null) {
|
||||
fail(player, failure);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
J.runRegionFuture(world, 0, 0, () -> {
|
||||
switch (arguments[0]) {
|
||||
case "pending" -> pending(player, world);
|
||||
case "neighbor" -> neighbor(player, world);
|
||||
case "loaded" -> invalidateLoaded(player, world, IrisToolbelt.access(world).getEngine());
|
||||
default -> throw new IllegalArgumentException("Unknown fixture operation");
|
||||
}
|
||||
}).whenComplete((ignored, failure) -> {
|
||||
if (failure != null) {
|
||||
fail(player, failure);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
private void prepare(Player player) {
|
||||
CompletableFuture.runAsync(() -> {
|
||||
if (Bukkit.getWorld(WORLD_KEY) == null) {
|
||||
try {
|
||||
new IrisCreator().name(WORLD_KEY.toString()).dimension("ce-native-fixture")
|
||||
.seed(78264193L).create();
|
||||
} catch (Exception failure) {
|
||||
throw new IllegalStateException("Could not create CraftEngine fixture world", failure);
|
||||
}
|
||||
}
|
||||
}).thenCompose(ignored -> loadChunks()).thenCompose(ignored -> {
|
||||
World world = Bukkit.getWorld(WORLD_KEY);
|
||||
return J.runRegionFuture(world, 0, 0, () -> preparePlatform(world));
|
||||
}).thenAccept(ignored -> {
|
||||
World world = Bukkit.getWorld(WORLD_KEY);
|
||||
if (!J.runEntity(player, () -> player.teleportAsync(new Location(world, 8.5, TEST_Y + 3, 8.5))
|
||||
.whenComplete((teleported, failure) -> {
|
||||
if (failure != null || !Boolean.TRUE.equals(teleported)) {
|
||||
fail(player, failure == null ? new IllegalStateException("Fixture teleport failed") : failure);
|
||||
return;
|
||||
}
|
||||
player.sendMessage("CE_READY world=" + WORLD_KEY + " folia=" + J.isFolia());
|
||||
}))) {
|
||||
throw new IllegalStateException("Could not schedule fixture teleport");
|
||||
}
|
||||
}).exceptionally(failure -> {
|
||||
fail(player, failure);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
private void importedFeatures(Player player) {
|
||||
CompletableFuture.runAsync(() -> {
|
||||
if (Bukkit.getWorld(FEATURE_WORLD_KEY) == null) {
|
||||
try {
|
||||
new IrisCreator().name(FEATURE_WORLD_KEY.toString()).dimension("ce-native-feature-fixture")
|
||||
.seed(78264193L).create();
|
||||
} catch (Exception failure) {
|
||||
throw new IllegalStateException("Could not create imported feature fixture world", failure);
|
||||
}
|
||||
}
|
||||
}).thenCompose(ignored -> {
|
||||
World world = Bukkit.getWorld(FEATURE_WORLD_KEY);
|
||||
return world.getChunkAtAsync(0, 0, true);
|
||||
}).thenCompose(chunk -> J.runRegionFuture(chunk.getWorld(), 0, 0, () -> {
|
||||
World world = chunk.getWorld();
|
||||
Engine engine = IrisToolbelt.access(world).getEngine();
|
||||
int snow = 0;
|
||||
int pending = 0;
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
if (world.getBlockAt(x, 97, z).getType() == Material.SNOW) {
|
||||
snow++;
|
||||
}
|
||||
if (engine.getMantle().getMantle().get(x, 97 - engine.getMinHeight(), z, Identifier.class) != null) {
|
||||
pending++;
|
||||
}
|
||||
}
|
||||
}
|
||||
require(snow == 256, "Imported snow feature did not cover all 256 test columns: " + snow);
|
||||
require(pending == 0, "Imported feature left pending furniture records: " + pending);
|
||||
for (Entity entity : chunk.getEntities()) {
|
||||
require(!CraftEngineFurniture.isFurniture(entity), "Imported snow feature left generated furniture");
|
||||
}
|
||||
if (!J.runEntity(player, () -> player.teleportAsync(new Location(world, 8.5, 101, 8.5))
|
||||
.whenComplete((teleported, failure) -> {
|
||||
if (failure != null || !Boolean.TRUE.equals(teleported)) {
|
||||
fail(player, failure == null ? new IllegalStateException("Feature world teleport failed") : failure);
|
||||
return;
|
||||
}
|
||||
player.sendMessage("CE_FEATURES snow=256 pending=0 furniture=0");
|
||||
}))) {
|
||||
throw new IllegalStateException("Could not schedule feature world teleport");
|
||||
}
|
||||
})).exceptionally(failure -> {
|
||||
fail(player, failure);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
private CompletableFuture<Void> loadChunks() {
|
||||
World world = Bukkit.getWorld(WORLD_KEY);
|
||||
require(world != null && IrisToolbelt.isIrisWorld(world), "Fixture must use a real Iris world");
|
||||
List<CompletableFuture<Void>> chunks = new ArrayList<>(15);
|
||||
for (int x = -1; x <= 3; x++) {
|
||||
for (int z = -1; z <= 1; z++) {
|
||||
chunks.add(world.getChunkAtAsync(x, z, true).thenCompose(chunk ->
|
||||
J.runRegionFuture(world, chunk.getX(), chunk.getZ(), () -> chunk.addPluginChunkTicket(this))));
|
||||
}
|
||||
}
|
||||
return CompletableFuture.allOf(chunks.toArray(CompletableFuture[]::new));
|
||||
}
|
||||
|
||||
private void preparePlatform(World world) {
|
||||
require(CraftEngineBlocks.byId(Key.of("default:topaz_ore")) != null, "CraftEngine default block is missing");
|
||||
require(CraftEngineFurniture.byId(Key.of("default:flower_basket")) != null, "CraftEngine furniture is missing");
|
||||
for (int chunkX = 0; chunkX <= 2; chunkX++) {
|
||||
for (Entity entity : world.getChunkAt(chunkX, 0).getEntities()) {
|
||||
if (CraftEngineFurniture.isFurniture(entity)) {
|
||||
CraftEngineFurniture.remove(entity, false, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int x = 0; x <= 47; x++) {
|
||||
for (int z = 2; z <= 14; z++) {
|
||||
world.getBlockAt(x, TEST_Y - 1, z).setType(Material.STONE, false);
|
||||
for (int y = TEST_Y; y <= TEST_Y + 5; y++) {
|
||||
world.getBlockAt(x, y, z).setType(Material.AIR, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void pending(Player player, World world) {
|
||||
Engine engine = IrisToolbelt.access(world).getEngine();
|
||||
Mantle<Matter> mantle = engine.getMantle().getMantle();
|
||||
queue(engine, world, 2, BLOCK_ID);
|
||||
queue(engine, world, 4, BLOCK_ID);
|
||||
queue(engine, world, 6, BLOCK_ID);
|
||||
queue(engine, world, 8, BLOCK_ID);
|
||||
queue(engine, world, 10, FURNITURE_ID);
|
||||
queue(engine, world, 12, FURNITURE_ID);
|
||||
Block chest = world.getBlockAt(14, TEST_Y, TEST_Z);
|
||||
chest.setType(Material.CHEST, false);
|
||||
mantle.set(14, TEST_Y - engine.getMinHeight(), TEST_Z, new TileWrapper(TileData.getTileState(chest, false)));
|
||||
world.getBlockAt(4, TEST_Y, TEST_Z).setType(Material.AIR, false);
|
||||
world.getBlockAt(6, TEST_Y, TEST_Z).setType(Material.COBBLESTONE, false);
|
||||
|
||||
NativeBlockChangeSet changes = DeferredPlacementRecords.track(engine, bounds(world));
|
||||
changes.add(8, TEST_Y, TEST_Z);
|
||||
changes.add(12, TEST_Y - 1, TEST_Z);
|
||||
changes.add(12, TEST_Y, TEST_Z);
|
||||
changes.add(14, TEST_Y, TEST_Z);
|
||||
world.getBlockAt(8, TEST_Y, TEST_Z).setType(Material.AIR, false);
|
||||
world.getBlockAt(12, TEST_Y - 1, TEST_Z).setType(Material.AIR, false);
|
||||
chest.setType(Material.COBBLESTONE, false);
|
||||
DeferredPlacementRecords.invalidate(engine, changes);
|
||||
require(record(engine, 8, Identifier.class) == null, "Native cut retained a pending custom record");
|
||||
require(record(engine, 12, Identifier.class) == null, "Flatten column retained pending furniture");
|
||||
require(record(engine, 14, TileWrapper.class) == null, "Native wall retained pending chest data");
|
||||
require(record(engine, 2, Identifier.class) != null, "Native writes invalidated an untouched custom record");
|
||||
replay(engine, world, 0);
|
||||
CustomBlockOwnership ownership = record(engine, 10, CustomBlockOwnership.class);
|
||||
require(record(engine, 2, Identifier.class) == null, "Custom materialization did not consume pending records");
|
||||
require(CraftEngineBlocks.isCustomBlock(world.getBlockAt(2, TEST_Y, TEST_Z)), "Intact custom block was not placed");
|
||||
require(world.getBlockAt(4, TEST_Y, TEST_Z).getType().isAir(), "Cut custom block was resurrected");
|
||||
require(world.getBlockAt(6, TEST_Y, TEST_Z).getType() == Material.COBBLESTONE, "Custom block replaced native wall");
|
||||
require(world.getBlockAt(8, TEST_Y, TEST_Z).getType().isAir(), "Invalidated custom block was resurrected");
|
||||
require(chest.getType() == Material.COBBLESTONE, "Chest data replaced native wall");
|
||||
later(player, world, () -> {
|
||||
int intactFurniture = furnitureAt(world, 10);
|
||||
require(intactFurniture == 1, "Intact furniture count is " + intactFurniture
|
||||
+ "; pending=" + record(engine, 10, Identifier.class)
|
||||
+ "; ownership=" + ownership
|
||||
+ "; chunkEntities=" + world.getChunkAt(0, 0).getEntities().length);
|
||||
require(furnitureAt(world, 12) == 0, "Flattened furniture was resurrected");
|
||||
player.sendMessage("CE_PENDING intact=1 air=1 wall=1 invalidated=1 flatten=1 chest=1 furniture=1");
|
||||
});
|
||||
}
|
||||
|
||||
private void neighbor(Player player, World world) {
|
||||
Engine engine = IrisToolbelt.access(world).getEngine();
|
||||
queue(engine, world, 18, FURNITURE_ID);
|
||||
queue(engine, world, 20, FURNITURE_ID);
|
||||
queue(engine, world, 22, FURNITURE_ID);
|
||||
queue(engine, world, 42, FURNITURE_ID);
|
||||
replay(engine, world, 1);
|
||||
replay(engine, world, 2);
|
||||
world.getBlockAt(40, TEST_Y, TEST_Z).setType(Material.DIAMOND_BLOCK, false);
|
||||
later(player, world, () -> {
|
||||
require(furnitureAt(world, 18) == 1 && furnitureAt(world, 20) == 1
|
||||
&& furnitureAt(world, 22) == 1, "Neighbor furniture did not materialize");
|
||||
require(furnitureAt(world, 42) == 1 && record(engine, 42, CustomBlockOwnership.class) != null,
|
||||
"Regeneration target furniture did not materialize with saved ownership");
|
||||
if (!J.runEntity(player, () -> player.teleportAsync(new Location(world, 40.5, TEST_Y + 3, TEST_Z + 0.5))
|
||||
.whenComplete((teleported, failure) -> {
|
||||
if (failure != null || !Boolean.TRUE.equals(teleported)) {
|
||||
fail(player, failure == null ? new IllegalStateException("Regeneration target teleport failed") : failure);
|
||||
return;
|
||||
}
|
||||
player.sendMessage("CE_NEIGHBOR_READY chunk=2,0 furniture=3 target=1");
|
||||
}))) {
|
||||
throw new IllegalStateException("Could not schedule regeneration target teleport");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void invalidateLoaded(Player player, World world, Engine engine) {
|
||||
require(world.getBlockAt(40, TEST_Y, TEST_Z).getType().isAir(), "Adjacent target chunk did not regenerate");
|
||||
require(furnitureAt(world, 18) == 1 && furnitureAt(world, 20) == 1
|
||||
&& furnitureAt(world, 22) == 1, "Neighbor furniture did not materialize");
|
||||
NativeBlockChangeSet changes = DeferredPlacementRecords.track(engine, bounds(world));
|
||||
changes.add(18, TEST_Y - 1, TEST_Z);
|
||||
changes.add(18, TEST_Y, TEST_Z);
|
||||
changes.add(22, TEST_Y, TEST_Z);
|
||||
world.getBlockAt(18, TEST_Y - 1, TEST_Z).setType(Material.AIR, false);
|
||||
world.getBlockAt(18, TEST_Y, TEST_Z).setType(Material.COBBLESTONE, false);
|
||||
DeferredPlacementRecords.invalidate(engine, changes);
|
||||
require(CraftEngineFurniture.place(new Location(world, 22.5, TEST_Y, TEST_Z + 0.5),
|
||||
Key.of("default:flower_basket"), "ground", false) != null,
|
||||
"Could not place replacement furniture before scheduled cleanup");
|
||||
later(player, world, () -> {
|
||||
require(furnitureAt(world, 42) == 0, "Regeneration retained target chunk furniture");
|
||||
require(furnitureAt(world, 18) == 0, "Native write left materialized neighbor furniture");
|
||||
require(furnitureAt(world, 20) == 1, "Native write removed untouched neighbor furniture");
|
||||
require(furnitureAt(world, 22) == 1, "Native cleanup removed replacement furniture or retained the old one");
|
||||
require(world.getBlockAt(18, TEST_Y, TEST_Z).getType() == Material.COBBLESTONE,
|
||||
"Furniture cleanup replaced the native wall");
|
||||
player.sendMessage("CE_LOADED removed=1 untouched=1 replacement=1 wall=1 target=1");
|
||||
});
|
||||
}
|
||||
|
||||
private void restarted(Player player, World world) {
|
||||
require(furnitureAt(world, 20) == 1, "Generated neighbor furniture did not survive restart");
|
||||
require(furnitureAt(world, 10) == 1, "Untouched furniture did not survive restart");
|
||||
Engine engine = IrisToolbelt.access(world).getEngine();
|
||||
NativeBlockChangeSet changes = DeferredPlacementRecords.track(engine, bounds(world));
|
||||
changes.add(20, TEST_Y, TEST_Z);
|
||||
DeferredPlacementRecords.invalidate(engine, changes);
|
||||
later(player, world, () -> {
|
||||
require(furnitureAt(world, 20) == 0, "Persisted furniture ownership was not invalidated after restart");
|
||||
require(furnitureAt(world, 10) == 1, "Restart cleanup removed untouched furniture");
|
||||
player.sendMessage("CE_RESTART removed=1 untouched=1");
|
||||
});
|
||||
}
|
||||
|
||||
private void queue(Engine engine, World world, int x, String identifier) {
|
||||
ExternalDataSVC service = IrisServices.get(ExternalDataSVC.class);
|
||||
BlockData data = service.getBlockData(Identifier.fromString(identifier)).orElseThrow();
|
||||
require(data instanceof IrisCustomData, "Iris did not resolve real CraftEngine content: " + identifier);
|
||||
IrisCustomData custom = (IrisCustomData) data;
|
||||
world.getBlockAt(x, TEST_Y, TEST_Z).setBlockData(custom.getBase(), false);
|
||||
engine.getMantle().getMantle().set(x, TEST_Y - engine.getMinHeight(), TEST_Z, custom.getCustom());
|
||||
}
|
||||
|
||||
private <T> T record(Engine engine, int x, Class<T> type) {
|
||||
return engine.getMantle().getMantle().get(x, TEST_Y - engine.getMinHeight(), TEST_Z, type);
|
||||
}
|
||||
|
||||
private void replay(Engine engine, World world, int chunkX) {
|
||||
Mantle<Matter> mantle = engine.getMantle().getMantle();
|
||||
for (int x = chunkX - 1; x <= chunkX + 1; x++) {
|
||||
for (int z = -1; z <= 1; z++) {
|
||||
require(world.isChunkLoaded(x, z), "Replay neighbor is not loaded: " + x + "," + z);
|
||||
}
|
||||
}
|
||||
require(mantle.isLoaded(world.getChunkAt(chunkX, 0)), "Replay mantle chunk is not loaded: " + chunkX);
|
||||
mantle.flag(chunkX, 0, MantleFlag.ETCHED, false);
|
||||
mantle.flag(chunkX, 0, MantleFlag.TILE, false);
|
||||
mantle.flag(chunkX, 0, MantleFlag.CUSTOM, false);
|
||||
EngineBukkitOps.updateChunk(engine, world.getChunkAt(chunkX, 0), mantle);
|
||||
}
|
||||
|
||||
private int furnitureAt(World world, int x) {
|
||||
int count = 0;
|
||||
for (Entity entity : world.getChunkAt(x >> 4, TEST_Z >> 4).getEntities()) {
|
||||
Location location = entity.getLocation();
|
||||
if (location.getBlockX() == x && location.getBlockY() == TEST_Y && location.getBlockZ() == TEST_Z
|
||||
&& CraftEngineFurniture.getLoadedFurnitureByMetaEntity(entity) != null) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private void later(Player player, World world, Runnable assertion) {
|
||||
Bukkit.getRegionScheduler().runDelayed(this, new Location(world, 8, TEST_Y, TEST_Z), task -> {
|
||||
try {
|
||||
assertion.run();
|
||||
} catch (Throwable failure) {
|
||||
fail(player, failure);
|
||||
}
|
||||
}, 20);
|
||||
}
|
||||
|
||||
private NativeBlockChangeSet.Bounds bounds(World world) {
|
||||
return new NativeBlockChangeSet.Bounds(-1, -1, 3, 1, world.getMinHeight(), world.getMaxHeight() - world.getMinHeight());
|
||||
}
|
||||
|
||||
private void require(boolean condition, String message) {
|
||||
if (!condition) {
|
||||
throw new IllegalStateException(message);
|
||||
}
|
||||
}
|
||||
|
||||
private void fail(CommandSender sender, Throwable failure) {
|
||||
getLogger().log(Level.SEVERE, "CraftEngine native placement assertion failed", failure);
|
||||
sender.sendMessage("CE_FAILED " + failure.getMessage());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "Flat",
|
||||
"derivative": "minecraft:plains",
|
||||
"vanillaDerivative": "minecraft:plains",
|
||||
"layers": [
|
||||
{
|
||||
"minHeight": 1,
|
||||
"maxHeight": 1,
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:lime_wool"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"minHeight": 384,
|
||||
"maxHeight": 384,
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:green_concrete"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"generators": [
|
||||
{
|
||||
"generator": "flat",
|
||||
"min": 96,
|
||||
"max": 96
|
||||
}
|
||||
]
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "CraftEngine Native Fixture",
|
||||
"version": 1,
|
||||
"mode": {
|
||||
"type": "OVERWORLD"
|
||||
},
|
||||
"regions": [
|
||||
"flat"
|
||||
],
|
||||
"focusRegion": "flat",
|
||||
"focus": "flat",
|
||||
"landChance": 1,
|
||||
"environment": "NORMAL",
|
||||
"dimensionHeight": {
|
||||
"min": -64,
|
||||
"max": 320
|
||||
},
|
||||
"logicalHeight": 384,
|
||||
"fluidHeight": 0,
|
||||
"bedrock": false,
|
||||
"rockPalette": {
|
||||
"palette": [
|
||||
{
|
||||
"block": "minecraft:red_concrete"
|
||||
}
|
||||
]
|
||||
},
|
||||
"importedStructures": {
|
||||
"disabled": [
|
||||
"minecraft:"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"interpolator": {
|
||||
"function": "NONE",
|
||||
"horizontalScale": 1
|
||||
},
|
||||
"seed": 310,
|
||||
"composite": [
|
||||
{
|
||||
"seed": 310,
|
||||
"style": {
|
||||
"style": "FLAT"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "Flat",
|
||||
"landBiomes": [
|
||||
"flat"
|
||||
],
|
||||
"seaBiomes": [
|
||||
"flat"
|
||||
],
|
||||
"shoreBiomes": [
|
||||
"flat"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
|
||||
export default {
|
||||
name: 'iris-craftengine-native-restart',
|
||||
description: 'Verify persisted generated furniture ownership after a full server restart.',
|
||||
async run(context) {
|
||||
const source = await readFile(path.join(context.server.directory, '.server-source'), 'utf8')
|
||||
context.expect(/^isolated=true\s*$/m.test(source), 'CraftEngine regression requires an isolated instance')
|
||||
await context.step('native writes remove persisted ownership after restart', async () => {
|
||||
const response = await context.command('/iriscenative restart', /CE_RESTART|CE_FAILED/, 60000)
|
||||
context.expect(response.includes('CE_RESTART removed=1 untouched=1'), response)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
|
||||
export default {
|
||||
name: 'iris-craftengine-native',
|
||||
description: 'Verify deferred native invalidation and loaded neighbor cleanup with real Iris and CraftEngine.',
|
||||
async run(context) {
|
||||
const source = await readFile(path.join(context.server.directory, '.server-source'), 'utf8')
|
||||
context.expect(/^isolated=true\s*$/m.test(source), 'CraftEngine regression requires an isolated instance')
|
||||
await context.step('create and enter a real Iris world with CraftEngine content', async () => {
|
||||
const response = await context.command('/iriscenative prepare', /CE_READY|CE_FAILED/, 180000)
|
||||
context.expect(response.includes('CE_READY'), response)
|
||||
})
|
||||
await context.step('native writes remove pending block, chest, and flatten furniture records', async () => {
|
||||
const response = await context.command('/iriscenative pending', /CE_PENDING|CE_FAILED/, 60000)
|
||||
context.expect(response.includes('CE_PENDING intact=1 air=1 wall=1 invalidated=1 flatten=1 chest=1 furniture=1'), response)
|
||||
await context.waitUntil(() => {
|
||||
const origin = context.bot.entity.position.floored()
|
||||
const cut = context.bot.blockAt(origin.set(4, 180, 8))
|
||||
const wall = context.bot.blockAt(origin.set(6, 180, 8))
|
||||
const invalidated = context.bot.blockAt(origin.set(8, 180, 8))
|
||||
const chest = context.bot.blockAt(origin.set(14, 180, 8))
|
||||
return cut?.name === 'air' && wall?.name === 'cobblestone'
|
||||
&& invalidated?.name === 'air' && chest?.name === 'cobblestone'
|
||||
}, { label: 'Client observes retained native air and walls', timeoutMs: 30000, intervalMs: 100 })
|
||||
})
|
||||
await context.step('regeneration removes target furniture and preserves neighbor ownership', async () => {
|
||||
const ready = await context.command('/iriscenative neighbor', /CE_NEIGHBOR_READY|CE_FAILED/, 30000)
|
||||
context.expect(ready.includes('CE_NEIGHBOR_READY chunk=2,0 furniture=3 target=1'), ready)
|
||||
const regenerated = await context.command('/iris developer regen radius=0', /Regen complete:|Regen failed:/, 90000)
|
||||
context.expect(regenerated.includes('Regen complete:'), regenerated)
|
||||
const response = await context.command('/iriscenative loaded', /CE_LOADED|CE_FAILED/, 30000)
|
||||
context.expect(response.includes('CE_LOADED removed=1 untouched=1 replacement=1 wall=1 target=1'), response)
|
||||
})
|
||||
await context.step('imported snow features replace generated furniture records', async () => {
|
||||
const response = await context.command('/iriscenative features', /CE_FEATURES|CE_FAILED/, 180000)
|
||||
context.expect(response.includes('CE_FEATURES snow=256 pending=0 furniture=0'), response)
|
||||
await context.waitUntil(() => {
|
||||
const position = context.bot.entity.position.floored()
|
||||
for (let x = 0; x < 16; x++) {
|
||||
for (let z = 0; z < 16; z++) {
|
||||
if (context.bot.blockAt(position.set(x, 97, z))?.name !== 'snow') return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}, { label: 'Client observes 256 imported snow layers', timeoutMs: 30000, intervalMs: 100 })
|
||||
})
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -57,6 +57,8 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedChunkGenera
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeGeneratorContext;
|
||||
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedStructureStage;
|
||||
import art.arcane.iris.world.safeguard.GenerationRefusalNotice;
|
||||
@@ -829,12 +831,12 @@ public final class IrisModdedChunkGenerator implements NativeGeneratorOwner, Nat
|
||||
importedFeatures.invalidate();
|
||||
}
|
||||
|
||||
public void updateRegeneratedChunk(int chunkX, int chunkZ) {
|
||||
public void updateRegeneratedChunk(Mantle<Matter> mantle, int chunkX, int chunkZ) {
|
||||
ModdedChunkUpdateService service = ModdedEngineBootstrap.services().service(ModdedChunkUpdateService.class);
|
||||
if (service == null) {
|
||||
throw new IllegalStateException("Iris chunk update service is unavailable during regeneration");
|
||||
}
|
||||
service.updateRegeneratedChunk(engine(), boundLevel(), chunkX, chunkZ);
|
||||
service.updateRegeneratedChunk(engine(), boundLevel(), mantle, chunkX, chunkZ);
|
||||
}
|
||||
|
||||
public NativeBiomeResolver regenBiomeResolver() {
|
||||
|
||||
+12
@@ -2,6 +2,7 @@ package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.volmlib.nativelib.terrain.structure.StructureStagePolicy;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.mantle.DeferredPlacementRecords;
|
||||
import art.arcane.iris.generation.terrain.IrisMaterialPalette;
|
||||
import art.arcane.iris.structure.nativegen.GenerationWritePolicy;
|
||||
import art.arcane.iris.structure.nativegen.IrisStructurePolicy;
|
||||
@@ -13,6 +14,7 @@ import art.arcane.iris.structure.nativegen.StructureLocateSearch;
|
||||
import art.arcane.iris.structure.placement.IrisStructureLocator;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedStructureStage;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureVolumeSource;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockChangeSet;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockPositionPredicate;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.nativelib.terrain.structure.StructureLocateSearchAccess;
|
||||
@@ -107,6 +109,16 @@ final class ModdedNativeStructurePolicy implements NativeModdedStructureStage.Po
|
||||
context.getDimensionStackContext(), context.getMinHeight());
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeBlockChangeSet trackBlockChanges(Engine context, NativeBlockChangeSet.Bounds bounds) {
|
||||
return DeferredPlacementRecords.track(context, bounds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidateBlockChanges(Engine context, NativeBlockChangeSet changes) {
|
||||
DeferredPlacementRecords.invalidate(context, changes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IntBinaryOperator surfaceHeight(Engine context) {
|
||||
return (x, z) -> Engine.hostHeight(context, x, z, true) + context.getMinHeight();
|
||||
|
||||
+25
-4
@@ -19,6 +19,8 @@
|
||||
package art.arcane.iris.modded.api;
|
||||
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.mantle.CustomBlockOwnership;
|
||||
import art.arcane.iris.integration.Identifier;
|
||||
import art.arcane.iris.modded.ModdedIrisLog;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.ModdedBlockState;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeBlockPlacement;
|
||||
@@ -297,11 +299,11 @@ public final class ModdedCustomContentRegistry {
|
||||
* consulted for that position. An unparseable key or no matching provider is logged and skipped. Called on the
|
||||
* server thread with the chunk loaded.
|
||||
*/
|
||||
public static void processBlockPlacement(Engine engine, String key, Supplier<NativeBlockPlacement> placement) {
|
||||
public static CustomBlockOwnership.Entry processBlockPlacement(Engine engine, String key, Supplier<NativeBlockPlacement> placement) {
|
||||
String base = parseIdentifier(key);
|
||||
if (base == null) {
|
||||
ModdedIrisLog.warn("Iris deferred custom block placement rejected invalid id {}", key);
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
Map<String, String> state = parseState(key);
|
||||
for (ModdedDataProvider provider : PROVIDERS) {
|
||||
@@ -309,14 +311,33 @@ public final class ModdedCustomContentRegistry {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
provider.processBlockPlacement(new ModdedBlockPlacementContext(
|
||||
String token = provider.processBlockPlacement(new ModdedBlockPlacementContext(
|
||||
engine, placement.get(), base, state));
|
||||
int properties = key.indexOf('[');
|
||||
String qualifiedKey = base.replace(':', '/') + (properties < 0 ? "" : key.substring(properties));
|
||||
return token == null ? null : new CustomBlockOwnership.Entry(
|
||||
new Identifier(provider.modId(), qualifiedKey), token, false);
|
||||
} catch (Throwable error) {
|
||||
ModdedIrisLog.error("Iris custom content provider '{}' failed post-placement for {}", provider.modId(), key, error);
|
||||
}
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
ModdedIrisLog.warn("Iris deferred custom block placement has no provider for {}", key);
|
||||
return null;
|
||||
}
|
||||
|
||||
public static boolean removeGenerated(Engine engine, CustomBlockOwnership.Entry entry,
|
||||
Supplier<NativeBlockPlacement> placement) {
|
||||
for (ModdedDataProvider provider : PROVIDERS) {
|
||||
if (provider.modId().equals(entry.identifier().namespace()) && provider.isReady()) {
|
||||
String storedKey = entry.identifier().key();
|
||||
int separator = storedKey.indexOf('/');
|
||||
String key = storedKey.substring(0, separator) + ":" + storedKey.substring(separator + 1);
|
||||
return provider.removeGenerated(new ModdedBlockPlacementContext(
|
||||
engine, placement.get(), parseIdentifier(key), parseState(key)), entry.token());
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+6
-1
@@ -95,7 +95,12 @@ public interface ModdedDataProvider {
|
||||
* Called on the server thread, once per deferred position, by the first provider that claims the identifier -
|
||||
* later providers are not consulted for that position. Only fires for states returned as deferred.
|
||||
*/
|
||||
default void processBlockPlacement(ModdedBlockPlacementContext context) {
|
||||
default String processBlockPlacement(ModdedBlockPlacementContext context) {
|
||||
return null;
|
||||
}
|
||||
|
||||
default boolean removeGenerated(ModdedBlockPlacementContext context, String token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+19
-2
@@ -22,6 +22,7 @@ import art.arcane.iris.modded.ModdedIrisLog;
|
||||
import art.arcane.iris.world.WorldMaintenance;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.mantle.EngineMantle;
|
||||
import art.arcane.iris.generation.mantle.DeferredPlacementRecords;
|
||||
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||
import art.arcane.iris.modded.ModdedBlockBuffer;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
@@ -36,6 +37,9 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeCommandSource;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeChunkRegeneration;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeProtocolPlayer;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
@@ -110,10 +114,12 @@ public final class ModdedRegen {
|
||||
|
||||
private void resetMantleMargin() {
|
||||
EngineMantle engineMantle = engine.getMantle();
|
||||
Mantle<Matter> mantle = engineMantle.getMantle();
|
||||
int margin = radius + Math.max(engineMantle.getRadius(), engineMantle.getRealRadius()) + 1;
|
||||
for (int dx = -margin; dx <= margin; dx++) {
|
||||
for (int dz = -margin; dz <= margin; dz++) {
|
||||
engineMantle.getMantle().deleteChunk(centerX + dx, centerZ + dz);
|
||||
DeferredPlacementRecords.resetMantleChunk(mantle, centerX + dx, centerZ + dz,
|
||||
Math.abs(dx) <= radius && Math.abs(dz) <= radius);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -199,10 +205,21 @@ public final class ModdedRegen {
|
||||
}
|
||||
|
||||
private void apply(int chunkX, int chunkZ, ModdedBlockBuffer blocks, Hunk<NativeBiome> biomes) {
|
||||
Mantle<Matter> mantle = engine.getMantle().getMantle();
|
||||
regeneration.apply(new NativeChunkRegeneration.Replacement(chunkX, chunkZ,
|
||||
engine.getMinHeight(), engine.getMaxHeight() - engine.getMinHeight(),
|
||||
blocks::rawOrNull, generator.regenBiomeResolver()),
|
||||
() -> generator.updateRegeneratedChunk(chunkX, chunkZ));
|
||||
() -> completeReplacement(mantle, chunkX, chunkZ));
|
||||
}
|
||||
|
||||
private void completeReplacement(Mantle<Matter> mantle, int chunkX, int chunkZ) {
|
||||
MantleChunk<Matter> records = mantle.useChunk(chunkX, chunkZ);
|
||||
try {
|
||||
DeferredPlacementRecords.invalidateCustomBlockOwnership(records);
|
||||
} finally {
|
||||
records.release();
|
||||
}
|
||||
generator.updateRegeneratedChunk(mantle, chunkX, chunkZ);
|
||||
}
|
||||
|
||||
private void ok(String message) {
|
||||
|
||||
+88
-23
@@ -28,13 +28,17 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeBlockProperties;
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.studio.view.PregeneratorJob;
|
||||
import art.arcane.iris.integration.Identifier;
|
||||
import art.arcane.iris.generation.mantle.CustomBlockOwnership;
|
||||
import art.arcane.iris.generation.mantle.DeferredPlacementRecords;
|
||||
import art.arcane.iris.generation.cache.Cache;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.block.TileData;
|
||||
import art.arcane.iris.generation.decoration.IrisProceduralBlocks;
|
||||
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||
import art.arcane.iris.modded.ModdedBlockResolution;
|
||||
import art.arcane.iris.modded.ModdedLootApplier;
|
||||
import art.arcane.iris.modded.ModdedTileData;
|
||||
import art.arcane.iris.modded.api.ModdedBlockData;
|
||||
import art.arcane.iris.modded.api.ModdedCustomContentRegistry;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeBlockPlacement;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
@@ -50,6 +54,8 @@ import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
@@ -162,8 +168,8 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
|
||||
updatePassChunk(engine, level, (int) chunkKey, (int) (chunkKey >> 32)));
|
||||
}
|
||||
|
||||
public void updateRegeneratedChunk(Engine engine, NativeWorld level, int chunkX, int chunkZ) {
|
||||
updateChunk(engine, level, chunkX, chunkZ, false);
|
||||
public void updateRegeneratedChunk(Engine engine, NativeWorld level, Mantle<Matter> mantle, int chunkX, int chunkZ) {
|
||||
updateChunk(engine, level, mantle, chunkX, chunkZ, false);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -172,7 +178,7 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
|
||||
*/
|
||||
private void updatePassChunk(Engine engine, NativeWorld level, int chunkX, int chunkZ) {
|
||||
try {
|
||||
updateChunk(engine, level, chunkX, chunkZ, true);
|
||||
updateChunk(engine, level, engine.getMantle().getMantle(), chunkX, chunkZ, true);
|
||||
} catch (Throwable failure) {
|
||||
if (engine.isClosed() || engine.getMantle().getMantle().isClosed()) {
|
||||
throw failure;
|
||||
@@ -184,7 +190,8 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
|
||||
}
|
||||
}
|
||||
|
||||
private void updateChunk(Engine engine, NativeWorld level, int chunkX, int chunkZ, boolean requireNeighbors) {
|
||||
private void updateChunk(Engine engine, NativeWorld level, Mantle<Matter> mantle,
|
||||
int chunkX, int chunkZ, boolean requireNeighbors) {
|
||||
if (requireNeighbors) {
|
||||
for (int x = -1; x <= 1; x++) {
|
||||
for (int z = -1; z <= 1; z++) {
|
||||
@@ -195,11 +202,11 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
|
||||
}
|
||||
}
|
||||
|
||||
Mantle<Matter> mantle = engine.getMantle().getMantle();
|
||||
if (!mantle.isChunkLoaded(chunkX, chunkZ)) {
|
||||
warmupMantleChunk(mantle, chunkX, chunkZ);
|
||||
return;
|
||||
}
|
||||
DeferredPlacementRecords.removeInvalidated(engine, mantle, chunkX, chunkZ);
|
||||
if (mantle.hasFlag(chunkX, chunkZ, MantleFlag.ETCHED)) {
|
||||
return;
|
||||
}
|
||||
@@ -225,30 +232,51 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
|
||||
int baseX = chunkX << 4;
|
||||
int baseZ = chunkZ << 4;
|
||||
int minHeight = engine.getWorld().minHeight();
|
||||
materializeDeferredSlice(chunk, TileWrapper.class, () ->
|
||||
chunk.iterate(TileWrapper.class, (Integer x, Integer yf, Integer z, TileWrapper v) -> {
|
||||
int y = yf + minHeight;
|
||||
if (y < level.minHeight() || y >= level.maxHeight() - 1) {
|
||||
return;
|
||||
}
|
||||
applyTile(level, baseX + (x & 15), y, baseZ + (z & 15), v.getData());
|
||||
}));
|
||||
materializeDeferredSlice(chunk, TileWrapper.class, () -> {
|
||||
for (DeferredPlacementRecords.Pending<TileWrapper> record : DeferredPlacementRecords.snapshot(chunk, TileWrapper.class)) {
|
||||
if (!DeferredPlacementRecords.isPending(chunk, TileWrapper.class, record)) {
|
||||
continue;
|
||||
}
|
||||
NativeBlockPoint position = record.position();
|
||||
int y = position.y() + minHeight;
|
||||
if (y < level.minHeight() || y >= level.maxHeight() - 1) {
|
||||
continue;
|
||||
}
|
||||
applyTile(level, baseX + (position.x() & 15), y, baseZ + (position.z() & 15), record.value().getData());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void runCustomPass(Engine engine, NativeWorld level, int chunkX, int chunkZ, MantleChunk<Matter> chunk) {
|
||||
int baseX = chunkX << 4;
|
||||
int baseZ = chunkZ << 4;
|
||||
int minHeight = engine.getWorld().minHeight();
|
||||
materializeDeferredSlice(chunk, Identifier.class, () ->
|
||||
chunk.iterate(Identifier.class, (Integer x, Integer yf, Integer z, Identifier identifier) -> {
|
||||
int y = yf + minHeight;
|
||||
if (y < level.minHeight() || y >= level.maxHeight() - 1) {
|
||||
return;
|
||||
}
|
||||
NativeBlockPoint position = new NativeBlockPoint(baseX + (x & 15), y, baseZ + (z & 15));
|
||||
ModdedCustomContentRegistry.processBlockPlacement(engine, identifier.toString(),
|
||||
() -> NativeWorldMaintenance.placement(level, position));
|
||||
}));
|
||||
StaleCarriers stale = new StaleCarriers();
|
||||
materializeDeferredSlice(chunk, Identifier.class, () -> {
|
||||
for (DeferredPlacementRecords.Pending<Identifier> record : DeferredPlacementRecords.snapshot(chunk, Identifier.class)) {
|
||||
if (!DeferredPlacementRecords.isPending(chunk, Identifier.class, record)) {
|
||||
continue;
|
||||
}
|
||||
NativeBlockPoint local = record.position();
|
||||
int y = local.y() + minHeight;
|
||||
if (y < level.minHeight() || y >= level.maxHeight() - 1) {
|
||||
continue;
|
||||
}
|
||||
int blockX = baseX + (local.x() & 15);
|
||||
int blockZ = baseZ + (local.z() & 15);
|
||||
String key = record.value().toString();
|
||||
if (stale.carrierReplaced(key, level.getBlock(blockX, y, blockZ))) {
|
||||
continue;
|
||||
}
|
||||
NativeBlockPoint position = new NativeBlockPoint(blockX, y, blockZ);
|
||||
CustomBlockOwnership.Entry ownership = ModdedCustomContentRegistry.processBlockPlacement(engine, key,
|
||||
() -> NativeWorldMaintenance.placement(level, position));
|
||||
if (ownership != null) {
|
||||
DeferredPlacementRecords.remember(engine, chunk, local, ownership);
|
||||
}
|
||||
}
|
||||
});
|
||||
stale.report(level.name(), chunkX, chunkZ);
|
||||
}
|
||||
|
||||
static void materializeDeferredSlice(MantleChunk<Matter> chunk, Class<?> sliceType, Runnable materializer) {
|
||||
@@ -386,4 +414,41 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
|
||||
NativeWorldMaintenance.refresh(level, pos);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class StaleCarriers {
|
||||
private static final String UNCHECKED = "";
|
||||
|
||||
private final Map<String, String> carriers = new HashMap<>();
|
||||
private int skipped;
|
||||
|
||||
private boolean carrierReplaced(String key, NativeBlockState live) {
|
||||
String carrier = carriers.get(key);
|
||||
if (carrier == null) {
|
||||
carrier = carrierKey(key);
|
||||
carriers.put(key, carrier);
|
||||
}
|
||||
if (carrier.equals(UNCHECKED) || carrier.equals(IrisProceduralBlocks.materialKey(live))
|
||||
|| ((carrier.equals("minecraft:fire") || carrier.equals("minecraft:soul_fire")) && live.isAir())) {
|
||||
return false;
|
||||
}
|
||||
skipped++;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void report(String levelName, int chunkX, int chunkZ) {
|
||||
if (skipped == 0) {
|
||||
return;
|
||||
}
|
||||
IrisLogging.debug("Skipped %d stale custom block records at chunk %d,%d in %s",
|
||||
skipped, chunkX, chunkZ, levelName);
|
||||
}
|
||||
|
||||
private static String carrierKey(String key) {
|
||||
ModdedBlockData resolved = ModdedCustomContentRegistry.resolveBlock(key);
|
||||
if (resolved == null || resolved.state().isAir()) {
|
||||
return UNCHECKED;
|
||||
}
|
||||
return IrisProceduralBlocks.materialKey(resolved.state());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+10
@@ -28,6 +28,10 @@ import art.arcane.iris.modded.ModdedSavedTerrainCapture;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import art.arcane.iris.generation.runtime.EnginePlatformHooks;
|
||||
import art.arcane.iris.generation.mantle.CustomBlockOwnership;
|
||||
import art.arcane.iris.modded.api.ModdedCustomContentRegistry;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockPoint;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldMaintenance;
|
||||
import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.generation.terrain.IrisDimensionRuntimeContract;
|
||||
@@ -72,6 +76,12 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
|
||||
private volatile ExecutorService executor;
|
||||
private long lastPollAt;
|
||||
|
||||
@Override
|
||||
public boolean removeCustomBlock(Engine engine, NativeBlockPoint position, CustomBlockOwnership.Entry entry) {
|
||||
return ModdedCustomContentRegistry.removeGenerated(engine, entry,
|
||||
() -> NativeWorldMaintenance.placement(engine.getWorld().platformWorld(), position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> flushSavedTerrainCapture(Engine engine) {
|
||||
return ModdedSavedTerrainCapture.flush(engine);
|
||||
|
||||
+41
-6
@@ -3,6 +3,7 @@ package art.arcane.iris.modded;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.ModdedNativeStructureWorldgenAccess;
|
||||
|
||||
import art.arcane.iris.structure.placement.IrisStructureTerrain;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockChangeSet;
|
||||
import art.arcane.volmlib.util.structure.StructureTerrainMode;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructurePostProcessor;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.terrain.NativeStructureTerrainIntegrator;
|
||||
@@ -230,8 +231,9 @@ public class ModdedNativeStructureWorldgenAccessTest {
|
||||
recording.world(), generationCenter,
|
||||
(x, z) -> SURFACE_FIRST_FREE_Y,
|
||||
(x, z) -> FLOOR_FIRST_FREE_Y,
|
||||
true,
|
||||
position -> false);
|
||||
true, true,
|
||||
position -> false,
|
||||
NativeBlockChangeSet.disabled());
|
||||
int x = generationCenter.getMiddleBlockX();
|
||||
int z = generationCenter.getMiddleBlockZ();
|
||||
|
||||
@@ -248,8 +250,9 @@ public class ModdedNativeStructureWorldgenAccessTest {
|
||||
recording.world(), generationCenter(),
|
||||
(x, z) -> SURFACE_FIRST_FREE_Y,
|
||||
(x, z) -> FLOOR_FIRST_FREE_Y,
|
||||
false,
|
||||
position::equals);
|
||||
false, true,
|
||||
position::equals,
|
||||
NativeBlockChangeSet.disabled());
|
||||
|
||||
assertSame(Blocks.DIRT, access.getBlockState(position).getBlock());
|
||||
assertFalse(access.ensureCanWrite(position));
|
||||
@@ -270,6 +273,37 @@ public class ModdedNativeStructureWorldgenAccessTest {
|
||||
assertEquals(1, recording.mutations);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptedWritesAndReportedChangesAreTrackedOutsideProtectedCells() {
|
||||
RecordingDelegate recording = new RecordingDelegate();
|
||||
BlockPos protectedCell = generationCenter().getMiddleBlockPosition(70);
|
||||
WorldGenLevel delegate = recording.world();
|
||||
NativeBlockChangeSet changes = NativeBlockChangeSet.create(
|
||||
ModdedNativeStructureWorldgenAccess.changeBounds(delegate, generationCenter()));
|
||||
ModdedNativeStructureWorldgenAccess access = ModdedNativeStructureWorldgenAccess.create(
|
||||
delegate, generationCenter(),
|
||||
(x, z) -> SURFACE_FIRST_FREE_Y,
|
||||
(x, z) -> FLOOR_FIRST_FREE_Y,
|
||||
false, true,
|
||||
protectedCell::equals,
|
||||
changes);
|
||||
BlockPos written = protectedCell.above();
|
||||
BlockPos removed = protectedCell.east();
|
||||
BlockPos reported = protectedCell.above(2);
|
||||
|
||||
assertFalse(access.setBlock(protectedCell, Blocks.STONE.defaultBlockState(), 2));
|
||||
assertTrue(access.setBlock(written, Blocks.STONE.defaultBlockState(), 2));
|
||||
assertTrue(access.removeBlock(removed, false));
|
||||
access.recordBlockChange(reported.getX(), reported.getY(), reported.getZ());
|
||||
access.recordBlockChange(protectedCell.getX(), protectedCell.getY(), protectedCell.getZ());
|
||||
|
||||
assertEquals(3, changes.size());
|
||||
assertTrue(changes.contains(written.getX(), written.getY(), written.getZ()));
|
||||
assertTrue(changes.contains(removed.getX(), removed.getY(), removed.getZ()));
|
||||
assertTrue(changes.contains(reported.getX(), reported.getY(), reported.getZ()));
|
||||
assertFalse(changes.contains(protectedCell.getX(), protectedCell.getY(), protectedCell.getZ()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void statuslessChunkReadsPreserveWorldgenDelegateSemantics() {
|
||||
RecordingDelegate recording = new RecordingDelegate();
|
||||
@@ -319,8 +353,9 @@ public class ModdedNativeStructureWorldgenAccessTest {
|
||||
recording.world(), generationCenter(),
|
||||
(x, z) -> SURFACE_FIRST_FREE_Y,
|
||||
(x, z) -> FLOOR_FIRST_FREE_Y,
|
||||
false,
|
||||
position -> false);
|
||||
false, true,
|
||||
position -> false,
|
||||
NativeBlockChangeSet.disabled());
|
||||
}
|
||||
|
||||
private static ChunkPos generationCenter() {
|
||||
|
||||
+32
@@ -1,5 +1,9 @@
|
||||
package art.arcane.iris.modded.api;
|
||||
|
||||
import art.arcane.iris.generation.mantle.CustomBlockOwnership;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.integration.Identifier;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeBlockPlacement;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeEntityRuntime;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeSpawnedEntity;
|
||||
import net.minecraft.SharedConstants;
|
||||
@@ -9,6 +13,7 @@ import org.junit.Test;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
@@ -20,6 +25,33 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ModdedCustomContentRegistryTest {
|
||||
@Test
|
||||
public void removalReturnsToOriginalProviderWithTokenAndPlacementState() {
|
||||
ModdedDataProvider provider = mock(ModdedDataProvider.class);
|
||||
Engine engine = mock(Engine.class);
|
||||
NativeBlockPlacement placement = mock(NativeBlockPlacement.class);
|
||||
ModdedBlockPlacementContext context = new ModdedBlockPlacementContext(
|
||||
engine, placement, "provider_test:furniture/chair", Map.of("variant", "oak"));
|
||||
when(provider.modId()).thenReturn("ownership_test");
|
||||
when(provider.isReady()).thenReturn(true);
|
||||
when(provider.isValidProvider("provider_test:furniture/chair", ModdedDataType.BLOCK)).thenReturn(true);
|
||||
when(provider.processBlockPlacement(context)).thenReturn("entity-token");
|
||||
when(provider.removeGenerated(context, "entity-token")).thenReturn(true);
|
||||
ModdedCustomContentRegistry.Discovery discovery = ModdedCustomContentRegistry.discover(List.of(provider));
|
||||
try {
|
||||
CustomBlockOwnership.Entry ownership = ModdedCustomContentRegistry.processBlockPlacement(engine,
|
||||
"provider_test:furniture/chair[variant=oak]", () -> placement);
|
||||
|
||||
assertEquals(new CustomBlockOwnership.Entry(
|
||||
Identifier.fromString("ownership_test:provider_test/furniture/chair[variant=oak]"),
|
||||
"entity-token", false), ownership);
|
||||
assertTrue(ModdedCustomContentRegistry.removeGenerated(engine, ownership, () -> placement));
|
||||
verify(provider).removeGenerated(context, "entity-token");
|
||||
} finally {
|
||||
discovery.rollback();
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeClass
|
||||
public static void bootstrapRegistries() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
|
||||
+244
@@ -0,0 +1,244 @@
|
||||
package art.arcane.iris.modded.service;
|
||||
|
||||
import art.arcane.iris.generation.mantle.EngineMantle;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.EngineMetrics;
|
||||
import art.arcane.iris.integration.Identifier;
|
||||
import art.arcane.iris.modded.api.ModdedBlockData;
|
||||
import art.arcane.iris.modded.api.ModdedBlockPlacementContext;
|
||||
import art.arcane.iris.modded.api.ModdedCustomContentRegistry;
|
||||
import art.arcane.iris.modded.api.ModdedDataProvider;
|
||||
import art.arcane.iris.modded.api.ModdedDataType;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.world.IrisWorld;
|
||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.ModdedBlockState;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||
import art.arcane.volmlib.util.function.Consumer4;
|
||||
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleDataAdapter;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleHooks;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.NoteBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ModdedChunkUpdateServiceCarrierTest {
|
||||
private static final String MOD_ID = "iris_carrier_guard_test";
|
||||
private static final String LAMP = MOD_ID + ":lamp";
|
||||
private static final String CHAIR = MOD_ID + ":chair";
|
||||
private static final String FIRE = MOD_ID + ":fire";
|
||||
private static final String SOUL_FIRE = MOD_ID + ":soul_fire";
|
||||
private static final List<BlockPos> PLACED = new CopyOnWriteArrayList<>();
|
||||
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraftRegistries() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
ModdedCustomContentRegistry.register(new CarrierProvider());
|
||||
}
|
||||
|
||||
@Before
|
||||
public void clearPlacements() {
|
||||
PLACED.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void staleRecordsAreSkippedWhileIntactAndAirCarrierRecordsArePlaced() {
|
||||
RecordMantleChunk chunk = new RecordMantleChunk(List.of(
|
||||
new Placed(1, LAMP),
|
||||
new Placed(2, LAMP),
|
||||
new Placed(3, LAMP),
|
||||
new Placed(4, CHAIR)));
|
||||
NativeWorld level = level(Map.of(
|
||||
1, Blocks.NOTE_BLOCK.defaultBlockState().setValue(NoteBlock.NOTE, 7),
|
||||
2, Blocks.AIR.defaultBlockState(),
|
||||
3, Blocks.COBBLESTONE.defaultBlockState(),
|
||||
4, Blocks.COBBLESTONE.defaultBlockState()));
|
||||
List<String> debug = new ArrayList<>();
|
||||
Engine engine = engine(chunk);
|
||||
|
||||
try (MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
|
||||
logging.when(() -> IrisLogging.debug(anyString(), any(Object[].class))).thenAnswer((InvocationOnMock invocation) -> {
|
||||
Object[] arguments = invocation.getArguments();
|
||||
debug.add(String.format((String) arguments[0], Arrays.copyOfRange(arguments, 1, arguments.length)));
|
||||
return null;
|
||||
});
|
||||
|
||||
new ModdedChunkUpdateService().updateRegeneratedChunk(engine, level, engine.getMantle().getMantle(), 0, 0);
|
||||
}
|
||||
|
||||
assertEquals(List.of(new BlockPos(1, 6, 1), new BlockPos(4, 6, 4)), PLACED);
|
||||
assertTrue(chunk.isFlagged(MantleFlag.ETCHED));
|
||||
assertTrue(chunk.customDeleted());
|
||||
assertEquals(1, debug.size());
|
||||
assertTrue(debug.getFirst(), debug.getFirst().contains("2 stale"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void extinguishedFireCarriersArePlacedWhileCoveredCarriersAreSkipped() {
|
||||
RecordMantleChunk chunk = new RecordMantleChunk(List.of(
|
||||
new Placed(1, FIRE), new Placed(2, SOUL_FIRE),
|
||||
new Placed(3, FIRE), new Placed(4, SOUL_FIRE)));
|
||||
NativeWorld level = level(Map.of(
|
||||
1, Blocks.AIR.defaultBlockState(),
|
||||
2, Blocks.CAVE_AIR.defaultBlockState(),
|
||||
3, Blocks.COBBLESTONE.defaultBlockState(),
|
||||
4, Blocks.COBBLESTONE.defaultBlockState()));
|
||||
Engine engine = engine(chunk);
|
||||
|
||||
new ModdedChunkUpdateService().updateRegeneratedChunk(engine, level, engine.getMantle().getMantle(), 0, 0);
|
||||
|
||||
assertEquals(List.of(new BlockPos(1, 6, 1), new BlockPos(2, 6, 2)), PLACED);
|
||||
assertTrue(chunk.customDeleted());
|
||||
}
|
||||
|
||||
private static NativeWorld level(Map<Integer, BlockState> live) {
|
||||
ServerLevel serverLevel = mock(ServerLevel.class);
|
||||
when(serverLevel.getBlockState(any(BlockPos.class))).thenReturn(Blocks.STONE.defaultBlockState());
|
||||
NativeWorld level = mock(NativeWorld.class);
|
||||
when(level.name()).thenReturn("example:carriers");
|
||||
when(level.minHeight()).thenReturn(-64);
|
||||
when(level.maxHeight()).thenReturn(320);
|
||||
when(level.nativeHandle()).thenReturn(serverLevel);
|
||||
for (Map.Entry<Integer, BlockState> entry : live.entrySet()) {
|
||||
int offset = entry.getKey();
|
||||
when(level.getBlock(offset, 6, offset)).thenReturn(ModdedBlockState.of(entry.getValue(), null));
|
||||
}
|
||||
return level;
|
||||
}
|
||||
|
||||
private static Engine engine(RecordMantleChunk chunk) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Mantle<Matter> mantle = mock(Mantle.class);
|
||||
when(mantle.isChunkLoaded(0, 0)).thenReturn(true);
|
||||
when(mantle.hasFlag(0, 0, MantleFlag.ETCHED)).thenAnswer((InvocationOnMock invocation) -> chunk.isFlagged(MantleFlag.ETCHED));
|
||||
when(mantle.useChunk(0, 0)).thenAnswer((InvocationOnMock invocation) -> chunk.use());
|
||||
IrisWorld world = mock(IrisWorld.class);
|
||||
when(world.minHeight()).thenReturn(-64);
|
||||
EngineMantle engineMantle = mock(EngineMantle.class);
|
||||
when(engineMantle.getMantle()).thenReturn(mantle);
|
||||
Engine engine = mock(Engine.class);
|
||||
when(engine.getWorld()).thenReturn(world);
|
||||
when(engine.getMetrics()).thenReturn(new EngineMetrics(8));
|
||||
when(engine.getMantle()).thenReturn(engineMantle);
|
||||
return engine;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static MantleDataAdapter<Matter> emptyAdapter() {
|
||||
return (MantleDataAdapter<Matter>) Proxy.newProxyInstance(
|
||||
MantleDataAdapter.class.getClassLoader(),
|
||||
new Class<?>[]{MantleDataAdapter.class},
|
||||
(proxy, method, arguments) -> {
|
||||
throw new UnsupportedOperationException(method.getName());
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private record Placed(int offset, String key) {
|
||||
}
|
||||
|
||||
private static final class RecordMantleChunk extends MantleChunk<Matter> {
|
||||
private final List<Placed> records;
|
||||
private boolean customDeleted;
|
||||
|
||||
private RecordMantleChunk(List<Placed> records) {
|
||||
super(1, 0, 0, emptyAdapter(), MantleHooks.NONE);
|
||||
this.records = records;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> void iterate(Class<T> type, Consumer4<Integer, Integer, Integer, T> iterator) {
|
||||
if (type != Identifier.class) {
|
||||
return;
|
||||
}
|
||||
for (Placed record : records) {
|
||||
iterator.accept(record.offset(), 70, record.offset(), type.cast(Identifier.fromString(record.key())));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T get(int x, int y, int z, Class<T> type) {
|
||||
if (type == Identifier.class && y == 70 && x == z) {
|
||||
for (Placed record : records) {
|
||||
if (record.offset() == x) {
|
||||
return type.cast(Identifier.fromString(record.key()));
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteSlices(Class<?> type) {
|
||||
if (type == Identifier.class) {
|
||||
customDeleted = true;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean customDeleted() {
|
||||
return customDeleted;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class CarrierProvider implements ModdedDataProvider {
|
||||
@Override
|
||||
public String modId() {
|
||||
return MOD_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> getTypes(ModdedDataType type) {
|
||||
return type == ModdedDataType.BLOCK ? List.of(LAMP, CHAIR, FIRE, SOUL_FIRE) : List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValidProvider(String id, ModdedDataType type) {
|
||||
return type == ModdedDataType.BLOCK && getTypes(type).contains(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModdedBlockData getBlockData(String blockId, Map<String, String> state) {
|
||||
BlockState carrier = switch (blockId) {
|
||||
case CHAIR -> Blocks.AIR.defaultBlockState();
|
||||
case FIRE -> Blocks.FIRE.defaultBlockState();
|
||||
case SOUL_FIRE -> Blocks.SOUL_FIRE.defaultBlockState();
|
||||
default -> Blocks.NOTE_BLOCK.defaultBlockState();
|
||||
};
|
||||
return ModdedBlockData.deferred(ModdedBlockState.of(carrier, null));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String processBlockPlacement(ModdedBlockPlacementContext context) {
|
||||
PLACED.add(context.placement().position());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
-5
@@ -68,6 +68,29 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ModdedChunkUpdateServiceTest {
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void regeneratedCleanupUsesSuppliedMantleAfterCurrentRuntimeChanges() {
|
||||
Mantle<Matter> selected = mock(Mantle.class);
|
||||
Mantle<Matter> replacement = mock(Mantle.class);
|
||||
MantleDataAdapter<Matter> adapter = mock(MantleDataAdapter.class);
|
||||
MantleChunk<Matter> chunk = new MantleChunk<>(1, 0, 0, adapter, MantleHooks.NONE);
|
||||
when(selected.isChunkLoaded(0, 0)).thenReturn(true);
|
||||
when(selected.useChunk(0, 0)).thenAnswer(invocation -> chunk.use());
|
||||
when(selected.hasFlag(0, 0, MantleFlag.ETCHED)).thenReturn(true);
|
||||
NativeWorld level = mock(NativeWorld.class);
|
||||
when(level.isChunkLoaded(0, 0)).thenReturn(true);
|
||||
Engine engine = engineWith(replacement);
|
||||
when(engine.getWorld().platformWorld()).thenReturn(level);
|
||||
|
||||
new ModdedChunkUpdateService().updateRegeneratedChunk(engine, level, selected, 0, 0);
|
||||
|
||||
verify(selected).useChunk(0, 0);
|
||||
verify(replacement, never()).isChunkLoaded(anyInt(), anyInt());
|
||||
verify(replacement, never()).useChunk(anyInt(), anyInt());
|
||||
assertFalse(chunk.inUse());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fallingFluidUpdateRetainsLevelEightStateAndRequestsPhysics() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
@@ -154,7 +177,7 @@ public class ModdedChunkUpdateServiceTest {
|
||||
return null;
|
||||
});
|
||||
|
||||
service.updateRegeneratedChunk(engine, level, 0, 0);
|
||||
service.updateRegeneratedChunk(engine, level, engine.getMantle().getMantle(), 0, 0);
|
||||
|
||||
assertFalse(chunk.isFlagged(MantleFlag.ETCHED));
|
||||
assertFalse(chunk.isFlagged(MantleFlag.UPDATE));
|
||||
@@ -162,7 +185,7 @@ public class ModdedChunkUpdateServiceTest {
|
||||
logging.verify(() -> IrisLogging.reportError(any(Throwable.class)), never());
|
||||
logging.verify(() -> IrisLogging.reportError(anyString(), any(Throwable.class)), never());
|
||||
|
||||
service.updateRegeneratedChunk(engine, level, 0, 0);
|
||||
service.updateRegeneratedChunk(engine, level, engine.getMantle().getMantle(), 0, 0);
|
||||
|
||||
assertTrue(chunk.isFlagged(MantleFlag.ETCHED));
|
||||
assertTrue(chunk.deleted());
|
||||
@@ -187,7 +210,7 @@ public class ModdedChunkUpdateServiceTest {
|
||||
});
|
||||
|
||||
assertThrows(SavedBiomeUnavailableException.class,
|
||||
() -> new ModdedChunkUpdateService().updateRegeneratedChunk(engine, level, 0, 0));
|
||||
() -> new ModdedChunkUpdateService().updateRegeneratedChunk(engine, level, engine.getMantle().getMantle(), 0, 0));
|
||||
|
||||
assertFalse(chunk.isFlagged(MantleFlag.ETCHED));
|
||||
assertFalse(chunk.isFlagged(MantleFlag.UPDATE));
|
||||
@@ -215,13 +238,13 @@ public class ModdedChunkUpdateServiceTest {
|
||||
});
|
||||
|
||||
assertSame(failure, assertThrows(IllegalStateException.class,
|
||||
() -> service.updateRegeneratedChunk(engine, level, 0, 0)));
|
||||
() -> service.updateRegeneratedChunk(engine, level, engine.getMantle().getMantle(), 0, 0)));
|
||||
assertFalse(chunk.isFlagged(MantleFlag.UPDATE));
|
||||
assertFalse(chunk.isFlagged(MantleFlag.ETCHED));
|
||||
assertFalse(chunk.deleted());
|
||||
verify(chunk.slice, never()).set(2, 5, 2, null);
|
||||
|
||||
service.updateRegeneratedChunk(engine, level, 0, 0);
|
||||
service.updateRegeneratedChunk(engine, level, engine.getMantle().getMantle(), 0, 0);
|
||||
|
||||
assertTrue(chunk.isFlagged(MantleFlag.ETCHED));
|
||||
assertTrue(chunk.deleted());
|
||||
|
||||
@@ -136,6 +136,18 @@
|
||||
"replacement": "when(level.getMaxBuildHeight()).thenReturn(320);"
|
||||
}
|
||||
],
|
||||
"art/arcane/iris/modded/api/ModdedCustomContentRegistryTest.java": [
|
||||
{
|
||||
"pattern": "\\bResourceLocation\\b",
|
||||
"replacement": "Identifier"
|
||||
}
|
||||
],
|
||||
"art/arcane/iris/modded/service/ModdedChunkUpdateServiceCarrierTest.java": [
|
||||
{
|
||||
"pattern": "\\bResourceLocation\\b",
|
||||
"replacement": "Identifier"
|
||||
}
|
||||
],
|
||||
"art/arcane/volmlib/nativelib/minecraft26_2/modded/NativeSpawnPopulationTest.java": [
|
||||
{
|
||||
"pattern": "when\\(level\\.getMaxY\\(\\)\\)\\.thenReturn\\(320\\);",
|
||||
|
||||
+51
@@ -15,6 +15,7 @@ import net.momirealms.craftengine.bukkit.api.CraftEngineFurniture;
|
||||
import net.momirealms.craftengine.bukkit.api.CraftEngineItems;
|
||||
import net.momirealms.craftengine.bukkit.api.event.CraftEngineReloadEvent;
|
||||
import net.momirealms.craftengine.bukkit.item.BukkitItemDefinition;
|
||||
import net.momirealms.craftengine.bukkit.entity.furniture.BukkitFurniture;
|
||||
import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine;
|
||||
import net.momirealms.craftengine.core.block.BlockDefinition;
|
||||
import net.momirealms.craftengine.core.block.ImmutableBlockState;
|
||||
@@ -24,10 +25,12 @@ import net.momirealms.craftengine.core.entity.furniture.FurnitureVariant;
|
||||
import net.momirealms.craftengine.core.registry.Holder;
|
||||
import net.momirealms.craftengine.core.util.Key;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.junit.Test;
|
||||
@@ -37,6 +40,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.MissingResourceException;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
@@ -48,12 +52,59 @@ import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class CraftEngineDataProviderTest {
|
||||
@Test
|
||||
public void furniturePlacementReturnsExactEntityTokenAndCleanupKeepsReplacement() {
|
||||
FurnitureDefinition definition = furnitureDefinition();
|
||||
Engine engine = mock(Engine.class);
|
||||
SeedManager seeds = mock(SeedManager.class);
|
||||
when(engine.getSeedManager()).thenReturn(seeds);
|
||||
Block block = mock(Block.class);
|
||||
when(block.getLocation()).thenAnswer(invocation -> new Location(null, 0, 70, 0));
|
||||
Chunk chunk = mock(Chunk.class);
|
||||
when(block.getChunk()).thenReturn(chunk);
|
||||
when(chunk.isEntitiesLoaded()).thenReturn(true);
|
||||
Entity original = mock(Entity.class);
|
||||
Entity replacement = mock(Entity.class);
|
||||
UUID originalId = UUID.randomUUID();
|
||||
when(original.getUniqueId()).thenReturn(originalId);
|
||||
when(replacement.getUniqueId()).thenReturn(UUID.randomUUID());
|
||||
when(chunk.getEntities()).thenReturn(new Entity[]{replacement, original});
|
||||
BukkitFurniture placed = mock(BukkitFurniture.class);
|
||||
when(placed.baseEntity()).thenReturn(original);
|
||||
CraftEngineDataProvider provider = new CraftEngineDataProvider();
|
||||
try (MockedStatic<CraftEngineBlocks> blocks = mockStatic(CraftEngineBlocks.class);
|
||||
MockedStatic<CraftEngineFurniture> furniture = mockStatic(CraftEngineFurniture.class)) {
|
||||
furniture.when(() -> CraftEngineFurniture.byId(Key.of("test:chair"))).thenReturn(definition);
|
||||
furniture.when(() -> CraftEngineFurniture.place(any(Location.class), eq(definition), eq("alpha"), eq(false)))
|
||||
.thenReturn(placed);
|
||||
furniture.when(() -> CraftEngineFurniture.remove(original, false, false)).thenReturn(true);
|
||||
|
||||
String token = provider.processUpdate(engine, block, Identifier.fromString("test:chair"));
|
||||
|
||||
assertEquals(originalId.toString(), token);
|
||||
assertTrue(provider.removeGenerated(block, token));
|
||||
furniture.verify(() -> CraftEngineFurniture.remove(original, false, false));
|
||||
furniture.verify(() -> CraftEngineFurniture.remove(replacement, false, false), never());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void furnitureCleanupWaitsForEntitiesToLoad() {
|
||||
Block block = mock(Block.class);
|
||||
Chunk chunk = mock(Chunk.class);
|
||||
when(block.getChunk()).thenReturn(chunk);
|
||||
|
||||
assertFalse(new CraftEngineDataProvider().removeGenerated(block, UUID.randomUUID().toString()));
|
||||
verify(chunk, never()).getEntities();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void startupWaitsForReloadEventAndEmptyLoadedRegistriesAreReady() {
|
||||
CraftEngineDataProvider provider = spy(new CraftEngineDataProvider());
|
||||
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
package art.arcane.iris.platform.generation;
|
||||
|
||||
import art.arcane.iris.generation.block.IrisCustomData;
|
||||
import art.arcane.iris.generation.mantle.CustomBlockOwnership;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.SeedManager;
|
||||
import art.arcane.iris.integration.ExternalDataProvider;
|
||||
import art.arcane.iris.integration.ExternalDataSVC;
|
||||
import art.arcane.iris.integration.Identifier;
|
||||
import art.arcane.iris.integration.data.CraftEngineDataProvider;
|
||||
import art.arcane.iris.platform.bukkit.BukkitBlockResolution;
|
||||
import art.arcane.iris.platform.registry.RegistryUtil;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.testsupport.BukkitTestServer;
|
||||
import art.arcane.iris.world.IrisWorld;
|
||||
import art.arcane.volmlib.util.function.Consumer4;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.matter.MatterSlice;
|
||||
import net.momirealms.craftengine.bukkit.api.CraftEngineBlocks;
|
||||
import net.momirealms.craftengine.bukkit.api.CraftEngineFurniture;
|
||||
import net.momirealms.craftengine.bukkit.api.CraftEngineItems;
|
||||
import net.momirealms.craftengine.bukkit.api.event.CraftEngineReloadEvent;
|
||||
import net.momirealms.craftengine.bukkit.entity.furniture.BukkitFurniture;
|
||||
import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine;
|
||||
import net.momirealms.craftengine.core.block.BlockDefinition;
|
||||
import net.momirealms.craftengine.core.block.ImmutableBlockState;
|
||||
import net.momirealms.craftengine.core.entity.furniture.FurnitureDefinition;
|
||||
import net.momirealms.craftengine.core.entity.furniture.FurnitureVariant;
|
||||
import net.momirealms.craftengine.core.registry.Holder;
|
||||
import net.momirealms.craftengine.core.util.Key;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class CraftEngineStaleRecordTest {
|
||||
@Test
|
||||
public void stalePlacementRecordsAreSkippedAndIntactOnesPlaced() throws Exception {
|
||||
BukkitTestServer.install();
|
||||
BlockDefinition lampDefinition = mock(BlockDefinition.class);
|
||||
ImmutableBlockState lamp = mock(ImmutableBlockState.class);
|
||||
when(lamp.owner()).thenReturn(Holder.direct(lampDefinition));
|
||||
when(lamp.toString()).thenReturn("test:lamp");
|
||||
when(lampDefinition.defaultState()).thenReturn(lamp);
|
||||
FurnitureDefinition chairDefinition = mock(FurnitureDefinition.class);
|
||||
when(chairDefinition.id()).thenReturn(Key.of("test:chair"));
|
||||
when(chairDefinition.variants()).thenReturn(Map.of("alpha", mock(FurnitureVariant.class)));
|
||||
BlockData noteBlock = blockData(Material.NOTE_BLOCK);
|
||||
BlockData air = blockData(Material.AIR);
|
||||
CraftEngineDataProvider provider = new CraftEngineDataProvider();
|
||||
ExternalDataSVC service = new ExternalDataSVC();
|
||||
activate(service, provider);
|
||||
|
||||
Engine engine = mock(Engine.class);
|
||||
when(engine.getWorld()).thenReturn(mock(IrisWorld.class));
|
||||
SeedManager seeds = mock(SeedManager.class);
|
||||
when(engine.getSeedManager()).thenReturn(seeds);
|
||||
World world = mock(World.class);
|
||||
when(world.getMinHeight()).thenReturn(0);
|
||||
when(world.getMaxHeight()).thenReturn(256);
|
||||
Chunk chunk = mock(Chunk.class);
|
||||
when(chunk.getWorld()).thenReturn(world);
|
||||
block(chunk, 1, Material.NOTE_BLOCK);
|
||||
block(chunk, 2, Material.AIR);
|
||||
block(chunk, 3, Material.COBBLESTONE);
|
||||
block(chunk, 4, Material.COBBLESTONE);
|
||||
|
||||
try (MockedStatic<BukkitCraftEngine> craftEngine = mockStatic(BukkitCraftEngine.class);
|
||||
MockedStatic<CraftEngineBlocks> blocks = mockStatic(CraftEngineBlocks.class);
|
||||
MockedStatic<CraftEngineFurniture> furniture = mockStatic(CraftEngineFurniture.class);
|
||||
MockedStatic<CraftEngineItems> items = mockStatic(CraftEngineItems.class);
|
||||
MockedStatic<RegistryUtil> registry = materialRegistry();
|
||||
MockedStatic<BukkitBlockResolution> resolution = mockStatic(BukkitBlockResolution.class);
|
||||
MockedStatic<IrisServices> services = mockStatic(IrisServices.class)) {
|
||||
resolution.when(BukkitBlockResolution::getAir).thenReturn(air);
|
||||
services.when(() -> IrisServices.get(ExternalDataSVC.class)).thenReturn(service);
|
||||
blocks.when(CraftEngineBlocks::loadedBlocks).thenReturn(Map.of(Key.of("test:lamp"), lampDefinition));
|
||||
furniture.when(CraftEngineFurniture::loadedFurniture).thenReturn(Map.of(Key.of("test:chair"), chairDefinition));
|
||||
items.when(CraftEngineItems::loadedItems).thenReturn(Map.of());
|
||||
blocks.when(() -> CraftEngineBlocks.byId(Key.of("test:lamp"))).thenReturn(lampDefinition);
|
||||
blocks.when(() -> CraftEngineBlocks.getBukkitBlockData(lamp)).thenReturn(noteBlock);
|
||||
blocks.when(() -> CraftEngineBlocks.place(any(Location.class), eq(lamp), eq(false))).thenReturn(true);
|
||||
furniture.when(() -> CraftEngineFurniture.byId(Key.of("test:chair"))).thenReturn(chairDefinition);
|
||||
BukkitFurniture placed = mock(BukkitFurniture.class);
|
||||
Entity entity = mock(Entity.class);
|
||||
when(placed.baseEntity()).thenReturn(entity);
|
||||
when(entity.getUniqueId()).thenReturn(UUID.randomUUID());
|
||||
furniture.when(() -> CraftEngineFurniture.place(any(Location.class), eq(chairDefinition), eq("alpha"), eq(false)))
|
||||
.thenReturn(placed);
|
||||
provider.onReload(mock(CraftEngineReloadEvent.class));
|
||||
Identifier lampRecord = generatedRecord(service, "test:lamp");
|
||||
Identifier chairRecord = generatedRecord(service, "test:chair");
|
||||
assertEquals("craftengine:test/lamp", lampRecord.toString());
|
||||
assertEquals("craftengine:test/chair[variant=alpha]", chairRecord.toString());
|
||||
|
||||
EngineBukkitOps.materializeCustomBlocks(engine, chunk, records(
|
||||
new Placed(1, lampRecord),
|
||||
new Placed(2, lampRecord),
|
||||
new Placed(3, lampRecord),
|
||||
new Placed(4, chairRecord)));
|
||||
|
||||
blocks.verify(() -> CraftEngineBlocks.place(any(Location.class), eq(lamp), eq(false)), times(1));
|
||||
blocks.verify(() -> CraftEngineBlocks.place(location(1), lamp, false));
|
||||
blocks.verify(() -> CraftEngineBlocks.place(location(2), lamp, false), never());
|
||||
blocks.verify(() -> CraftEngineBlocks.place(location(3), lamp, false), never());
|
||||
furniture.verify(() -> CraftEngineFurniture.place(location(4), chairDefinition, "alpha", false));
|
||||
}
|
||||
}
|
||||
|
||||
private static MockedStatic<RegistryUtil> materialRegistry() {
|
||||
MockedStatic<RegistryUtil> registry = mockStatic(RegistryUtil.class);
|
||||
registry.when(() -> RegistryUtil.find(Material.class, "grass", "short_grass")).thenReturn(Material.SHORT_GRASS);
|
||||
return registry;
|
||||
}
|
||||
|
||||
private static Identifier generatedRecord(ExternalDataSVC service, String key) {
|
||||
BlockData resolved = service.getBlockData(Identifier.fromString(key)).orElseThrow();
|
||||
return ((IrisCustomData) resolved).getCustom();
|
||||
}
|
||||
|
||||
private static BlockData blockData(Material material) {
|
||||
BlockData data = mock(BlockData.class);
|
||||
when(data.getMaterial()).thenReturn(material);
|
||||
return data;
|
||||
}
|
||||
|
||||
private static Block block(Chunk chunk, int offset, Material type) {
|
||||
Block block = mock(Block.class);
|
||||
when(block.getType()).thenReturn(type);
|
||||
when(block.getX()).thenReturn(offset);
|
||||
when(block.getY()).thenReturn(10);
|
||||
when(block.getZ()).thenReturn(offset);
|
||||
when(block.getLocation()).thenAnswer((InvocationOnMock invocation) -> location(offset));
|
||||
when(chunk.getBlock(offset, 10, offset)).thenReturn(block);
|
||||
return block;
|
||||
}
|
||||
|
||||
private static Location location(int offset) {
|
||||
return new Location(null, offset, 10, offset);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static MantleChunk<Matter> records(Placed... records) {
|
||||
MantleChunk<Matter> chunk = mock(MantleChunk.class);
|
||||
Matter section = mock(Matter.class);
|
||||
MatterSlice<CustomBlockOwnership> ownership = mock(MatterSlice.class);
|
||||
MatterSlice<Identifier> deferred = mock(MatterSlice.class);
|
||||
when(chunk.getOrCreate(anyInt())).thenReturn(section);
|
||||
when(section.<CustomBlockOwnership>slice(CustomBlockOwnership.class)).thenReturn(ownership);
|
||||
when(section.getSlice(Identifier.class)).thenReturn(deferred);
|
||||
for (Placed record : records) {
|
||||
when(chunk.get(record.offset(), 10, record.offset(), Identifier.class)).thenReturn(record.identifier());
|
||||
when(deferred.get(record.offset(), 10, record.offset())).thenReturn(record.identifier());
|
||||
}
|
||||
doAnswer((InvocationOnMock invocation) -> {
|
||||
Consumer4<Integer, Integer, Integer, Identifier> iterator = invocation.getArgument(1);
|
||||
for (Placed record : records) {
|
||||
iterator.accept(record.offset(), 10, record.offset(), record.identifier());
|
||||
}
|
||||
return null;
|
||||
}).when(chunk).iterate(eq(Identifier.class), any());
|
||||
return chunk;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static void activate(ExternalDataSVC service, ExternalDataProvider provider) throws Exception {
|
||||
Field field = ExternalDataSVC.class.getDeclaredField("activeProviders");
|
||||
field.setAccessible(true);
|
||||
((List<ExternalDataProvider>) field.get(service)).add(provider);
|
||||
}
|
||||
|
||||
private record Placed(int offset, Identifier identifier) {
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
@@ -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."));
|
||||
|
||||
@@ -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
|
||||
|
||||
+12
@@ -4,6 +4,7 @@ import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.volmlib.nativelib.terrain.structure.StructureCachePolicy;
|
||||
import art.arcane.volmlib.nativelib.terrain.structure.StructureStagePolicy;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.mantle.DeferredPlacementRecords;
|
||||
import art.arcane.iris.generation.terrain.IrisMaterialPalette;
|
||||
import art.arcane.iris.structure.nativegen.GenerationWritePolicy;
|
||||
import art.arcane.iris.structure.nativegen.IrisStructurePolicy;
|
||||
@@ -13,6 +14,7 @@ import art.arcane.iris.structure.nativegen.NativeStructureStartPlan;
|
||||
import art.arcane.iris.structure.nativegen.NativeStructureVolumeIndex;
|
||||
import art.arcane.iris.structure.nativegen.StructureLocateSearch;
|
||||
import art.arcane.iris.structure.placement.IrisStructureLocator;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockChangeSet;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockPositionPredicate;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.nativelib.terrain.structure.StructureLocateSearchAccess;
|
||||
@@ -103,6 +105,16 @@ public final class BukkitStructureStagePolicy implements StructureStagePolicy<
|
||||
context.getDimensionStackContext(), context.getMinHeight());
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeBlockChangeSet trackBlockChanges(Engine context, NativeBlockChangeSet.Bounds bounds) {
|
||||
return DeferredPlacementRecords.track(context, bounds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidateBlockChanges(Engine context, NativeBlockChangeSet changes) {
|
||||
DeferredPlacementRecords.invalidate(context, changes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IntBinaryOperator surfaceHeight(Engine context) {
|
||||
return (x, z) -> Engine.hostHeight(context, x, z, true) + context.getMinHeight();
|
||||
|
||||
@@ -23,6 +23,9 @@ import art.arcane.iris.world.history.GenerationFindCatalog;
|
||||
import art.arcane.iris.world.event.IrisLootEvent;
|
||||
import art.arcane.iris.integration.Identifier;
|
||||
import art.arcane.iris.integration.ExternalDataSVC;
|
||||
import art.arcane.iris.generation.mantle.CustomBlockOwnership;
|
||||
import art.arcane.iris.generation.mantle.DeferredPlacementRecords;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockPoint;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.GenerationSessionLease;
|
||||
import art.arcane.iris.generation.locator.Locator;
|
||||
@@ -36,6 +39,7 @@ import art.arcane.iris.world.loot.InventorySlotType;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.world.loot.IrisLootTable;
|
||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||
import art.arcane.iris.generation.block.IrisCustomData;
|
||||
import art.arcane.iris.generation.block.TileData;
|
||||
import art.arcane.iris.platform.bukkit.BukkitBlockResolution;
|
||||
import art.arcane.iris.platform.bukkit.BukkitBlockState;
|
||||
@@ -83,7 +87,10 @@ import org.bukkit.loot.Lootable;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@@ -133,48 +140,73 @@ public final class EngineBukkitOps {
|
||||
Runnable tileTask = () -> materializeTiles(engine, c, chunk);
|
||||
Runnable customTask = () -> materializeCustomBlocks(engine, c, chunk);
|
||||
Runnable updateTask = () -> materializeUpdates(engine, c, chunk);
|
||||
Runnable materialize = () -> {
|
||||
DeferredPlacementRecords.removeInvalidated(engine, mantle, c.getX(), c.getZ());
|
||||
runMaterializationPasses(chunk, tileTask, customTask, updateTask);
|
||||
};
|
||||
|
||||
if (shouldRunChunkUpdateInline(c)) {
|
||||
runMaterializationPasses(chunk, tileTask, customTask, updateTask);
|
||||
materialize.run();
|
||||
return;
|
||||
}
|
||||
|
||||
runScheduledMaterializationPasses(c, () -> runMaterializationPasses(chunk, tileTask, customTask, updateTask));
|
||||
runScheduledMaterializationPasses(c, materialize);
|
||||
} finally {
|
||||
chunk.release();
|
||||
}
|
||||
}
|
||||
|
||||
static void materializeTiles(Engine engine, Chunk chunk, MantleChunk<Matter> mantleChunk) {
|
||||
mantleChunk.iterate(TileWrapper.class, (x, y, z, value) -> {
|
||||
int worldY = y + engine.getWorld().minHeight();
|
||||
StaleRecords stale = new StaleRecords();
|
||||
for (DeferredPlacementRecords.Pending<TileWrapper> record : DeferredPlacementRecords.snapshot(mantleChunk, TileWrapper.class)) {
|
||||
if (!DeferredPlacementRecords.isPending(mantleChunk, TileWrapper.class, record)) {
|
||||
continue;
|
||||
}
|
||||
NativeBlockPoint position = record.position();
|
||||
TileWrapper value = record.value();
|
||||
int worldY = position.y() + engine.getWorld().minHeight();
|
||||
if (worldY < chunk.getWorld().getMinHeight() || worldY >= chunk.getWorld().getMaxHeight()) {
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
Block block = chunk.getBlock(position.x() & 15, worldY, position.z() & 15);
|
||||
if (stale.tileReplaced(block, value.getData())) {
|
||||
continue;
|
||||
}
|
||||
Block block = chunk.getBlock(x & 15, worldY, z & 15);
|
||||
if (!TileData.setTileState(block, value.getData())) {
|
||||
NamespacedKey blockTypeKey = KeyedType.getKey(block.getType());
|
||||
String blockType = blockTypeKey == null ? block.getType().name() : blockTypeKey.toString();
|
||||
String tileType = value.getData().getMaterialKey();
|
||||
IrisLogging.warn("Failed to set tile entity data at [%d %d %d | %s] for tile %s!", block.getX(), block.getY(), block.getZ(), blockType, tileType);
|
||||
}
|
||||
});
|
||||
}
|
||||
mantleChunk.deleteSlices(TileWrapper.class);
|
||||
stale.report("tile", chunk);
|
||||
}
|
||||
|
||||
static void materializeCustomBlocks(Engine engine, Chunk chunk, MantleChunk<Matter> mantleChunk) {
|
||||
mantleChunk.iterate(Identifier.class, (x, y, z, value) -> {
|
||||
int worldY = y + engine.getWorld().minHeight();
|
||||
if (worldY < chunk.getWorld().getMinHeight() || worldY >= chunk.getWorld().getMaxHeight()) {
|
||||
return;
|
||||
StaleRecords stale = new StaleRecords();
|
||||
for (DeferredPlacementRecords.Pending<Identifier> record : DeferredPlacementRecords.snapshot(mantleChunk, Identifier.class)) {
|
||||
if (!DeferredPlacementRecords.isPending(mantleChunk, Identifier.class, record)) {
|
||||
continue;
|
||||
}
|
||||
IrisServices.get(ExternalDataSVC.class).processUpdate(
|
||||
engine,
|
||||
chunk.getBlock(x & 15, worldY, z & 15),
|
||||
value
|
||||
);
|
||||
});
|
||||
NativeBlockPoint position = record.position();
|
||||
Identifier value = record.value();
|
||||
int worldY = position.y() + engine.getWorld().minHeight();
|
||||
if (worldY < chunk.getWorld().getMinHeight() || worldY >= chunk.getWorld().getMaxHeight()) {
|
||||
continue;
|
||||
}
|
||||
ExternalDataSVC service = IrisServices.get(ExternalDataSVC.class);
|
||||
Block block = chunk.getBlock(position.x() & 15, worldY, position.z() & 15);
|
||||
if (stale.carrierReplaced(service, block, value)) {
|
||||
continue;
|
||||
}
|
||||
CustomBlockOwnership.Entry ownership = service.processUpdate(engine, block, value);
|
||||
if (ownership != null) {
|
||||
DeferredPlacementRecords.remember(engine, mantleChunk, position, ownership);
|
||||
}
|
||||
}
|
||||
mantleChunk.deleteSlices(Identifier.class);
|
||||
stale.report("custom block", chunk);
|
||||
}
|
||||
|
||||
static void materializeUpdates(Engine engine, Chunk chunk, MantleChunk<Matter> mantleChunk) {
|
||||
@@ -855,4 +887,50 @@ public final class EngineBukkitOps {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class StaleRecords {
|
||||
private final Map<Identifier, Material> carriers = new HashMap<>();
|
||||
private int skipped;
|
||||
|
||||
private boolean carrierReplaced(ExternalDataSVC service, Block block, Identifier record) {
|
||||
Material carrier = carriers.get(record);
|
||||
if (carrier == null) {
|
||||
carrier = carrierType(service, record);
|
||||
carriers.put(record, carrier);
|
||||
}
|
||||
Material live = block.getType();
|
||||
if (carrier == Material.AIR || live == carrier
|
||||
|| ((carrier == Material.FIRE || carrier == Material.SOUL_FIRE)
|
||||
&& (live == Material.AIR || live == Material.CAVE_AIR || live == Material.VOID_AIR))) {
|
||||
return false;
|
||||
}
|
||||
skipped++;
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean tileReplaced(Block block, TileData tile) {
|
||||
if (tile.resolveMaterial() == null || tile.isApplicable(block.getBlockData())) {
|
||||
return false;
|
||||
}
|
||||
skipped++;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void report(String records, Chunk chunk) {
|
||||
if (skipped == 0) {
|
||||
return;
|
||||
}
|
||||
IrisLogging.debug("Skipped %d stale %s records at chunk %d,%d in %s",
|
||||
skipped, records, chunk.getX(), chunk.getZ(), chunk.getWorld().getName());
|
||||
}
|
||||
|
||||
private static Material carrierType(ExternalDataSVC service, Identifier record) {
|
||||
Optional<BlockData> resolved = service.getBlockData(record);
|
||||
if (resolved.isEmpty()) {
|
||||
return Material.AIR;
|
||||
}
|
||||
BlockData data = resolved.get();
|
||||
Material type = (data instanceof IrisCustomData custom ? custom.getBase() : data).getMaterial();
|
||||
return type == Material.CAVE_AIR || type == Material.VOID_AIR ? Material.AIR : type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package art.arcane.iris.structure.nativegen;
|
||||
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockPositionPredicate;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockChangeSet;
|
||||
|
||||
import art.arcane.iris.generation.mantle.DeferredPlacementRecords;
|
||||
import art.arcane.iris.generation.runtime.DimensionStackContext;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
@@ -48,6 +50,16 @@ public class ImportedFeaturePolicy implements NativeImportedFeaturePolicy<IrisDi
|
||||
return NativeFeatureGenerationPolicy.control(engine);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeBlockChangeSet trackedBlockPositions(NativeBlockChangeSet.Bounds bounds) {
|
||||
return DeferredPlacementRecords.positions(engine, bounds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidateBlockChanges(NativeBlockChangeSet changes) {
|
||||
DeferredPlacementRecords.invalidate(engine, changes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> visibleBiomeKeys() {
|
||||
Set<String> keys = new LinkedHashSet<>();
|
||||
|
||||
@@ -28,11 +28,14 @@ import art.arcane.iris.generation.chunk.TerrainChunkBiomeHunkView;
|
||||
import art.arcane.iris.platform.generation.EngineBukkitOps;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.mantle.EngineMantle;
|
||||
import art.arcane.iris.generation.mantle.DeferredPlacementRecords;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBiome;
|
||||
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.ChunkSnapshot;
|
||||
import org.bukkit.Material;
|
||||
@@ -96,11 +99,12 @@ public final class InPlaceChunkRegenerator {
|
||||
|
||||
private void resetMantleMargin() {
|
||||
EngineMantle engineMantle = engine.getMantle();
|
||||
Mantle mantle = engineMantle.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++) {
|
||||
mantle.deleteChunk(centerChunkX + dx, centerChunkZ + dz);
|
||||
DeferredPlacementRecords.resetMantleChunk(mantle, centerChunkX + dx, centerChunkZ + dz,
|
||||
Math.abs(dx) <= radius && Math.abs(dz) <= radius);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -188,7 +192,14 @@ public final class InPlaceChunkRegenerator {
|
||||
}
|
||||
}
|
||||
|
||||
EngineBukkitOps.updateChunk(engine, chunk);
|
||||
Mantle<Matter> mantle = engine.getMantle().getMantle();
|
||||
MantleChunk<Matter> records = mantle.useChunk(chunkX, chunkZ);
|
||||
try {
|
||||
DeferredPlacementRecords.invalidateCustomBlockOwnership(records);
|
||||
} finally {
|
||||
records.release();
|
||||
}
|
||||
EngineBukkitOps.updateChunk(engine, chunk, mantle);
|
||||
world.refreshChunk(chunkX, chunkZ);
|
||||
}
|
||||
|
||||
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package art.arcane.iris.world.storage.matter;
|
||||
|
||||
import art.arcane.iris.generation.mantle.CustomBlockOwnership;
|
||||
import art.arcane.iris.integration.Identifier;
|
||||
import art.arcane.volmlib.util.data.palette.Palette;
|
||||
import art.arcane.volmlib.util.matter.Sliced;
|
||||
import art.arcane.volmlib.util.matter.slices.RawMatter;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Sliced
|
||||
public final class CustomBlockOwnershipMatter extends RawMatter<CustomBlockOwnership> {
|
||||
public CustomBlockOwnershipMatter() {
|
||||
this(1, 1, 1);
|
||||
}
|
||||
|
||||
public CustomBlockOwnershipMatter(int width, int height, int depth) {
|
||||
super(width, height, depth, CustomBlockOwnership.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Palette<CustomBlockOwnership> getGlobalPalette() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeNode(CustomBlockOwnership ownership, DataOutputStream output) throws IOException {
|
||||
output.writeInt(ownership.entries().size());
|
||||
for (CustomBlockOwnership.Entry entry : ownership.entries()) {
|
||||
output.writeUTF(entry.identifier().toString());
|
||||
output.writeUTF(entry.token());
|
||||
output.writeBoolean(entry.invalidated());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CustomBlockOwnership readNode(DataInputStream input) throws IOException {
|
||||
int size = input.readInt();
|
||||
if (size < 0 || size > 65_536) {
|
||||
throw new IOException("Invalid custom block ownership count: " + size);
|
||||
}
|
||||
List<CustomBlockOwnership.Entry> entries = new ArrayList<>(size);
|
||||
for (int index = 0; index < size; index++) {
|
||||
entries.add(new CustomBlockOwnership.Entry(Identifier.fromString(input.readUTF()),
|
||||
input.readUTF(), input.readBoolean()));
|
||||
}
|
||||
return new CustomBlockOwnership(entries);
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,7 @@ public final class IrisMatterSupport {
|
||||
}
|
||||
|
||||
IrisMatter.registerSliceType(new EntityMatter());
|
||||
IrisMatter.registerSliceType(new CustomBlockOwnershipMatter());
|
||||
IrisMatter.registerSliceType(new ObjectContinuationMatter());
|
||||
IrisMatter.registerSliceType("art.arcane.iris.core.link.Identifier", new IdentifierMatter());
|
||||
IrisMatter.registerSliceType("art.arcane.iris.engine.framework.NativeStructureOwnershipBundle", new NativeStructureOwnershipMatter());
|
||||
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
package art.arcane.iris.generation.mantle;
|
||||
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.IrisEngineMantle;
|
||||
import art.arcane.iris.integration.Identifier;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.testsupport.PlatformLeakGuard;
|
||||
import art.arcane.iris.world.IrisWorld;
|
||||
import art.arcane.iris.world.storage.matter.IrisMatterSupport;
|
||||
import art.arcane.iris.world.storage.matter.PreObjectMatterTest;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockPoint;
|
||||
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.MantleHooks;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import org.junit.Before;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class DeferredPlacementRegenerationTest {
|
||||
@ClassRule
|
||||
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
|
||||
|
||||
private static final Identifier ID = Identifier.fromString("craftengine:example/chair");
|
||||
private static final CustomBlockOwnership.Entry ORIGINAL = new CustomBlockOwnership.Entry(ID, "original", false);
|
||||
private static final NativeBlockPoint POSITION = new NativeBlockPoint(3, 70, 5);
|
||||
|
||||
private Mantle<Matter> mantle;
|
||||
private MantleChunk<Matter> chunk;
|
||||
|
||||
@Before
|
||||
@SuppressWarnings("unchecked")
|
||||
public void setUp() {
|
||||
PreObjectMatterTest.setUpBukkit();
|
||||
IrisMatterSupport.ensureRegistered();
|
||||
chunk = spy(new MantleChunk<>(24, 1, -1,
|
||||
IrisEngineMantle.createRuntimeDataAdapter(mock(IrisData.class)), MantleHooks.NONE));
|
||||
mantle = mock(Mantle.class);
|
||||
when(mantle.useChunk(1, -1)).thenAnswer(invocation -> chunk.use());
|
||||
chunk.getOrCreate(POSITION.y() >> 4).slice(CustomBlockOwnership.class)
|
||||
.set(POSITION.x(), POSITION.y() & 15, POSITION.z(), new CustomBlockOwnership(List.of(ORIGINAL)));
|
||||
chunk.getOrCreate(POSITION.y() >> 4).slice(Identifier.class)
|
||||
.set(POSITION.x(), POSITION.y() & 15, POSITION.z(), ID);
|
||||
chunk.flag(MantleFlag.OBJECT, true);
|
||||
chunk.flag(MantleFlag.of(200), true);
|
||||
chunk.flag(MantleFlag.TILE, true);
|
||||
chunk.flag(MantleFlag.CUSTOM, true);
|
||||
chunk.flag(MantleFlag.UPDATE, true);
|
||||
chunk.flag(MantleFlag.ETCHED, true);
|
||||
chunk.flag(MantleFlag.INITIAL_SPAWNED_MARKER, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void marginResetKeepsOwnershipAndPreventsDuplicateMaterialization() {
|
||||
CustomBlockOwnership before = ownership();
|
||||
|
||||
DeferredPlacementRecords.resetMantleChunk(mantle, 1, -1, false);
|
||||
|
||||
assertSame(before, ownership());
|
||||
assertNull(chunk.get(POSITION.x(), POSITION.y(), POSITION.z(), Identifier.class));
|
||||
assertFalse(chunk.isFlagged(MantleFlag.OBJECT));
|
||||
assertFalse(chunk.isFlagged(MantleFlag.of(200)));
|
||||
assertTrue(chunk.isFlagged(MantleFlag.TILE));
|
||||
assertTrue(chunk.isFlagged(MantleFlag.CUSTOM));
|
||||
assertTrue(chunk.isFlagged(MantleFlag.UPDATE));
|
||||
assertTrue(chunk.isFlagged(MantleFlag.INITIAL_SPAWNED_MARKER));
|
||||
AtomicInteger placements = new AtomicInteger();
|
||||
chunk.raiseFlagUnchecked(MantleFlag.ETCHED, placements::incrementAndGet);
|
||||
assertEquals(0, placements.get());
|
||||
verify(mantle, never()).deleteChunk(1, -1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void targetResetKeepsOwnershipUntilSuccessfulLiveReplacement() {
|
||||
DeferredPlacementRecords.resetMantleChunk(mantle, 1, -1, true);
|
||||
|
||||
assertEquals(List.of(ORIGINAL), ownership().entries());
|
||||
assertFalse(chunk.isFlagged(MantleFlag.TILE));
|
||||
assertFalse(chunk.isFlagged(MantleFlag.CUSTOM));
|
||||
assertFalse(chunk.isFlagged(MantleFlag.UPDATE));
|
||||
assertFalse(chunk.isFlagged(MantleFlag.ETCHED));
|
||||
assertFalse(chunk.isFlagged(MantleFlag.INITIAL_SPAWNED_MARKER));
|
||||
}
|
||||
|
||||
@Test(timeout = 5000)
|
||||
public void successfulTargetReplacementInvalidatesOldTokensWithoutDeletingNewRecords() {
|
||||
DeferredPlacementRecords.resetMantleChunk(mantle, 1, -1, true);
|
||||
chunk.getOrCreate(POSITION.y() >> 4).slice(Identifier.class)
|
||||
.set(POSITION.x(), POSITION.y() & 15, POSITION.z(), ID);
|
||||
|
||||
DeferredPlacementRecords.invalidateCustomBlockOwnership(chunk);
|
||||
|
||||
assertEquals(List.of(new CustomBlockOwnership.Entry(ID, "original", true)), ownership().entries());
|
||||
assertEquals(ID, chunk.get(POSITION.x(), POSITION.y(), POSITION.z(), Identifier.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ownershipAddedDuringResetSurvivesInTheSameChunk() throws Exception {
|
||||
CountDownLatch resetting = new CountDownLatch(1);
|
||||
CountDownLatch resume = new CountDownLatch(1);
|
||||
AtomicBoolean firstRead = new AtomicBoolean(true);
|
||||
doAnswer(invocation -> {
|
||||
if (firstRead.compareAndSet(true, false)) {
|
||||
resetting.countDown();
|
||||
assertTrue(resume.await(5, TimeUnit.SECONDS));
|
||||
}
|
||||
return invocation.callRealMethod();
|
||||
}).when(chunk).get(0);
|
||||
Engine engine = mock(Engine.class);
|
||||
when(engine.getWorld()).thenReturn(mock(IrisWorld.class));
|
||||
CustomBlockOwnership.Entry concurrent = new CustomBlockOwnership.Entry(ID, "concurrent", false);
|
||||
ExecutorService executor = Executors.newFixedThreadPool(2);
|
||||
try {
|
||||
Future<?> reset = executor.submit(() -> DeferredPlacementRecords.resetMantleChunk(mantle, 1, -1, false));
|
||||
assertTrue(resetting.await(5, TimeUnit.SECONDS));
|
||||
Future<?> remember = executor.submit(() -> DeferredPlacementRecords.remember(engine, chunk, POSITION, concurrent));
|
||||
resume.countDown();
|
||||
reset.get(5, TimeUnit.SECONDS);
|
||||
remember.get(5, TimeUnit.SECONDS);
|
||||
|
||||
assertEquals(List.of(ORIGINAL, new CustomBlockOwnership.Entry(ID, "concurrent", true)), ownership().entries());
|
||||
assertFalse(chunk.inUse());
|
||||
} finally {
|
||||
resume.countDown();
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
private CustomBlockOwnership ownership() {
|
||||
return chunk.get(POSITION.x(), POSITION.y(), POSITION.z(), CustomBlockOwnership.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
package art.arcane.iris.platform.generation;
|
||||
|
||||
import art.arcane.iris.generation.mantle.CustomBlockOwnership;
|
||||
import art.arcane.iris.generation.mantle.DeferredPlacementRecords;
|
||||
import art.arcane.iris.generation.mantle.EngineMantle;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.EnginePlatformHooks;
|
||||
import art.arcane.iris.generation.runtime.IrisEngineMantle;
|
||||
import art.arcane.iris.integration.Identifier;
|
||||
import art.arcane.iris.integration.ExternalDataSVC;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import art.arcane.iris.spi.PlatformScheduler;
|
||||
import art.arcane.iris.testsupport.PlatformLeakGuard;
|
||||
import art.arcane.iris.world.IrisWorld;
|
||||
import art.arcane.iris.world.storage.matter.CustomBlockOwnershipMatter;
|
||||
import art.arcane.iris.world.storage.matter.IrisMatterSupport;
|
||||
import art.arcane.iris.world.storage.matter.PreObjectMatterTest;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockChangeSet;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockPoint;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
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.mantle.runtime.MantleHooks;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class CustomBlockOwnershipTest {
|
||||
@ClassRule
|
||||
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
|
||||
|
||||
private static final int MIN_HEIGHT = -64;
|
||||
private static final NativeBlockPoint WORLD_POSITION = new NativeBlockPoint(19, 70, -11);
|
||||
private static final NativeBlockPoint MANTLE_POSITION = new NativeBlockPoint(3, 134, 5);
|
||||
private static final CustomBlockOwnership.Entry ORIGINAL = new CustomBlockOwnership.Entry(
|
||||
Identifier.fromString("craftengine:example/chair"), "original-entity", false);
|
||||
private static final CustomBlockOwnership.Entry REPLACEMENT = new CustomBlockOwnership.Entry(
|
||||
Identifier.fromString("craftengine:example/chair"), "replacement-entity", false);
|
||||
|
||||
private IrisPlatform previous;
|
||||
private PlatformScheduler scheduler;
|
||||
private EnginePlatformHooks hooks;
|
||||
private Engine engine;
|
||||
private NativeWorld world;
|
||||
private MantleChunk<Matter> chunk;
|
||||
|
||||
@Before
|
||||
@SuppressWarnings("unchecked")
|
||||
public void setUp() {
|
||||
PreObjectMatterTest.setUpBukkit();
|
||||
IrisMatterSupport.ensureRegistered();
|
||||
previous = IrisPlatforms.isBound() ? IrisPlatforms.get() : null;
|
||||
IrisPlatform platform = mock(IrisPlatform.class);
|
||||
PlatformRegistries registries = mock(PlatformRegistries.class);
|
||||
when(registries.block("AIR")).thenReturn(mock(NativeBlockState.class));
|
||||
when(platform.registries()).thenReturn(registries);
|
||||
scheduler = mock(PlatformScheduler.class);
|
||||
when(platform.scheduler()).thenReturn(scheduler);
|
||||
IrisPlatforms.unbind();
|
||||
IrisPlatforms.bind(platform);
|
||||
chunk = new MantleChunk<>(24, 1, -1,
|
||||
IrisEngineMantle.createRuntimeDataAdapter(mock(IrisData.class)), MantleHooks.NONE);
|
||||
Mantle<Matter> mantle = mock(Mantle.class);
|
||||
when(mantle.useChunk(1, -1)).thenAnswer(invocation -> chunk.use());
|
||||
EngineMantle engineMantle = mock(EngineMantle.class);
|
||||
when(engineMantle.getMantle()).thenReturn(mantle);
|
||||
world = mock(NativeWorld.class);
|
||||
when(world.isChunkLoaded(1, -1)).thenReturn(true);
|
||||
IrisWorld irisWorld = mock(IrisWorld.class);
|
||||
when(irisWorld.platformWorld()).thenReturn(world);
|
||||
when(irisWorld.minHeight()).thenReturn(MIN_HEIGHT);
|
||||
engine = mock(Engine.class);
|
||||
when(engine.getWorld()).thenReturn(irisWorld);
|
||||
when(engine.getMantle()).thenReturn(engineMantle);
|
||||
when(engine.getMinHeight()).thenReturn(MIN_HEIGHT);
|
||||
hooks = mock(EnginePlatformHooks.class);
|
||||
IrisServices.register(EnginePlatformHooks.class, hooks);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
IrisServices.remove(EnginePlatformHooks.class);
|
||||
IrisPlatforms.unbind();
|
||||
if (previous != null) {
|
||||
IrisPlatforms.bind(previous);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void materializedFurnitureEnablesTrackingAndRemovesOnOwningThread() {
|
||||
remember(ORIGINAL);
|
||||
NativeBlockChangeSet changes = changes();
|
||||
assertNotSame(NativeBlockChangeSet.disabled(), changes);
|
||||
|
||||
DeferredPlacementRecords.invalidate(engine, changes);
|
||||
|
||||
verify(hooks, never()).removeCustomBlock(any(), any(), any());
|
||||
CustomBlockOwnership.Entry pending = pendingOriginal();
|
||||
assertEquals(List.of(pending), ownership().entries());
|
||||
when(hooks.removeCustomBlock(engine, WORLD_POSITION, pending)).thenReturn(true);
|
||||
scheduledTask().run();
|
||||
|
||||
verify(hooks).removeCustomBlock(engine, WORLD_POSITION, pending);
|
||||
assertNull(ownership());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queuedRemovalKeepsReplacementOwnership() {
|
||||
remember(ORIGINAL);
|
||||
DeferredPlacementRecords.invalidate(engine, changes());
|
||||
remember(REPLACEMENT);
|
||||
when(hooks.removeCustomBlock(engine, WORLD_POSITION, pendingOriginal())).thenReturn(true);
|
||||
|
||||
scheduledTask().run();
|
||||
|
||||
assertEquals(List.of(REPLACEMENT), ownership().entries());
|
||||
verify(hooks, never()).removeCustomBlock(eq(engine), eq(WORLD_POSITION), eq(REPLACEMENT));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unloadedChunkRetainsRemovalUntilLoadedAndRetriesDeclinedCleanup() {
|
||||
remember(ORIGINAL);
|
||||
DeferredPlacementRecords.invalidate(engine, changes());
|
||||
when(world.isChunkLoaded(1, -1)).thenReturn(false);
|
||||
|
||||
scheduledTask().run();
|
||||
|
||||
verify(hooks, never()).removeCustomBlock(any(), any(), any());
|
||||
when(world.isChunkLoaded(1, -1)).thenReturn(true);
|
||||
DeferredPlacementRecords.removeInvalidated(engine, 1, -1);
|
||||
assertEquals(List.of(pendingOriginal()), ownership().entries());
|
||||
when(hooks.removeCustomBlock(engine, WORLD_POSITION, pendingOriginal())).thenReturn(true);
|
||||
|
||||
DeferredPlacementRecords.removeInvalidated(engine, 1, -1);
|
||||
|
||||
assertNull(ownership());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ownershipPersistsPendingAndReplacementTokens() throws Exception {
|
||||
CustomBlockOwnership expected = new CustomBlockOwnership(List.of(pendingOriginal(), REPLACEMENT));
|
||||
CustomBlockOwnershipMatter matter = new CustomBlockOwnershipMatter();
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
matter.writeNode(expected, new DataOutputStream(bytes));
|
||||
|
||||
assertEquals(expected, matter.readNode(new DataInputStream(new ByteArrayInputStream(bytes.toByteArray()))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidationDuringProviderCallbackRemovesCompletedPlacement() {
|
||||
chunk.getOrCreate(MANTLE_POSITION.y() >> 4).slice(Identifier.class)
|
||||
.set(MANTLE_POSITION.x(), MANTLE_POSITION.y() & 15, MANTLE_POSITION.z(), ORIGINAL.identifier());
|
||||
DeferredPlacementRecords.invalidate(engine, changes());
|
||||
|
||||
DeferredPlacementRecords.remember(engine, chunk, MANTLE_POSITION, ORIGINAL);
|
||||
|
||||
assertEquals(List.of(pendingOriginal()), ownership().entries());
|
||||
when(hooks.removeCustomBlock(engine, WORLD_POSITION, pendingOriginal())).thenReturn(true);
|
||||
scheduledTask().run();
|
||||
assertNull(ownership());
|
||||
}
|
||||
|
||||
@Test(timeout = 10000)
|
||||
public void providerCallbackAllowsConcurrentInvalidationAndSkipsCanceledSnapshotEntries() {
|
||||
chunk.getOrCreate(MANTLE_POSITION.y() >> 4).slice(Identifier.class)
|
||||
.set(MANTLE_POSITION.x(), MANTLE_POSITION.y() & 15, MANTLE_POSITION.z(), ORIGINAL.identifier());
|
||||
chunk.getOrCreate(MANTLE_POSITION.y() >> 4).slice(Identifier.class)
|
||||
.set(MANTLE_POSITION.x() + 1, MANTLE_POSITION.y() & 15, MANTLE_POSITION.z(), ORIGINAL.identifier());
|
||||
NativeBlockChangeSet changes = changes();
|
||||
changes.add(WORLD_POSITION.x() + 1, WORLD_POSITION.y(), WORLD_POSITION.z());
|
||||
World bukkitWorld = mock(World.class);
|
||||
when(bukkitWorld.getMinHeight()).thenReturn(MIN_HEIGHT);
|
||||
when(bukkitWorld.getMaxHeight()).thenReturn(320);
|
||||
Chunk bukkitChunk = mock(Chunk.class);
|
||||
when(bukkitChunk.getWorld()).thenReturn(bukkitWorld);
|
||||
Block started = mock(Block.class);
|
||||
Block canceled = mock(Block.class);
|
||||
when(bukkitChunk.getBlock(3, 70, 5)).thenReturn(started);
|
||||
when(bukkitChunk.getBlock(4, 70, 5)).thenReturn(canceled);
|
||||
ExternalDataSVC provider = mock(ExternalDataSVC.class);
|
||||
IrisServices.register(ExternalDataSVC.class, provider);
|
||||
try (ExecutorService worker = Executors.newVirtualThreadPerTaskExecutor()) {
|
||||
when(provider.processUpdate(engine, started, ORIGINAL.identifier())).thenAnswer(invocation -> {
|
||||
CompletableFuture.runAsync(() -> DeferredPlacementRecords.invalidate(engine, changes), worker)
|
||||
.get(3, TimeUnit.SECONDS);
|
||||
return ORIGINAL;
|
||||
});
|
||||
|
||||
EngineBukkitOps.materializeCustomBlocks(engine, bukkitChunk, chunk);
|
||||
|
||||
verify(provider).processUpdate(engine, started, ORIGINAL.identifier());
|
||||
verify(provider, never()).processUpdate(eq(engine), eq(canceled), any());
|
||||
assertEquals(List.of(pendingOriginal()), ownership().entries());
|
||||
when(hooks.removeCustomBlock(engine, WORLD_POSITION, pendingOriginal())).thenReturn(true);
|
||||
scheduledTask().run();
|
||||
assertNull(ownership());
|
||||
} finally {
|
||||
IrisServices.remove(ExternalDataSVC.class);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void queuedInvalidationKeepsSelectedMantleAndHeightAfterRuntimeChanges() {
|
||||
remember(ORIGINAL);
|
||||
DeferredPlacementRecords.invalidate(engine, changes());
|
||||
Mantle<Matter> replacementMantle = mock(Mantle.class);
|
||||
EngineMantle replacementRuntime = mock(EngineMantle.class);
|
||||
when(replacementRuntime.getMantle()).thenReturn(replacementMantle);
|
||||
when(engine.getMantle()).thenReturn(replacementRuntime);
|
||||
when(engine.getMinHeight()).thenReturn(0);
|
||||
when(hooks.removeCustomBlock(engine, WORLD_POSITION, pendingOriginal())).thenReturn(true);
|
||||
|
||||
scheduledTask().run();
|
||||
|
||||
assertNull(ownership());
|
||||
verify(hooks).removeCustomBlock(engine, WORLD_POSITION, pendingOriginal());
|
||||
verify(replacementMantle, never()).useChunk(anyInt(), anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void completedPlacementUsesSuppliedChunkWhenEngineSelectsAnotherMantle() {
|
||||
Mantle<Matter> replacementMantle = mock(Mantle.class);
|
||||
EngineMantle replacementRuntime = mock(EngineMantle.class);
|
||||
when(replacementRuntime.getMantle()).thenReturn(replacementMantle);
|
||||
when(engine.getMantle()).thenReturn(replacementRuntime);
|
||||
|
||||
DeferredPlacementRecords.remember(engine, chunk, MANTLE_POSITION, ORIGINAL);
|
||||
|
||||
when(engine.getMinHeight()).thenReturn(0);
|
||||
when(hooks.removeCustomBlock(engine, WORLD_POSITION, pendingOriginal())).thenReturn(true);
|
||||
scheduledTask().run();
|
||||
|
||||
assertNull(ownership());
|
||||
verify(hooks).removeCustomBlock(engine, WORLD_POSITION, pendingOriginal());
|
||||
verify(replacementMantle, never()).useChunk(anyInt(), anyInt());
|
||||
}
|
||||
|
||||
private void remember(CustomBlockOwnership.Entry entry) {
|
||||
chunk.getOrCreate(MANTLE_POSITION.y() >> 4).slice(Identifier.class)
|
||||
.set(MANTLE_POSITION.x(), MANTLE_POSITION.y() & 15, MANTLE_POSITION.z(), entry.identifier());
|
||||
DeferredPlacementRecords.remember(engine, chunk, MANTLE_POSITION, entry);
|
||||
}
|
||||
|
||||
private NativeBlockChangeSet changes() {
|
||||
NativeBlockChangeSet changes = DeferredPlacementRecords.track(engine,
|
||||
new NativeBlockChangeSet.Bounds(1, -1, 1, -1, MIN_HEIGHT, 384));
|
||||
changes.add(WORLD_POSITION.x(), WORLD_POSITION.y(), WORLD_POSITION.z());
|
||||
return changes;
|
||||
}
|
||||
|
||||
private Runnable scheduledTask() {
|
||||
ArgumentCaptor<Runnable> task = ArgumentCaptor.forClass(Runnable.class);
|
||||
verify(scheduler).region(eq(world), eq(1), eq(-1), task.capture());
|
||||
return task.getValue();
|
||||
}
|
||||
|
||||
private CustomBlockOwnership ownership() {
|
||||
return chunk.get(MANTLE_POSITION.y() >> 4).getSlice(CustomBlockOwnership.class)
|
||||
.get(MANTLE_POSITION.x(), MANTLE_POSITION.y() & 15, MANTLE_POSITION.z());
|
||||
}
|
||||
|
||||
private static CustomBlockOwnership.Entry pendingOriginal() {
|
||||
return new CustomBlockOwnership.Entry(ORIGINAL.identifier(), ORIGINAL.token(), true);
|
||||
}
|
||||
}
|
||||
+266
@@ -0,0 +1,266 @@
|
||||
package art.arcane.iris.platform.generation;
|
||||
|
||||
import art.arcane.iris.generation.block.TileData;
|
||||
import art.arcane.iris.generation.block.IrisCustomData;
|
||||
import art.arcane.iris.generation.mantle.DeferredPlacementRecords;
|
||||
import art.arcane.iris.generation.mantle.CustomBlockOwnership;
|
||||
import art.arcane.iris.generation.mantle.EngineMantle;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.IrisEngineMantle;
|
||||
import art.arcane.iris.integration.ExternalDataSVC;
|
||||
import art.arcane.iris.integration.Identifier;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import art.arcane.iris.structure.nativegen.ImportedFeaturePolicy;
|
||||
import art.arcane.iris.testsupport.PlatformLeakGuard;
|
||||
import art.arcane.iris.world.IrisWorld;
|
||||
import art.arcane.iris.world.storage.matter.IrisMatterSupport;
|
||||
import art.arcane.iris.world.storage.matter.PreObjectMatterTest;
|
||||
import art.arcane.iris.world.storage.matter.TileWrapper;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockChangeSet;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
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 org.bukkit.Chunk;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class DeferredPlacementInvalidationTest {
|
||||
@ClassRule
|
||||
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
|
||||
|
||||
private static final int MIN_HEIGHT = -64;
|
||||
private static final int SECTIONS = 24;
|
||||
|
||||
@Test
|
||||
public void nativeWritesClearDeferredRecordsSoMaterializationSkipsThem() {
|
||||
withMantleData(adapter -> {
|
||||
Map<Long, MantleChunk<Matter>> chunks = new HashMap<>();
|
||||
MantleChunk<Matter> stored = chunk(chunks, adapter, 0, 0);
|
||||
Identifier replaced = Identifier.fromString("craftengine:fence_post");
|
||||
Identifier kept = Identifier.fromString("craftengine:lantern");
|
||||
setRecord(stored, 3, 70, 5, Identifier.class, replaced);
|
||||
setRecord(stored, 3, 76, 5, Identifier.class, kept);
|
||||
setRecord(stored, 4, 70, 5, TileWrapper.class,
|
||||
new TileWrapper(new TileData("minecraft:chest", new KMap<>())));
|
||||
Engine engine = engine(chunks, adapter);
|
||||
|
||||
NativeBlockChangeSet changes = DeferredPlacementRecords.track(engine, bounds());
|
||||
changes.add(3, 70, 5);
|
||||
changes.add(4, 70, 5);
|
||||
DeferredPlacementRecords.invalidate(engine, changes);
|
||||
|
||||
assertNull(record(stored, 4, 70, 5, TileWrapper.class));
|
||||
assertEquals(kept, record(stored, 3, 76, 5, Identifier.class));
|
||||
World world = mock(World.class);
|
||||
when(world.getMinHeight()).thenReturn(MIN_HEIGHT);
|
||||
when(world.getMaxHeight()).thenReturn(320);
|
||||
Chunk bukkitChunk = mock(Chunk.class);
|
||||
when(bukkitChunk.getWorld()).thenReturn(world);
|
||||
Block replacedBlock = mock(Block.class);
|
||||
Block keptBlock = mock(Block.class);
|
||||
when(bukkitChunk.getBlock(3, 70, 5)).thenReturn(replacedBlock);
|
||||
when(bukkitChunk.getBlock(3, 76, 5)).thenReturn(keptBlock);
|
||||
ExternalDataSVC provider = mock(ExternalDataSVC.class);
|
||||
try (MockedStatic<IrisServices> services = mockStatic(IrisServices.class)) {
|
||||
services.when(() -> IrisServices.get(ExternalDataSVC.class)).thenReturn(provider);
|
||||
|
||||
EngineBukkitOps.materializeCustomBlocks(engine, bukkitChunk, stored);
|
||||
}
|
||||
|
||||
verify(provider).processUpdate(engine, keptBlock, kept);
|
||||
verify(provider, never()).processUpdate(eq(engine), eq(replacedBlock), any(Identifier.class));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void regionsWithoutDeferredRecordsAreNotTracked() {
|
||||
withMantleData(adapter -> {
|
||||
Map<Long, MantleChunk<Matter>> chunks = new HashMap<>();
|
||||
setRecord(chunk(chunks, adapter, 3, 0), 0, 70, 0, Identifier.class,
|
||||
Identifier.fromString("craftengine:outside"));
|
||||
|
||||
assertSame(NativeBlockChangeSet.disabled(),
|
||||
DeferredPlacementRecords.track(engine(chunks, adapter), bounds()));
|
||||
assertSame(NativeBlockChangeSet.disabled(),
|
||||
DeferredPlacementRecords.positions(engine(chunks, adapter), bounds()));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void importedFeatureCutRemovesFireRecordBeforeMaterialization() {
|
||||
withMantleData(adapter -> {
|
||||
Map<Long, MantleChunk<Matter>> chunks = new HashMap<>();
|
||||
MantleChunk<Matter> stored = chunk(chunks, adapter, 0, 0);
|
||||
Identifier fire = Identifier.fromString("craftengine:example/fire");
|
||||
setRecord(stored, 3, 70, 5, Identifier.class, fire);
|
||||
setRecord(stored, 4, 70, 5, Identifier.class, fire);
|
||||
Engine engine = engine(chunks, adapter);
|
||||
ImportedFeaturePolicy policy = new ImportedFeaturePolicy(engine);
|
||||
NativeBlockChangeSet candidates = policy.trackedBlockPositions(bounds());
|
||||
assertEquals(2, candidates.size());
|
||||
NativeBlockChangeSet changes = NativeBlockChangeSet.create(candidates.bounds());
|
||||
changes.add(3, 70, 5);
|
||||
|
||||
policy.invalidateBlockChanges(changes);
|
||||
|
||||
assertNull(record(stored, 3, 70, 5, Identifier.class));
|
||||
World world = mock(World.class);
|
||||
when(world.getMinHeight()).thenReturn(MIN_HEIGHT);
|
||||
when(world.getMaxHeight()).thenReturn(320);
|
||||
Chunk bukkitChunk = mock(Chunk.class);
|
||||
when(bukkitChunk.getWorld()).thenReturn(world);
|
||||
Block cut = mock(Block.class);
|
||||
Block extinguished = mock(Block.class);
|
||||
when(cut.getType()).thenReturn(Material.AIR);
|
||||
when(extinguished.getType()).thenReturn(Material.AIR);
|
||||
when(bukkitChunk.getBlock(3, 70, 5)).thenReturn(cut);
|
||||
when(bukkitChunk.getBlock(4, 70, 5)).thenReturn(extinguished);
|
||||
BlockData carrier = mock(BlockData.class);
|
||||
when(carrier.getMaterial()).thenReturn(Material.FIRE);
|
||||
ExternalDataSVC provider = mock(ExternalDataSVC.class);
|
||||
when(provider.getBlockData(fire)).thenReturn(Optional.of(IrisCustomData.of(carrier, fire)));
|
||||
try (MockedStatic<IrisServices> services = mockStatic(IrisServices.class)) {
|
||||
services.when(() -> IrisServices.get(ExternalDataSVC.class)).thenReturn(provider);
|
||||
|
||||
EngineBukkitOps.materializeCustomBlocks(engine, bukkitChunk, stored);
|
||||
}
|
||||
|
||||
verify(provider, never()).processUpdate(eq(engine), eq(cut), any());
|
||||
verify(provider).processUpdate(engine, extinguished, fire);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void neighbouringRecordsEnableTrackingForTheWholeWriteRegion() {
|
||||
withMantleData(adapter -> {
|
||||
Map<Long, MantleChunk<Matter>> chunks = new HashMap<>();
|
||||
setRecord(chunk(chunks, adapter, 1, -1), 0, 70, 0, TileWrapper.class,
|
||||
new TileWrapper(new TileData("minecraft:barrel", new KMap<>())));
|
||||
|
||||
NativeBlockChangeSet changes = DeferredPlacementRecords.track(engine(chunks, adapter), bounds());
|
||||
|
||||
assertNotSame(NativeBlockChangeSet.disabled(), changes);
|
||||
changes.add(16, 70, -16);
|
||||
DeferredPlacementRecords.invalidate(engine(chunks, adapter), changes);
|
||||
assertNull(record(chunks.get(key(1, -1)), 0, 70, 0, TileWrapper.class));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void importedFeatureCandidatesIncludeNeighbourOwnershipAndDeduplicateSlices() {
|
||||
withMantleData(adapter -> {
|
||||
Map<Long, MantleChunk<Matter>> chunks = new HashMap<>();
|
||||
MantleChunk<Matter> stored = chunk(chunks, adapter, -1, 1);
|
||||
Identifier identifier = Identifier.fromString("craftengine:example/chair");
|
||||
setRecord(stored, 15, 70, 2, Identifier.class, identifier);
|
||||
setRecord(stored, 15, 70, 2, TileWrapper.class,
|
||||
new TileWrapper(new TileData("minecraft:chest", new KMap<>())));
|
||||
setRecord(stored, 14, 71, 3, CustomBlockOwnership.class,
|
||||
new CustomBlockOwnership(List.of(new CustomBlockOwnership.Entry(identifier, "entity-token", false))));
|
||||
setRecord(chunk(chunks, adapter, 2, 0), 0, 70, 0, Identifier.class, identifier);
|
||||
|
||||
NativeBlockChangeSet positions = DeferredPlacementRecords.positions(engine(chunks, adapter), bounds());
|
||||
|
||||
assertEquals(2, positions.size());
|
||||
assertTrue(positions.contains(-1, 70, 18));
|
||||
assertTrue(positions.contains(-2, 71, 19));
|
||||
});
|
||||
}
|
||||
|
||||
private static NativeBlockChangeSet.Bounds bounds() {
|
||||
return new NativeBlockChangeSet.Bounds(-1, -1, 1, 1, MIN_HEIGHT, 384);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Engine engine(Map<Long, MantleChunk<Matter>> chunks, MantleDataAdapter<Matter> adapter) {
|
||||
Mantle<Matter> mantle = mock(Mantle.class);
|
||||
when(mantle.useChunk(anyInt(), anyInt())).thenAnswer(invocation ->
|
||||
chunk(chunks, adapter, invocation.getArgument(0), invocation.getArgument(1)).use());
|
||||
EngineMantle engineMantle = mock(EngineMantle.class);
|
||||
when(engineMantle.getMantle()).thenReturn(mantle);
|
||||
IrisWorld world = mock(IrisWorld.class);
|
||||
when(world.minHeight()).thenReturn(MIN_HEIGHT);
|
||||
Engine engine = mock(Engine.class);
|
||||
when(engine.getMantle()).thenReturn(engineMantle);
|
||||
when(engine.getWorld()).thenReturn(world);
|
||||
when(engine.getMinHeight()).thenReturn(MIN_HEIGHT);
|
||||
return engine;
|
||||
}
|
||||
|
||||
private static MantleChunk<Matter> chunk(Map<Long, MantleChunk<Matter>> chunks, MantleDataAdapter<Matter> adapter,
|
||||
int chunkX, int chunkZ) {
|
||||
return chunks.computeIfAbsent(key(chunkX, chunkZ),
|
||||
ignored -> new MantleChunk<>(SECTIONS, chunkX, chunkZ, adapter, MantleHooks.NONE));
|
||||
}
|
||||
|
||||
private static long key(int chunkX, int chunkZ) {
|
||||
return ((long) chunkX << 32) | (chunkZ & 0xffffffffL);
|
||||
}
|
||||
|
||||
private static <T> void setRecord(MantleChunk<Matter> chunk, int x, int worldY, int z, Class<T> type, T value) {
|
||||
int y = worldY - MIN_HEIGHT;
|
||||
chunk.getOrCreate(y >> 4).slice(type).set(x & 15, y & 15, z & 15, value);
|
||||
}
|
||||
|
||||
private static <T> T record(MantleChunk<Matter> chunk, int x, int worldY, int z, Class<T> type) {
|
||||
int y = worldY - MIN_HEIGHT;
|
||||
return chunk.get(y >> 4).getSlice(type).get(x & 15, y & 15, z & 15);
|
||||
}
|
||||
|
||||
private static void withMantleData(MantleTest test) {
|
||||
PreObjectMatterTest.setUpBukkit();
|
||||
IrisMatterSupport.ensureRegistered();
|
||||
IrisPlatform previous = IrisPlatforms.isBound() ? IrisPlatforms.get() : null;
|
||||
IrisPlatform platform = mock(IrisPlatform.class);
|
||||
PlatformRegistries registries = mock(PlatformRegistries.class);
|
||||
when(registries.block("AIR")).thenReturn(mock(NativeBlockState.class));
|
||||
when(platform.registries()).thenReturn(registries);
|
||||
IrisPlatforms.unbind();
|
||||
IrisPlatforms.bind(platform);
|
||||
try {
|
||||
test.run(IrisEngineMantle.createRuntimeDataAdapter(mock(IrisData.class)));
|
||||
} finally {
|
||||
IrisPlatforms.unbind();
|
||||
if (previous != null) {
|
||||
IrisPlatforms.bind(previous);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface MantleTest {
|
||||
void run(MantleDataAdapter<Matter> adapter);
|
||||
}
|
||||
}
|
||||
+2
@@ -51,6 +51,7 @@ import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -76,6 +77,7 @@ public class EngineBukkitOpsDeferredMaterializationTest {
|
||||
iterator.accept(1, 2, 3, identifier);
|
||||
return null;
|
||||
}).when(stored).iterate(eq(Identifier.class), any());
|
||||
doReturn(identifier).when(stored).get(1, 2, 3, Identifier.class);
|
||||
Engine engine = mock(Engine.class);
|
||||
when(engine.getWorld()).thenReturn(mock(IrisWorld.class));
|
||||
when(engine.getMetrics()).thenReturn(new EngineMetrics(8));
|
||||
|
||||
+288
@@ -0,0 +1,288 @@
|
||||
package art.arcane.iris.platform.generation;
|
||||
|
||||
import art.arcane.iris.generation.block.IrisCustomData;
|
||||
import art.arcane.iris.generation.block.TileData;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.integration.ExternalDataSVC;
|
||||
import art.arcane.iris.integration.Identifier;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.testsupport.PlatformLeakGuard;
|
||||
import art.arcane.iris.world.IrisWorld;
|
||||
import art.arcane.iris.world.storage.matter.TileWrapper;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.function.Consumer4;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.TileState;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
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.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
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 EngineBukkitOpsStaleRecordTest {
|
||||
@ClassRule
|
||||
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
|
||||
|
||||
private static final Identifier ORE = Identifier.fromString("itemsadder:forest/amber_ore");
|
||||
private static final Identifier CHAIR = Identifier.fromString("craftengine:test/chair[variant=alpha]");
|
||||
|
||||
@Test
|
||||
public void customRecordOverFlattenedAirIsNotPlaced() {
|
||||
Fixture fixture = new Fixture();
|
||||
fixture.carrier(ORE, Material.NOTE_BLOCK);
|
||||
Block flattened = fixture.block(1, 10, 1, Material.AIR);
|
||||
MantleChunk<Matter> records = customRecords(new Placed<>(1, 10, 1, ORE));
|
||||
|
||||
fixture.materializeCustomBlocks(records);
|
||||
|
||||
verify(fixture.service, never()).processUpdate(any(), eq(flattened), any());
|
||||
verify(records).deleteSlices(Identifier.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customRecordOverReplacedBlockIsNotPlaced() {
|
||||
Fixture fixture = new Fixture();
|
||||
fixture.carrier(ORE, Material.NOTE_BLOCK);
|
||||
Block wall = fixture.block(2, 10, 2, Material.COBBLESTONE);
|
||||
MantleChunk<Matter> records = customRecords(new Placed<>(2, 10, 2, ORE));
|
||||
|
||||
fixture.materializeCustomBlocks(records);
|
||||
|
||||
verify(fixture.service, never()).processUpdate(any(), eq(wall), any());
|
||||
verify(records).deleteSlices(Identifier.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customRecordOverIntactCarrierIsPlaced() {
|
||||
Fixture fixture = new Fixture();
|
||||
fixture.carrier(ORE, Material.NOTE_BLOCK);
|
||||
Block intact = fixture.block(3, 10, 3, Material.NOTE_BLOCK);
|
||||
|
||||
fixture.materializeCustomBlocks(customRecords(new Placed<>(3, 10, 3, ORE)));
|
||||
|
||||
verify(fixture.service).processUpdate(fixture.engine, intact, ORE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void extinguishedFireCarrierIsPlacedButCoveredFireIsSkipped() {
|
||||
for (Material carrier : List.of(Material.FIRE, Material.SOUL_FIRE)) {
|
||||
Fixture fixture = new Fixture();
|
||||
fixture.carrier(ORE, carrier);
|
||||
Block extinguished = fixture.block(1, 10, 1, Material.AIR);
|
||||
Block caveAir = fixture.block(2, 10, 2, Material.CAVE_AIR);
|
||||
Block wall = fixture.block(3, 10, 3, Material.COBBLESTONE);
|
||||
|
||||
fixture.materializeCustomBlocks(customRecords(
|
||||
new Placed<>(1, 10, 1, ORE),
|
||||
new Placed<>(2, 10, 2, ORE),
|
||||
new Placed<>(3, 10, 3, ORE)));
|
||||
|
||||
verify(fixture.service).processUpdate(fixture.engine, extinguished, ORE);
|
||||
verify(fixture.service).processUpdate(fixture.engine, caveAir, ORE);
|
||||
verify(fixture.service, never()).processUpdate(any(), eq(wall), any());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void airCarrierFurnitureRecordIsPlacedWhateverTheLiveBlock() {
|
||||
Fixture fixture = new Fixture();
|
||||
fixture.carrier(CHAIR, Material.AIR);
|
||||
Block open = fixture.block(4, 10, 4, Material.AIR);
|
||||
Block covered = fixture.block(5, 10, 5, Material.COBBLESTONE);
|
||||
|
||||
fixture.materializeCustomBlocks(customRecords(
|
||||
new Placed<>(4, 10, 4, CHAIR),
|
||||
new Placed<>(5, 10, 5, CHAIR)));
|
||||
|
||||
verify(fixture.service).processUpdate(fixture.engine, open, CHAIR);
|
||||
verify(fixture.service).processUpdate(fixture.engine, covered, CHAIR);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unresolvableCarrierLeavesPlacementToTheProvider() {
|
||||
Fixture fixture = new Fixture();
|
||||
when(fixture.service.getBlockData(ORE)).thenReturn(Optional.empty());
|
||||
Block block = fixture.block(6, 10, 6, Material.STONE);
|
||||
|
||||
fixture.materializeCustomBlocks(customRecords(new Placed<>(6, 10, 6, ORE)));
|
||||
|
||||
verify(fixture.service).processUpdate(fixture.engine, block, ORE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void carrierResolvesOncePerIdentifierAndStaleRecordsLogOncePerChunk() {
|
||||
Fixture fixture = new Fixture();
|
||||
fixture.carrier(ORE, Material.NOTE_BLOCK);
|
||||
fixture.block(1, 10, 1, Material.AIR);
|
||||
fixture.block(2, 10, 2, Material.COBBLESTONE);
|
||||
Block intact = fixture.block(3, 10, 3, Material.NOTE_BLOCK);
|
||||
MantleChunk<Matter> records = customRecords(
|
||||
new Placed<>(1, 10, 1, ORE),
|
||||
new Placed<>(2, 10, 2, ORE),
|
||||
new Placed<>(3, 10, 3, ORE));
|
||||
|
||||
List<String> debug = fixture.materializeCustomBlocksCapturingDebug(records);
|
||||
|
||||
verify(fixture.service, times(1)).getBlockData(ORE);
|
||||
verify(fixture.service, times(1)).processUpdate(any(), any(), any());
|
||||
verify(fixture.service).processUpdate(fixture.engine, intact, ORE);
|
||||
assertEquals(1, debug.size());
|
||||
assertTrue(debug.getFirst(), debug.getFirst().contains("2 stale"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void staleTileRecordIsSkippedWithoutWarning() {
|
||||
Fixture fixture = new Fixture();
|
||||
TileData chest = chestTile();
|
||||
Block replaced = fixture.block(7, 10, 7, Material.STONE);
|
||||
BlockData stone = blockData(Material.STONE);
|
||||
when(replaced.getBlockData()).thenReturn(stone);
|
||||
|
||||
try (MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
|
||||
EngineBukkitOps.materializeTiles(fixture.engine, fixture.chunk, tileRecords(new Placed<>(7, 10, 7, chest)));
|
||||
|
||||
logging.verify(() -> IrisLogging.warn(anyString(), any(Object[].class)), never());
|
||||
logging.verify(() -> IrisLogging.debug(anyString(), any(Object[].class)), times(1));
|
||||
}
|
||||
verify(chest, never()).toBukkitTry(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void intactTileRecordIsApplied() {
|
||||
Fixture fixture = new Fixture();
|
||||
TileData chest = chestTile();
|
||||
Block intact = fixture.block(8, 10, 8, Material.CHEST);
|
||||
BlockData chestData = blockData(Material.CHEST);
|
||||
when(intact.getBlockData()).thenReturn(chestData);
|
||||
when(intact.getState()).thenReturn(mock(TileState.class));
|
||||
doReturn(true).when(chest).toBukkitTry(intact);
|
||||
|
||||
try (MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
|
||||
EngineBukkitOps.materializeTiles(fixture.engine, fixture.chunk, tileRecords(new Placed<>(8, 10, 8, chest)));
|
||||
|
||||
logging.verify(() -> IrisLogging.warn(anyString(), any(Object[].class)), never());
|
||||
}
|
||||
verify(chest).toBukkitTry(intact);
|
||||
}
|
||||
|
||||
private static TileData chestTile() {
|
||||
TileData tile = spy(new TileData("minecraft:chest", new KMap<>()));
|
||||
doReturn(Material.CHEST).when(tile).resolveMaterial();
|
||||
return tile;
|
||||
}
|
||||
|
||||
private static BlockData blockData(Material material) {
|
||||
BlockData data = mock(BlockData.class);
|
||||
when(data.getMaterial()).thenReturn(material);
|
||||
return data;
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
private static MantleChunk<Matter> customRecords(Placed<Identifier>... records) {
|
||||
return records(Identifier.class, records);
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
private static MantleChunk<Matter> tileRecords(Placed<TileData>... records) {
|
||||
List<Placed<TileWrapper>> wrapped = new ArrayList<>(records.length);
|
||||
for (Placed<TileData> record : records) {
|
||||
wrapped.add(new Placed<>(record.x(), record.y(), record.z(), new TileWrapper(record.value())));
|
||||
}
|
||||
return records(TileWrapper.class, wrapped.toArray(Placed[]::new));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T> MantleChunk<Matter> records(Class<T> type, Placed<T>[] records) {
|
||||
MantleChunk<Matter> chunk = mock(MantleChunk.class);
|
||||
for (Placed<T> record : records) {
|
||||
when(chunk.get(record.x(), record.y(), record.z(), type)).thenReturn(record.value());
|
||||
}
|
||||
doAnswer((InvocationOnMock invocation) -> {
|
||||
Consumer4<Integer, Integer, Integer, T> iterator = invocation.getArgument(1);
|
||||
for (Placed<T> record : records) {
|
||||
iterator.accept(record.x(), record.y(), record.z(), record.value());
|
||||
}
|
||||
return null;
|
||||
}).when(chunk).iterate(eq(type), any());
|
||||
return chunk;
|
||||
}
|
||||
|
||||
private record Placed<T>(int x, int y, int z, T value) {
|
||||
}
|
||||
|
||||
private static final class Fixture {
|
||||
private final Engine engine = mock(Engine.class);
|
||||
private final Chunk chunk = mock(Chunk.class);
|
||||
private final ExternalDataSVC service = mock(ExternalDataSVC.class);
|
||||
|
||||
private Fixture() {
|
||||
IrisWorld irisWorld = mock(IrisWorld.class);
|
||||
when(engine.getWorld()).thenReturn(irisWorld);
|
||||
World world = mock(World.class);
|
||||
when(world.getMinHeight()).thenReturn(0);
|
||||
when(world.getMaxHeight()).thenReturn(256);
|
||||
when(world.getName()).thenReturn("example");
|
||||
when(chunk.getWorld()).thenReturn(world);
|
||||
}
|
||||
|
||||
private void carrier(Identifier identifier, Material material) {
|
||||
BlockData carrier = IrisCustomData.of(blockData(material), identifier);
|
||||
when(service.getBlockData(identifier)).thenReturn(Optional.of(carrier));
|
||||
}
|
||||
|
||||
private Block block(int x, int y, int z, Material type) {
|
||||
Block block = mock(Block.class);
|
||||
when(block.getType()).thenReturn(type);
|
||||
when(chunk.getBlock(x, y, z)).thenReturn(block);
|
||||
return block;
|
||||
}
|
||||
|
||||
private void materializeCustomBlocks(MantleChunk<Matter> records) {
|
||||
try (MockedStatic<IrisServices> services = mockStatic(IrisServices.class)) {
|
||||
services.when(() -> IrisServices.get(ExternalDataSVC.class)).thenReturn(service);
|
||||
EngineBukkitOps.materializeCustomBlocks(engine, chunk, records);
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> materializeCustomBlocksCapturingDebug(MantleChunk<Matter> records) {
|
||||
List<String> messages = new ArrayList<>();
|
||||
try (MockedStatic<IrisServices> services = mockStatic(IrisServices.class);
|
||||
MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
|
||||
services.when(() -> IrisServices.get(ExternalDataSVC.class)).thenReturn(service);
|
||||
logging.when(() -> IrisLogging.debug(anyString(), any(Object[].class))).thenAnswer((InvocationOnMock invocation) -> {
|
||||
Object[] arguments = invocation.getArguments();
|
||||
messages.add(String.format((String) arguments[0], Arrays.copyOfRange(arguments, 1, arguments.length)));
|
||||
return null;
|
||||
});
|
||||
EngineBukkitOps.materializeCustomBlocks(engine, chunk, records);
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user