mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-25 21:40:05 +00:00
Fixed native POI ownership
- Native structure POI changes persist dirty chunk sections before deferred updates - Chunk load reconciles final POI states on the owning thread - Matching POI records retain claimed tickets - Native flag detection keeps unsupported server behavior intact - Regression tests cover persistence and failed repair retries
This commit is contained in:
1 parent
f3b6a0ec1b
commit
a28614c04e
7 files changed
+474
-3
No files matched your search
+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
|
@Override
|
||||||
public boolean clearChunkBlocks(Chunk bukkitChunk) {
|
public boolean clearChunkBlocks(Chunk bukkitChunk) {
|
||||||
try {
|
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.core.particles.ParticleOptions;
|
||||||
import net.minecraft.server.MinecraftServer;
|
import net.minecraft.server.MinecraftServer;
|
||||||
import net.minecraft.server.level.ServerLevel;
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.server.level.WorldGenRegion;
|
||||||
import net.minecraft.sounds.SoundEvent;
|
import net.minecraft.sounds.SoundEvent;
|
||||||
import net.minecraft.sounds.SoundSource;
|
import net.minecraft.sounds.SoundSource;
|
||||||
import net.minecraft.util.Mth;
|
import net.minecraft.util.Mth;
|
||||||
@@ -59,6 +60,7 @@ import java.util.function.Supplier;
|
|||||||
|
|
||||||
final class NativeStructureWorldgenAccess implements WorldGenLevel {
|
final class NativeStructureWorldgenAccess implements WorldGenLevel {
|
||||||
private static final int WRITE_RADIUS = 1;
|
private static final int WRITE_RADIUS = 1;
|
||||||
|
private static final NativeStructurePoiUpdates POI_UPDATES = NativeStructurePoiUpdates.create();
|
||||||
|
|
||||||
private final WorldGenLevel delegate;
|
private final WorldGenLevel delegate;
|
||||||
private final ChunkPos generationCenter;
|
private final ChunkPos generationCenter;
|
||||||
@@ -431,17 +433,31 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean setBlock(BlockPos position, BlockState state, int updateFlags, int updateLimit) {
|
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
|
@Override
|
||||||
public boolean removeBlock(BlockPos position, boolean move) {
|
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
|
@Override
|
||||||
public boolean destroyBlock(BlockPos position, boolean drop, Entity source, int updateLimit) {
|
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
|
@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 flushChunkIO(World world) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
default void reconcileNativeStructurePois(Chunk chunk) {
|
||||||
|
}
|
||||||
|
|
||||||
void injectBiomesFromMantle(Chunk e, Mantle<Matter> mantle);
|
void injectBiomesFromMantle(Chunk e, Mantle<Matter> mantle);
|
||||||
|
|
||||||
ItemStack applyCustomNbt(ItemStack itemStack, KMap<String, Object> customNbt) throws IllegalArgumentException;
|
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.platform.bukkit.BukkitWorldBinding;
|
||||||
import art.arcane.iris.core.IrisSettings;
|
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.data.cache.Cache;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
import art.arcane.iris.engine.framework.EngineAssignedWorldManager;
|
import art.arcane.iris.engine.framework.EngineAssignedWorldManager;
|
||||||
@@ -212,6 +213,8 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
INMS.get().reconcileNativeStructurePois(e);
|
||||||
|
|
||||||
if (cleanupServiceStopped || cleanupService == null || cleanupService.isShutdown()) {
|
if (cleanupServiceStopped || cleanupService == null || cleanupService.isShutdown()) {
|
||||||
return;
|
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