mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-11 19:14:11 +00:00
Preserved native POI ownership
- Native structure placement records pending POI repairs before block changes - Chunk loads reconcile affected sections on their owning thread - Matching POI records retain occupied tickets across repair - Native capability checks preserve supported version behavior
This commit is contained in:
+5
@@ -1150,6 +1150,11 @@ public class NMSBinding implements INMSBinding {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reconcileNativeStructurePois(Chunk chunk) {
|
||||
NativeStructurePoiUpdates.reconcile((LevelChunk) ((CraftChunk) chunk).getHandle(ChunkStatus.FULL));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean clearChunkBlocks(Chunk bukkitChunk) {
|
||||
try {
|
||||
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
|
||||
import ca.spottedleaf.moonrise.common.util.TickThread;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.SectionPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.entity.ai.village.poi.PoiManager;
|
||||
import net.minecraft.world.entity.ai.village.poi.PoiRecord;
|
||||
import net.minecraft.world.entity.ai.village.poi.PoiSection;
|
||||
import net.minecraft.world.entity.ai.village.poi.PoiType;
|
||||
import net.minecraft.world.entity.ai.village.poi.PoiTypes;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.ImposterProtoChunk;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.minecraft.world.level.chunk.LevelChunkSection;
|
||||
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
|
||||
final class NativeStructurePoiUpdates {
|
||||
private static final NamespacedKey DIRTY_SECTIONS = new NamespacedKey("iris", "native_poi_sections");
|
||||
|
||||
private final int skipPoiFlag;
|
||||
|
||||
NativeStructurePoiUpdates(int skipPoiFlag) {
|
||||
this.skipPoiFlag = skipPoiFlag;
|
||||
}
|
||||
|
||||
static NativeStructurePoiUpdates create() {
|
||||
try {
|
||||
return new NativeStructurePoiUpdates(Block.class.getField("UPDATE_SKIP_POI").getInt(null));
|
||||
} catch (NoSuchFieldException e) {
|
||||
return null;
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new ExceptionInInitializerError(e);
|
||||
}
|
||||
}
|
||||
|
||||
boolean setBlock(WorldGenLevel world, BlockPos position, BlockState state, int flags, int limit) {
|
||||
if ((flags & skipPoiFlag) != 0) {
|
||||
return world.setBlock(position, state, flags, limit);
|
||||
}
|
||||
ChunkAccess chunk = world.getChunk(position);
|
||||
if (!(chunk instanceof ProtoChunk)) {
|
||||
return world.setBlock(position, state, flags, limit);
|
||||
}
|
||||
if (!(chunk instanceof ImposterProtoChunk)
|
||||
&& !PoiTypes.forState(chunk.getBlockState(position)).equals(PoiTypes.forState(state))) {
|
||||
markSection(chunk, chunk.getSectionIndex(position.getY()));
|
||||
}
|
||||
return world.setBlock(position, state, flags | skipPoiFlag, limit);
|
||||
}
|
||||
|
||||
static void reconcile(LevelChunk chunk) {
|
||||
long[] sections = chunk.persistentDataContainer.get(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY);
|
||||
if (sections == null) {
|
||||
return;
|
||||
}
|
||||
validateSections(chunk, sections);
|
||||
ServerLevel world = (ServerLevel) chunk.getLevel();
|
||||
TickThread.ensureTickThread(world, chunk.getPos(), "Native structure POIs require the chunk owning thread");
|
||||
PoiManager manager = world.getPoiManager();
|
||||
for (int word = 0; word < sections.length; word++) {
|
||||
long remaining = sections[word];
|
||||
while (remaining != 0) {
|
||||
int index = (word << 6) + Long.numberOfTrailingZeros(remaining);
|
||||
reconcileSection(chunk, manager, index);
|
||||
remaining &= remaining - 1;
|
||||
}
|
||||
}
|
||||
chunk.persistentDataContainer.remove(DIRTY_SECTIONS);
|
||||
chunk.markUnsaved();
|
||||
}
|
||||
|
||||
private static void markSection(ChunkAccess chunk, int index) {
|
||||
long[] sections = chunk.persistentDataContainer.get(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY);
|
||||
int word = index >>> 6;
|
||||
long bit = 1L << (index & 63);
|
||||
if (sections == null) {
|
||||
sections = new long[word + 1];
|
||||
} else {
|
||||
validateSections(chunk, sections);
|
||||
if (word < sections.length && (sections[word] & bit) != 0) {
|
||||
return;
|
||||
}
|
||||
if (word >= sections.length) {
|
||||
sections = Arrays.copyOf(sections, word + 1);
|
||||
}
|
||||
}
|
||||
sections[word] |= bit;
|
||||
chunk.persistentDataContainer.set(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY, sections);
|
||||
}
|
||||
|
||||
private static void validateSections(ChunkAccess chunk, long[] sections) {
|
||||
int count = chunk.getSectionsCount();
|
||||
int words = (count + 63) >>> 6;
|
||||
if (sections.length > words
|
||||
|| sections.length == words && (count & 63) != 0
|
||||
&& (sections[words - 1] >>> (count & 63)) != 0) {
|
||||
throw new IllegalArgumentException("Native POI repair sections exceed chunk height at " + chunk.getPos());
|
||||
}
|
||||
}
|
||||
|
||||
private static void reconcileSection(LevelChunk chunk, PoiManager manager, int index) {
|
||||
SectionPos sectionPosition = SectionPos.of(chunk.getPos(), chunk.getSectionYFromSectionIndex(index));
|
||||
Optional<PoiSection> existing = manager.getOrLoad(sectionPosition.asLong());
|
||||
if (existing.isPresent()) {
|
||||
for (PoiRecord record : existing.get().getRecords(type -> true, PoiManager.Occupancy.ANY).toList()) {
|
||||
if (!PoiTypes.forState(chunk.getBlockState(record.getPos())).equals(Optional.of(record.getPoiType()))) {
|
||||
manager.remove(record.getPos());
|
||||
}
|
||||
}
|
||||
}
|
||||
LevelChunkSection section = chunk.getSection(index);
|
||||
if (!section.maybeHas(PoiTypes::hasPoi)) {
|
||||
return;
|
||||
}
|
||||
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
|
||||
for (int y = 0; y < 16; y++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
for (int x = 0; x < 16; x++) {
|
||||
Optional<Holder<PoiType>> type = PoiTypes.forState(section.getBlockState(x, y, z));
|
||||
if (type.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
position.set(sectionPosition.minBlockX() + x, sectionPosition.minBlockY() + y,
|
||||
sectionPosition.minBlockZ() + z);
|
||||
if (!manager.getType(position).equals(type)) {
|
||||
manager.add(position.immutable(), type.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
-3
@@ -11,6 +11,7 @@ import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.particles.ParticleOptions;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.WorldGenRegion;
|
||||
import net.minecraft.sounds.SoundEvent;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.util.Mth;
|
||||
@@ -59,6 +60,7 @@ import java.util.function.Supplier;
|
||||
|
||||
final class NativeStructureWorldgenAccess implements WorldGenLevel {
|
||||
private static final int WRITE_RADIUS = 1;
|
||||
private static final NativeStructurePoiUpdates POI_UPDATES = NativeStructurePoiUpdates.create();
|
||||
|
||||
private final WorldGenLevel delegate;
|
||||
private final ChunkPos generationCenter;
|
||||
@@ -431,17 +433,31 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
|
||||
|
||||
@Override
|
||||
public boolean setBlock(BlockPos position, BlockState state, int updateFlags, int updateLimit) {
|
||||
return isWritable(position) && delegate.setBlock(position, state, updateFlags, updateLimit);
|
||||
return isWritable(position)
|
||||
&& (POI_UPDATES != null
|
||||
? POI_UPDATES.setBlock(delegate, position, state, updateFlags, updateLimit)
|
||||
: delegate.setBlock(position, state, updateFlags, updateLimit));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeBlock(BlockPos position, boolean move) {
|
||||
return isWritable(position) && delegate.removeBlock(position, move);
|
||||
if (!isWritable(position)) {
|
||||
return false;
|
||||
}
|
||||
return POI_UPDATES != null && delegate instanceof WorldGenRegion
|
||||
? setBlock(position, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL, Block.UPDATE_LIMIT)
|
||||
: delegate.removeBlock(position, move);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean destroyBlock(BlockPos position, boolean drop, Entity source, int updateLimit) {
|
||||
return isWritable(position) && delegate.destroyBlock(position, drop, source, updateLimit);
|
||||
if (!isWritable(position)) {
|
||||
return false;
|
||||
}
|
||||
return POI_UPDATES != null && delegate instanceof WorldGenRegion
|
||||
? !delegate.getBlockState(position).isAir()
|
||||
&& setBlock(position, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL, updateLimit)
|
||||
: delegate.destroyBlock(position, drop, source, updateLimit);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
|
||||
import ca.spottedleaf.moonrise.common.util.TickThread;
|
||||
import com.mojang.serialization.Codec;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.IdMapper;
|
||||
import net.minecraft.core.SectionPos;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.entity.ai.village.poi.PoiManager;
|
||||
import net.minecraft.world.entity.ai.village.poi.PoiRecord;
|
||||
import net.minecraft.world.entity.ai.village.poi.PoiSection;
|
||||
import net.minecraft.world.entity.ai.village.poi.PoiType;
|
||||
import net.minecraft.world.entity.ai.village.poi.PoiTypes;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ImposterProtoChunk;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.minecraft.world.level.chunk.PalettedContainer;
|
||||
import net.minecraft.world.level.chunk.PalettedContainerFactory;
|
||||
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||
import net.minecraft.world.level.chunk.Strategy;
|
||||
import net.minecraft.world.level.chunk.UpgradeData;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry;
|
||||
import org.bukkit.craftbukkit.persistence.DirtyCraftPersistentDataContainer;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.FutureTask;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class NativeStructurePoiUpdatesTest {
|
||||
private static final NamespacedKey DIRTY_SECTIONS = new NamespacedKey("iris", "native_poi_sections");
|
||||
|
||||
@BeforeClass
|
||||
public static void bootstrapMinecraftRegistries() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generationRecordsIntentBeforeSuppressingNativePoiCallbacks() {
|
||||
Fixture fixture = new Fixture();
|
||||
BlockPos position = new BlockPos(-31, 5, 49);
|
||||
doAnswer(call -> {
|
||||
assertNotNull(fixture.proto.persistentDataContainer.get(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY));
|
||||
assertEquals(Block.UPDATE_CLIENTS | 4096, (int) call.getArgument(2));
|
||||
fixture.proto.setBlockState(position, call.getArgument(1), 0);
|
||||
return true;
|
||||
}).when(fixture.world).setBlock(any(), any(), anyInt(), anyInt());
|
||||
|
||||
assertTrue(fixture.updates.setBlock(fixture.world, position,
|
||||
Blocks.COMPOSTER.defaultBlockState(), Block.UPDATE_CLIENTS, 17));
|
||||
assertArrayEquals(new long[]{16L}, fixture.marker());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void denseWritesStoreOneSectionBitAndSurvivePersistentDataRoundTrip() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
for (int z = 0; z < 16; z++) {
|
||||
for (int x = 0; x < 16; x++) {
|
||||
fixture.write(new BlockPos(-32 + x, 5, 48 + z), Blocks.COMPOSTER.defaultBlockState());
|
||||
}
|
||||
}
|
||||
assertArrayEquals(new long[]{16L}, fixture.marker());
|
||||
DirtyCraftPersistentDataContainer restored = new DirtyCraftPersistentDataContainer(new CraftPersistentDataTypeRegistry());
|
||||
restored.putAll(fixture.proto.persistentDataContainer.toTagCompound());
|
||||
fixture.loaded.persistentDataContainer = restored;
|
||||
|
||||
onOwner(() -> NativeStructurePoiUpdates.reconcile(fixture.loaded));
|
||||
|
||||
assertEquals(256L, fixture.records().getRecords(type -> true, PoiManager.Occupancy.ANY).count());
|
||||
assertFalse(restored.has(DIRTY_SECTIONS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validSectionsReconcileFinalStatesAndKeepMatchingClaimedTickets() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
BlockPos retained = new BlockPos(-31, 5, 49);
|
||||
BlockPos replaced = new BlockPos(-30, 5, 49);
|
||||
BlockPos removed = new BlockPos(-29, 5, 49);
|
||||
Holder<PoiType> farmer = PoiTypes.forState(Blocks.COMPOSTER.defaultBlockState()).orElseThrow();
|
||||
PoiSection existing = new PoiSection.Packed(true, List.of(
|
||||
new PoiRecord.Packed(retained, farmer, 0),
|
||||
new PoiRecord.Packed(replaced, farmer, 1),
|
||||
new PoiRecord.Packed(removed, farmer, 1))).unpack(() -> {});
|
||||
fixture.pois.put(SectionPos.of(retained).asLong(), existing);
|
||||
PoiRecord retainedRecord = existing.getRecords(type -> true, PoiManager.Occupancy.ANY)
|
||||
.filter(record -> record.getPos().equals(retained)).findFirst().orElseThrow();
|
||||
for (BlockPos position : List.of(retained, replaced, removed)) {
|
||||
fixture.proto.setBlockState(position, Blocks.COMPOSTER.defaultBlockState(), 0);
|
||||
}
|
||||
fixture.write(replaced, Blocks.BARREL.defaultBlockState());
|
||||
fixture.write(removed, Blocks.AIR.defaultBlockState());
|
||||
|
||||
onOwner(() -> NativeStructurePoiUpdates.reconcile(fixture.loaded));
|
||||
|
||||
assertEquals(PoiTypes.forState(Blocks.BARREL.defaultBlockState()), existing.getType(replaced));
|
||||
assertTrue(existing.getType(removed).isEmpty());
|
||||
assertSame(retainedRecord, existing.getRecords(type -> true, PoiManager.Occupancy.ANY)
|
||||
.filter(record -> record.getPos().equals(retained)).findFirst().orElseThrow());
|
||||
assertEquals(0, existing.getFreeTickets(retained));
|
||||
onOwner(() -> NativeStructurePoiUpdates.reconcile(fixture.loaded));
|
||||
assertEquals(0, existing.getFreeTickets(retained));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedReconciliationRetainsIntentForRetry() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
fixture.write(new BlockPos(-31, 5, 49), Blocks.COMPOSTER.defaultBlockState());
|
||||
IllegalStateException failure = new IllegalStateException("POI storage failure");
|
||||
doThrow(failure).when(fixture.manager).add(any(), any());
|
||||
|
||||
onOwner(() -> assertSame(failure, assertThrows(IllegalStateException.class,
|
||||
() -> NativeStructurePoiUpdates.reconcile(fixture.loaded))));
|
||||
|
||||
assertArrayEquals(new long[]{16L}, fixture.marker());
|
||||
fixture.allowPoiAdds();
|
||||
onOwner(() -> NativeStructurePoiUpdates.reconcile(fixture.loaded));
|
||||
assertFalse(fixture.proto.persistentDataContainer.has(DIRTY_SECTIONS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void explicitPoiSuppressionAndReadOnlyFullChunksDoNotCreateIntent() {
|
||||
Fixture fixture = new Fixture();
|
||||
BlockPos position = new BlockPos(-31, 5, 49);
|
||||
fixture.updates.setBlock(fixture.world, position, Blocks.COMPOSTER.defaultBlockState(),
|
||||
4096, 17);
|
||||
assertFalse(fixture.proto.persistentDataContainer.has(DIRTY_SECTIONS));
|
||||
ImposterProtoChunk full = mock(ImposterProtoChunk.class);
|
||||
when(fixture.world.getChunk(any(BlockPos.class))).thenReturn(full);
|
||||
fixture.updates.setBlock(fixture.world, position, Blocks.BARREL.defaultBlockState(), 2, 17);
|
||||
assertFalse(fixture.proto.persistentDataContainer.has(DIRTY_SECTIONS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidSectionBitsRetainTheStoredRecord() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
fixture.proto.persistentDataContainer.set(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY, new long[]{1L << 24});
|
||||
onOwner(() -> assertThrows(IllegalArgumentException.class,
|
||||
() -> NativeStructurePoiUpdates.reconcile(fixture.loaded)));
|
||||
assertArrayEquals(new long[]{1L << 24}, fixture.marker());
|
||||
}
|
||||
|
||||
private static void onOwner(Runnable action) throws Exception {
|
||||
FutureTask<Void> task = new FutureTask<>(action, null);
|
||||
new TickThread(task, "Native POI test").start();
|
||||
task.get(5L, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private static PalettedContainerFactory containerFactory() {
|
||||
Strategy<BlockState> blocks = Strategy.createForBlockStates(Block.BLOCK_STATE_REGISTRY);
|
||||
BlockState air = Blocks.AIR.defaultBlockState();
|
||||
IdMapper<Holder<Biome>> biomeIds = new IdMapper<>();
|
||||
Holder<Biome> biome = Holder.direct((Biome) null);
|
||||
biomeIds.add(biome);
|
||||
Strategy<Holder<Biome>> biomes = Strategy.createForBiomes(biomeIds);
|
||||
Codec<Holder<Biome>> biomeCodec = Codec.STRING.xmap(name -> biome, holder -> "iris-test-biome");
|
||||
return new PalettedContainerFactory(blocks, air, PalettedContainer.codecRW(BlockState.CODEC, blocks, air),
|
||||
biomes, biome, PalettedContainer.codecRO(biomeCodec, biomes, biome),
|
||||
PalettedContainer.codecRW(biomeCodec, biomes, biome));
|
||||
}
|
||||
|
||||
private static final class Fixture {
|
||||
private final NativeStructurePoiUpdates updates = new NativeStructurePoiUpdates(4096);
|
||||
private final ProtoChunk proto = new ProtoChunk(new ChunkPos(-2, 3), UpgradeData.EMPTY,
|
||||
LevelHeightAccessor.create(-64, 384), containerFactory(), null);
|
||||
private final LevelChunk loaded = mock(LevelChunk.class);
|
||||
private final WorldGenLevel world = mock(WorldGenLevel.class);
|
||||
private final PoiManager manager = mock(PoiManager.class);
|
||||
private final Map<Long, PoiSection> pois = new HashMap<>();
|
||||
|
||||
private Fixture() {
|
||||
ServerLevel level = mock(ServerLevel.class);
|
||||
when(level.getPoiManager()).thenReturn(manager);
|
||||
loaded.persistentDataContainer = proto.persistentDataContainer;
|
||||
when(loaded.getLevel()).thenReturn(level);
|
||||
when(loaded.getPos()).thenReturn(proto.getPos());
|
||||
when(loaded.getSectionsCount()).thenReturn(proto.getSectionsCount());
|
||||
when(loaded.getSectionYFromSectionIndex(anyInt())).thenAnswer(call -> proto.getSectionYFromSectionIndex(call.getArgument(0)));
|
||||
when(loaded.getSection(anyInt())).thenAnswer(call -> proto.getSection(call.getArgument(0)));
|
||||
when(loaded.getBlockState(any())).thenAnswer(call -> proto.getBlockState(call.getArgument(0)));
|
||||
when(world.getChunk(any(BlockPos.class))).thenReturn(proto);
|
||||
when(world.setBlock(any(), any(), anyInt(), anyInt())).thenAnswer(call -> {
|
||||
proto.setBlockState(call.getArgument(0), call.getArgument(1), call.getArgument(2));
|
||||
return true;
|
||||
});
|
||||
when(manager.getOrLoad(anyLong())).thenAnswer(call -> Optional.ofNullable(pois.get((long) call.getArgument(0))));
|
||||
when(manager.getType(any())).thenAnswer(call -> {
|
||||
BlockPos position = call.getArgument(0);
|
||||
PoiSection section = pois.get(SectionPos.of(position).asLong());
|
||||
return section == null ? Optional.empty() : section.getType(position);
|
||||
});
|
||||
doAnswer(call -> {
|
||||
BlockPos position = call.getArgument(0);
|
||||
pois.get(SectionPos.of(position).asLong()).remove(position);
|
||||
return null;
|
||||
}).when(manager).remove(any());
|
||||
allowPoiAdds();
|
||||
}
|
||||
|
||||
private void allowPoiAdds() {
|
||||
doAnswer(call -> {
|
||||
BlockPos position = call.getArgument(0);
|
||||
return pois.computeIfAbsent(SectionPos.of(position).asLong(), ignored -> new PoiSection(() -> {}))
|
||||
.add(position, call.getArgument(1));
|
||||
}).when(manager).add(any(), any());
|
||||
}
|
||||
|
||||
private void write(BlockPos position, BlockState state) {
|
||||
updates.setBlock(world, position, state, Block.UPDATE_CLIENTS, Block.UPDATE_LIMIT);
|
||||
}
|
||||
|
||||
private long[] marker() {
|
||||
return proto.persistentDataContainer.get(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY);
|
||||
}
|
||||
|
||||
private PoiSection records() {
|
||||
return pois.get(SectionPos.of(new BlockPos(-31, 5, 49)).asLong());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -245,6 +245,9 @@ public interface INMSBinding {
|
||||
default void flushChunkIO(World world) {
|
||||
}
|
||||
|
||||
default void reconcileNativeStructurePois(Chunk chunk) {
|
||||
}
|
||||
|
||||
void injectBiomesFromMantle(Chunk e, Mantle<Matter> mantle);
|
||||
|
||||
ItemStack applyCustomNbt(ItemStack itemStack, KMap<String, Object> customNbt) throws IllegalArgumentException;
|
||||
|
||||
@@ -20,6 +20,7 @@ package art.arcane.iris.engine;
|
||||
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.nms.INMS;
|
||||
import art.arcane.iris.engine.data.cache.Cache;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineAssignedWorldManager;
|
||||
@@ -212,6 +213,8 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
||||
return;
|
||||
}
|
||||
|
||||
INMS.get().reconcileNativeStructurePois(e);
|
||||
|
||||
if (cleanupServiceStopped || cleanupService == null || cleanupService.isShutdown()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package art.arcane.iris.engine;
|
||||
|
||||
import art.arcane.iris.core.nms.INMS;
|
||||
import art.arcane.iris.core.nms.INMSBinding;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import org.bukkit.Chunk;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisWorldManagerNativePoiTest {
|
||||
@Test
|
||||
public void chunkLoadReconcilesPoisWithoutAnOptionalCleanupExecutor() {
|
||||
IrisWorldManager manager = mock(IrisWorldManager.class, CALLS_REAL_METHODS);
|
||||
Engine engine = mock(Engine.class);
|
||||
Chunk chunk = mock(Chunk.class);
|
||||
INMSBinding binding = mock(INMSBinding.class);
|
||||
when(manager.getEngine()).thenReturn(engine);
|
||||
|
||||
try (MockedStatic<INMS> nms = mockStatic(INMS.class)) {
|
||||
nms.when(INMS::get).thenReturn(binding);
|
||||
manager.onChunkLoad(chunk, false);
|
||||
}
|
||||
|
||||
verify(binding).reconcileNativeStructurePois(chunk);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void chunkLoadPropagatesPoiFailuresToTheRuntimeEventReporter() {
|
||||
IrisWorldManager manager = mock(IrisWorldManager.class, CALLS_REAL_METHODS);
|
||||
Engine engine = mock(Engine.class);
|
||||
Chunk chunk = mock(Chunk.class);
|
||||
INMSBinding binding = mock(INMSBinding.class);
|
||||
IllegalStateException failure = new IllegalStateException("Native POI storage failure");
|
||||
when(manager.getEngine()).thenReturn(engine);
|
||||
doThrow(failure).when(binding).reconcileNativeStructurePois(chunk);
|
||||
|
||||
try (MockedStatic<INMS> nms = mockStatic(INMS.class)) {
|
||||
nms.when(INMS::get).thenReturn(binding);
|
||||
assertSame(failure, assertThrows(IllegalStateException.class,
|
||||
() -> manager.onChunkLoad(chunk, true)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user