mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-20 15:26:51 +00:00
Skip stale deferred placements
- Skip deferred custom block records whose live block no longer matches the provider carrier type - Leave air-carrier furniture and unresolvable carriers to the provider unchanged - Resolve each carrier once per chunk pass and log skipped records once per chunk at debug level - Skip tile records quietly when the live block no longer matches the tile material - Apply the same carrier check to the modded custom block pass - Add Bukkit, CraftEngine and modded regression tests for flattened, replaced, intact and furniture records
This commit is contained in:
1 parent
a3eccd8a65
commit
dbc9d95372
6 files changed
+765
-7
No files matched your search
+50
-2
@@ -31,10 +31,12 @@ import art.arcane.iris.integration.Identifier;
|
||||
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 +52,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;
|
||||
@@ -239,16 +243,24 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
|
||||
int baseX = chunkX << 4;
|
||||
int baseZ = chunkZ << 4;
|
||||
int minHeight = engine.getWorld().minHeight();
|
||||
StaleCarriers stale = new StaleCarriers();
|
||||
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(),
|
||||
int blockX = baseX + (x & 15);
|
||||
int blockZ = baseZ + (z & 15);
|
||||
String key = identifier.toString();
|
||||
if (stale.carrierReplaced(key, level.getBlock(blockX, y, blockZ))) {
|
||||
return;
|
||||
}
|
||||
NativeBlockPoint position = new NativeBlockPoint(blockX, y, blockZ);
|
||||
ModdedCustomContentRegistry.processBlockPlacement(engine, key,
|
||||
() -> NativeWorldMaintenance.placement(level, position));
|
||||
}));
|
||||
stale.report(level.name(), chunkX, chunkZ);
|
||||
}
|
||||
|
||||
static void materializeDeferredSlice(MantleChunk<Matter> chunk, Class<?> sliceType, Runnable materializer) {
|
||||
@@ -386,4 +398,40 @@ 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))) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
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 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<>();
|
||||
|
||||
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(chunk), level, 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"));
|
||||
}
|
||||
|
||||
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 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) : List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValidProvider(String id, ModdedDataType type) {
|
||||
return type == ModdedDataType.BLOCK && (LAMP.equals(id) || CHAIR.equals(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModdedBlockData getBlockData(String blockId, Map<String, String> state) {
|
||||
BlockState carrier = CHAIR.equals(blockId) ? Blocks.AIR.defaultBlockState() : Blocks.NOTE_BLOCK.defaultBlockState();
|
||||
return ModdedBlockData.deferred(ModdedBlockState.of(carrier, null));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processBlockPlacement(ModdedBlockPlacementContext context) {
|
||||
PLACED.add(context.placement().position());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -136,6 +136,12 @@
|
||||
"replacement": "when(level.getMaxBuildHeight()).thenReturn(320);"
|
||||
}
|
||||
],
|
||||
"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\\);",
|
||||
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
package art.arcane.iris.platform.generation;
|
||||
|
||||
import art.arcane.iris.generation.block.IrisCustomData;
|
||||
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 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.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 static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
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);
|
||||
furniture.when(() -> CraftEngineFurniture.place(any(Location.class), eq(chairDefinition), eq("alpha"), eq(false)))
|
||||
.thenReturn(mock(BukkitFurniture.class));
|
||||
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);
|
||||
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) {
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,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 +84,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;
|
||||
@@ -146,12 +150,16 @@ public final class EngineBukkitOps {
|
||||
}
|
||||
|
||||
static void materializeTiles(Engine engine, Chunk chunk, MantleChunk<Matter> mantleChunk) {
|
||||
StaleRecords stale = new StaleRecords();
|
||||
mantleChunk.iterate(TileWrapper.class, (x, y, z, value) -> {
|
||||
int worldY = y + engine.getWorld().minHeight();
|
||||
if (worldY < chunk.getWorld().getMinHeight() || worldY >= chunk.getWorld().getMaxHeight()) {
|
||||
return;
|
||||
}
|
||||
Block block = chunk.getBlock(x & 15, worldY, z & 15);
|
||||
if (stale.tileReplaced(block, value.getData())) {
|
||||
return;
|
||||
}
|
||||
if (!TileData.setTileState(block, value.getData())) {
|
||||
NamespacedKey blockTypeKey = KeyedType.getKey(block.getType());
|
||||
String blockType = blockTypeKey == null ? block.getType().name() : blockTypeKey.toString();
|
||||
@@ -160,21 +168,25 @@ public final class EngineBukkitOps {
|
||||
}
|
||||
});
|
||||
mantleChunk.deleteSlices(TileWrapper.class);
|
||||
stale.report("tile", chunk);
|
||||
}
|
||||
|
||||
static void materializeCustomBlocks(Engine engine, Chunk chunk, MantleChunk<Matter> mantleChunk) {
|
||||
StaleRecords stale = new StaleRecords();
|
||||
mantleChunk.iterate(Identifier.class, (x, y, z, value) -> {
|
||||
int worldY = y + engine.getWorld().minHeight();
|
||||
if (worldY < chunk.getWorld().getMinHeight() || worldY >= chunk.getWorld().getMaxHeight()) {
|
||||
return;
|
||||
}
|
||||
IrisServices.get(ExternalDataSVC.class).processUpdate(
|
||||
engine,
|
||||
chunk.getBlock(x & 15, worldY, z & 15),
|
||||
value
|
||||
);
|
||||
ExternalDataSVC service = IrisServices.get(ExternalDataSVC.class);
|
||||
Block block = chunk.getBlock(x & 15, worldY, z & 15);
|
||||
if (stale.carrierReplaced(service, block, value)) {
|
||||
return;
|
||||
}
|
||||
service.processUpdate(engine, block, value);
|
||||
});
|
||||
mantleChunk.deleteSlices(Identifier.class);
|
||||
stale.report("custom block", chunk);
|
||||
}
|
||||
|
||||
static void materializeUpdates(Engine engine, Chunk chunk, MantleChunk<Matter> mantleChunk) {
|
||||
@@ -855,4 +867,47 @@ 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);
|
||||
}
|
||||
if (carrier == Material.AIR || block.getType() == carrier) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+265
@@ -0,0 +1,265 @@
|
||||
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 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);
|
||||
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