Testing Jar Packing for minimization

This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-24 06:46:27 -04:00
1 parent 38cf4ac7c7
commit 68aa6adec7
13 files changed
+2604 -14

No files matched your search

@@ -0,0 +1,259 @@
package art.arcane.iris.probe;
import ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk;
import ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray;
import net.minecraft.core.BlockPos;
import net.minecraft.core.SectionPos;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.DataLayer;
import net.minecraft.world.level.chunk.LightChunk;
import net.minecraft.world.level.chunk.LightChunkGetter;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.lighting.BlockLightEngine;
import net.minecraft.world.level.lighting.ChunkSkyLightSources;
import net.minecraft.world.level.lighting.LayerLightEventListener;
import net.minecraft.world.level.lighting.SkyLightEngine;
import net.minecraft.world.level.material.FluidState;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiConsumer;
final class HeadlessChunkLighting {
private static final int MAXIMUM_WINDOW_CHUNKS = 256;
private static final int HALO = 2;
private HeadlessChunkLighting() {
}
static void light(Request request) {
if (Thread.currentThread().isInterrupted()) {
throw new IllegalStateException("Offline lighting interrupted before initialization");
}
Window window = new Window(request);
BlockLightEngine block = new BlockLightEngine(window);
SkyLightEngine sky = request.skyLight() ? new SkyLightEngine(window) : null;
for (LightingChunk chunk : window.chunks.values()) {
ProtoChunk source = chunk.source;
for (int section = source.getMinSectionY(); section <= source.getMaxSectionY(); section++) {
SectionPos position = SectionPos.of(source.getPos(), section);
boolean empty = source.getSection(source.getSectionIndexFromSectionY(section)).hasOnlyAir();
block.updateSectionStatus(position, empty);
if (sky != null) {
sky.updateSectionStatus(position, empty);
}
}
}
for (LightingChunk chunk : window.chunks.values()) {
block.propagateLightSources(chunk.source.getPos());
if (sky != null) {
sky.propagateLightSources(chunk.source.getPos());
}
}
drain(block);
if (sky != null) {
drain(sky);
}
for (ChunkPos position : request.targets()) {
ProtoChunk chunk = window.chunks.get(position.pack()).source;
StarlightChunk destination = (StarlightChunk) chunk;
destination.starlight$setBlockNibbles(copyLight(block, chunk));
destination.starlight$setSkyNibbles(copyLight(sky, chunk));
boolean[] emptySections = new boolean[chunk.getSectionsCount()];
for (int index = 0; index < emptySections.length; index++) {
emptySections[index] = chunk.getSection(index).hasOnlyAir();
}
destination.starlight$setBlockEmptinessMap(emptySections.clone());
destination.starlight$setSkyEmptinessMap(emptySections);
chunk.setPersistedStatus(ChunkStatus.LIGHT);
chunk.setLightCorrect(true);
}
}
private static void drain(LayerLightEventListener engine) {
while (engine.hasLightWork()) {
if (Thread.currentThread().isInterrupted()) {
throw new IllegalStateException("Offline lighting interrupted before completion");
}
engine.runLightUpdates();
}
}
private static SWMRNibbleArray[] copyLight(LayerLightEventListener engine, ProtoChunk chunk) {
SWMRNibbleArray[] result = new SWMRNibbleArray[chunk.getSectionsCount() + 2];
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
for (int index = 0; index < result.length; index++) {
int sectionY = chunk.getMinSectionY() + index - 1;
DataLayer layer = engine == null ? new DataLayer() : engine.getDataLayerData(SectionPos.of(chunk.getPos(), sectionY));
if (layer == null) {
layer = new DataLayer();
for (int y = 0; y < 16; y++) {
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
position.set(chunk.getPos().getMinBlockX() + x, (sectionY << 4) + y,
chunk.getPos().getMinBlockZ() + z);
layer.set(x, y, z, engine.getLightValue(position));
}
}
}
}
result[index] = SWMRNibbleArray.fromVanilla(layer);
}
return result;
}
record Request(List<ProtoChunk> chunks, Set<ChunkPos> targets, boolean skyLight) {
Request {
chunks = List.copyOf(chunks);
targets = Set.copyOf(targets);
if (chunks.isEmpty() || chunks.size() > MAXIMUM_WINDOW_CHUNKS || targets.isEmpty()) {
throw new IllegalArgumentException("Offline lighting requires targets inside a bounded chunk window");
}
}
}
private static final class Window implements LightChunkGetter, BlockGetter {
private final Map<Long, LightingChunk> chunks;
private final int minimumY;
private final int height;
private Window(Request request) {
minimumY = request.chunks().getFirst().getMinY();
height = request.chunks().getFirst().getHeight();
chunks = new HashMap<>(request.chunks().size());
for (ProtoChunk chunk : request.chunks()) {
if (chunk.getMinY() != minimumY || chunk.getHeight() != height
|| chunk.getPersistedStatus() != ChunkStatus.FEATURES) {
throw new IllegalArgumentException("Offline lighting requires matching heights and completed FEATURES chunks");
}
if (chunks.putIfAbsent(chunk.getPos().pack(), new LightingChunk(chunk)) != null) {
throw new IllegalArgumentException("Duplicate lighting chunk: " + chunk.getPos());
}
}
for (ChunkPos target : request.targets()) {
for (int dz = -HALO; dz <= HALO; dz++) {
for (int dx = -HALO; dx <= HALO; dx++) {
if (!chunks.containsKey(ChunkPos.pack(Math.addExact(target.x(), dx), Math.addExact(target.z(), dz)))) {
throw new IllegalArgumentException("Offline lighting target lacks its completed neighbor halo: " + target);
}
}
}
}
}
@Override
public LightChunk getChunkForLighting(int x, int z) {
return chunks.get(ChunkPos.pack(x, z));
}
@Override
public BlockGetter getLevel() {
return this;
}
@Override
public int getMinY() {
return minimumY;
}
@Override
public int getHeight() {
return height;
}
@Override
public BlockEntity getBlockEntity(BlockPos position) {
return requireChunk(position).getBlockEntity(position);
}
@Override
public BlockState getBlockState(BlockPos position) {
return requireChunk(position).getBlockState(position);
}
@Override
public BlockState getBlockStateIfLoaded(BlockPos position) {
LightingChunk chunk = chunks.get(ChunkPos.pack(position.getX() >> 4, position.getZ() >> 4));
return chunk == null ? null : chunk.getBlockState(position);
}
@Override
public FluidState getFluidState(BlockPos position) {
return getBlockState(position).getFluidState();
}
@Override
public FluidState getFluidIfLoaded(BlockPos position) {
BlockState state = getBlockStateIfLoaded(position);
return state == null ? null : state.getFluidState();
}
private LightingChunk requireChunk(BlockPos position) {
return Objects.requireNonNull(chunks.get(ChunkPos.pack(position.getX() >> 4, position.getZ() >> 4)),
"Lighting query outside the supplied chunk window");
}
}
private static final class LightingChunk implements LightChunk {
private final ProtoChunk source;
private final ChunkSkyLightSources skySources;
private LightingChunk(ProtoChunk source) {
this.source = source;
skySources = new ChunkSkyLightSources(source);
skySources.fillFrom(source);
}
@Override
public void findBlockLightSources(BiConsumer<BlockPos, BlockState> consumer) {
source.findBlockLightSources(consumer);
}
@Override
public ChunkSkyLightSources getSkyLightSources() {
return skySources;
}
@Override
public BlockEntity getBlockEntity(BlockPos position) {
return source.getBlockEntity(position);
}
@Override
public BlockState getBlockState(BlockPos position) {
return source.getBlockState(position);
}
@Override
public BlockState getBlockStateIfLoaded(BlockPos position) {
return source.getBlockStateIfLoaded(position);
}
@Override
public FluidState getFluidState(BlockPos position) {
return source.getFluidState(position);
}
@Override
public FluidState getFluidIfLoaded(BlockPos position) {
return source.getFluidIfLoaded(position);
}
@Override
public int getMinY() {
return source.getMinY();
}
@Override
public int getHeight() {
return source.getHeight();
}
}
}
@@ -0,0 +1,260 @@
package art.arcane.iris.probe;
import ca.spottedleaf.moonrise.patches.starlight.chunk.StarlightChunk;
import ca.spottedleaf.moonrise.patches.starlight.light.StarLightInterface;
import ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray;
import ca.spottedleaf.moonrise.patches.starlight.util.SaveUtil;
import net.minecraft.core.BlockPos;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtAccounter;
import net.minecraft.nbt.NbtIo;
import net.minecraft.server.packs.PackType;
import net.minecraft.server.packs.repository.ServerPacksSource;
import net.minecraft.server.packs.resources.MultiPackResourceManager;
import net.minecraft.util.datafix.DataFixers;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.LightChunk;
import net.minecraft.world.level.chunk.LightChunkGetter;
import net.minecraft.world.level.chunk.PalettedContainerFactory;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.UpgradeData;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.chunk.storage.SerializableChunkData;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import net.minecraft.world.level.storage.LevelStorageSource;
import org.bukkit.Bukkit;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public final class HeadlessChunkLightingTest {
@Rule
public TemporaryFolder temporary = new TemporaryFolder();
@Test
public void nativeLightCrossesChunkEdgesAndRoundTripsSavedNibblesWithoutServer() throws Exception {
Fixture fixture = new Fixture(-1, -2);
int edgeX = fixture.target.x() * 16;
int z = fixture.target.z() * 16 + 8;
for (ProtoChunk chunk : fixture.chunks) {
for (int x = 0; x < 16; x++) {
for (int localZ = 0; localZ < 16; localZ++) {
chunk.setBlockState(new BlockPos(chunk.getPos().getMinBlockX() + x, 0,
chunk.getPos().getMinBlockZ() + localZ), Blocks.STONE.defaultBlockState(), 0);
}
}
}
fixture.set(new BlockPos(edgeX - 1, 0, z), Blocks.AIR.defaultBlockState());
fixture.set(new BlockPos(edgeX - 1, -12, z), Blocks.GLOWSTONE.defaultBlockState());
fixture.finishFeatures();
assertNull(Bukkit.getServer());
HeadlessChunkLighting.light(fixture.request(true));
ProtoChunk target = fixture.chunk(fixture.target);
assertEquals(14, light(target, false, new BlockPos(edgeX, -12, z)));
assertEquals(13, light(target, false, new BlockPos(edgeX + 1, -12, z)));
assertEquals(14, light(target, true, new BlockPos(edgeX, -1, z)));
assertEquals(15, light(target, true, new BlockPos(edgeX, 1, z)));
assertEquals(0, light(target, true, new BlockPos(edgeX + 15, -1, z)));
assertEquals(ChunkStatus.LIGHT, target.getPersistedStatus());
assertTrue(target.isLightCorrect());
assertEquals(ChunkStatus.FEATURES, fixture.chunk(new ChunkPos(fixture.target.x() - 2, fixture.target.z())).getPersistedStatus());
assertFalse(fixture.chunk(new ChunkPos(fixture.target.x() - 2, fixture.target.z())).isLightCorrect());
try (MultiPackResourceManager resources = new MultiPackResourceManager(PackType.SERVER_DATA,
List.of(ServerPacksSource.createVanillaPackSource().fullResources()));
LevelStorageSource.LevelStorageAccess storage = LevelStorageSource.createDefault(
temporary.newFolder().toPath()).createAccess("lighting")) {
StructureTemplateManager templates = new StructureTemplateManager(resources, storage,
DataFixers.getDataFixer(), BuiltInRegistries.BLOCK);
HeadlessChunkSerialization.Context context = new HeadlessChunkSerialization.Context(
new StructurePieceSerializationContext(resources, fixture.registries, templates), fixture.containers, 0L);
CompoundTag tag = HeadlessChunkSerialization.copyOf(target, context).write();
assertEquals("minecraft:light", tag.getStringOr("Status", ""));
assertEquals(SaveUtil.STARLIGHT_LIGHT_VERSION, tag.getIntOr(SaveUtil.STARLIGHT_VERSION_TAG, -1));
assertTrue(tag.contains("isLightOn"));
assertFalse(tag.getBooleanOr("isLightOn", true));
assertThrows(ClassCastException.class, () -> SerializableChunkData.parse(target, fixture.containers, tag));
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
NbtIo.writeCompressed(tag, bytes);
CompoundTag restored = NbtIo.readCompressed(new ByteArrayInputStream(bytes.toByteArray()), NbtAccounter.unlimitedHeap());
assertEquals(tag, restored);
List<CompoundTag> sections = restored.getListOrEmpty("sections").compoundStream().toList();
assertEquals(target.getSectionsCount() + 2, sections.size());
for (CompoundTag section : sections) {
int index = section.getByteOr("Y", (byte) 0) - target.getMinSectionY() + 1;
assertTrue(section.contains(SaveUtil.BLOCKLIGHT_STATE_TAG));
assertTrue(section.contains(SaveUtil.SKYLIGHT_STATE_TAG));
SWMRNibbleArray block = new SWMRNibbleArray(section.getByteArray("BlockLight").orElse(null),
section.getIntOr(SaveUtil.BLOCKLIGHT_STATE_TAG, 0));
SWMRNibbleArray sky = new SWMRNibbleArray(section.getByteArray("SkyLight").orElse(null),
section.getIntOr(SaveUtil.SKYLIGHT_STATE_TAG, 0));
assertNibbleEquals(((StarlightChunk) target).starlight$getBlockNibbles()[index], block);
assertNibbleEquals(((StarlightChunk) target).starlight$getSkyNibbles()[index], sky);
}
}
assertNull(Bukkit.getServer());
}
@Test
public void nativeOpacityAndShapeRulesApplyAcrossTheBoundaryWithoutSkylight() {
for (BlockState barrier : List.of(Blocks.GLASS.defaultBlockState(), Blocks.TINTED_GLASS.defaultBlockState(),
Blocks.STONE_SLAB.defaultBlockState(), Blocks.STONE.defaultBlockState())) {
Fixture fixture = new Fixture(0, 0);
for (int x = -2; x <= 3; x++) {
for (int y = -13; y <= -11; y++) {
for (int z = 7; z <= 9; z++) {
fixture.set(new BlockPos(x, y, z), Blocks.STONE.defaultBlockState());
}
}
}
for (int x = -1; x <= 2; x++) {
fixture.set(new BlockPos(x, -12, 8), Blocks.AIR.defaultBlockState());
}
fixture.set(new BlockPos(-1, -12, 8), Blocks.GLOWSTONE.defaultBlockState());
fixture.set(new BlockPos(0, -12, 8), barrier);
fixture.finishFeatures();
HeadlessChunkLighting.light(fixture.request(false));
ProtoChunk target = fixture.chunk(fixture.target);
int expected = barrier.is(Blocks.GLASS) || barrier.is(Blocks.STONE_SLAB) ? 13 : 0;
assertEquals(barrier.toString(), expected, light(target, false, new BlockPos(1, -12, 8)));
assertEquals(0, light(target, true, new BlockPos(1, 20, 8)));
}
assertNull(Bukkit.getServer());
}
@Test
public void emptySectionsRetainImplicitSkyLightAcrossFullHeightAndPadding() {
for (boolean sky : List.of(true, false)) {
Fixture fixture = new Fixture(0, 0);
fixture.finishFeatures();
HeadlessChunkLighting.light(fixture.request(sky));
ProtoChunk target = fixture.chunk(fixture.target);
for (int y = target.getMinY() - 16; y <= target.getMaxY() + 16; y++) {
assertEquals("sky y=" + y, sky ? 15 : 0, light(target, true, new BlockPos(8, y, 8)));
assertEquals("block y=" + y, 0, light(target, false, new BlockPos(8, y, 8)));
}
}
}
@Test
public void interruptionLeavesChunkUnlitAndRetainsInterrupt() {
Fixture fixture = new Fixture(0, 0);
fixture.finishFeatures();
Thread.currentThread().interrupt();
try {
assertThrows(IllegalStateException.class, () -> HeadlessChunkLighting.light(fixture.request(true)));
assertTrue(Thread.currentThread().isInterrupted());
assertFalse(fixture.chunk(fixture.target).isLightCorrect());
assertEquals(ChunkStatus.FEATURES, fixture.chunk(fixture.target).getPersistedStatus());
} finally {
Thread.interrupted();
}
}
@Test
public void rejectsIncompleteNeighborsBeforePromotingOrMutatingLight() {
Fixture fixture = new Fixture(0, 0);
fixture.finishFeatures();
fixture.chunks.removeFirst();
ProtoChunk target = fixture.chunk(fixture.target);
SWMRNibbleArray[] previous = ((StarlightChunk) target).starlight$getBlockNibbles();
assertThrows(IllegalArgumentException.class, () -> HeadlessChunkLighting.light(fixture.request(true)));
assertEquals(ChunkStatus.FEATURES, target.getPersistedStatus());
assertFalse(target.isLightCorrect());
assertArrayEquals(previous, ((StarlightChunk) target).starlight$getBlockNibbles());
fixture.chunks.add(new ProtoChunk(new ChunkPos(-2, -2), UpgradeData.EMPTY, target, fixture.containers, null));
assertThrows(IllegalArgumentException.class, () -> HeadlessChunkLighting.light(fixture.request(true)));
}
@Test
public void directStarlightRequiresLevelButNativeLightEnginesDoNot() {
Fixture fixture = new Fixture(0, 0);
ProtoChunk chunk = fixture.chunk(fixture.target);
LightChunkGetter source = new LightChunkGetter() {
@Override
public LightChunk getChunkForLighting(int x, int z) {
return chunk;
}
@Override
public BlockGetter getLevel() {
return chunk;
}
};
assertThrows(ClassCastException.class, () -> new StarLightInterface(source, true, true, null));
assertNull(Bukkit.getServer());
}
private static int light(ProtoChunk chunk, boolean sky, BlockPos position) {
StarlightChunk light = (StarlightChunk) chunk;
SWMRNibbleArray[] nibbles = sky ? light.starlight$getSkyNibbles() : light.starlight$getBlockNibbles();
return nibbles[(position.getY() >> 4) - chunk.getMinSectionY() + 1]
.getVisible(position.getX(), position.getY(), position.getZ());
}
private static void assertNibbleEquals(SWMRNibbleArray expected, SWMRNibbleArray actual) {
for (int index = 0; index < 4096; index++) {
assertEquals(expected.getVisible(index), actual.getVisible(index));
}
}
private static final class Fixture {
private final RegistryAccess registries = HeadlessNativeTestRegistries.get();
private final PalettedContainerFactory containers = PalettedContainerFactory.create(registries);
private final List<ProtoChunk> chunks = new ArrayList<>();
private final ChunkPos target;
private Fixture(int x, int z) {
target = new ChunkPos(x, z);
LevelHeightAccessor height = LevelHeightAccessor.create(-64, 128);
for (int dz = -2; dz <= 2; dz++) {
for (int dx = -2; dx <= 2; dx++) {
chunks.add(new ProtoChunk(new ChunkPos(x + dx, z + dz), UpgradeData.EMPTY, height, containers, null));
}
}
}
private ProtoChunk chunk(ChunkPos position) {
for (ProtoChunk chunk : chunks) {
if (chunk.getPos().equals(position)) {
return chunk;
}
}
throw new IllegalArgumentException("Missing fixture chunk " + position);
}
private void set(BlockPos position, BlockState state) {
chunk(new ChunkPos(position.getX() >> 4, position.getZ() >> 4)).setBlockState(position, state, 0);
}
private void finishFeatures() {
for (ProtoChunk chunk : chunks) {
chunk.setPersistedStatus(ChunkStatus.FEATURES);
}
}
private HeadlessChunkLighting.Request request(boolean sky) {
return new HeadlessChunkLighting.Request(chunks, Set.of(target), sky);
}
}
}
@@ -0,0 +1,443 @@
package art.arcane.iris.probe;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.Holder;
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.sounds.SoundEvent;
import net.minecraft.sounds.SoundSource;
import net.minecraft.util.RandomSource;
import net.minecraft.world.DifficultyInstance;
import net.minecraft.world.attribute.EnvironmentAttributeReader;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.flag.FeatureFlagSet;
import net.minecraft.world.flag.FeatureFlags;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LightLayer;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeManager;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.EntityBlock;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.border.WorldBorder;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkSource;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.entity.EntityTypeTest;
import net.minecraft.world.level.gameevent.GameEvent;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.lighting.LevelLightEngine;
import net.minecraft.world.level.material.Fluid;
import net.minecraft.world.level.material.FluidState;
import net.minecraft.world.level.storage.LevelData;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
import net.minecraft.world.ticks.LevelTickAccess;
import net.minecraft.world.ticks.ScheduledTick;
import net.minecraft.world.ticks.TickContainerAccess;
import org.bukkit.event.entity.CreatureSpawnEvent;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Predicate;
final class OfflineFeatureWorld implements WorldGenLevel {
private final Options options;
private final Map<Long, ProtoChunk> chunks = new HashMap<>();
private final RandomSource random;
private final BiomeManager biomeManager;
private final WorldBorder border = new WorldBorder();
private final LevelTickAccess<Block> blockTicks;
private final LevelTickAccess<Fluid> fluidTicks;
private long subTick;
OfflineFeatureWorld(Options options, List<ProtoChunk> chunks) {
this.options = options;
for (ProtoChunk chunk : chunks) {
if (chunk.getPersistedStatus() != ChunkStatus.TERRAIN) {
throw new IllegalArgumentException("Feature fixture requires TERRAIN chunks");
}
if (this.chunks.put(chunk.getPos().pack(), chunk) != null) {
throw new IllegalArgumentException("Duplicate chunk");
}
}
random = RandomSource.create(options.seed());
biomeManager = new BiomeManager(this, BiomeManager.obfuscateSeed(options.seed()));
blockTicks = ticks(ProtoChunk::getBlockTicks);
fluidTicks = ticks(ProtoChunk::getFluidTicks);
}
@Override
public long getGameTime() {
return 0L;
}
@Override
public ServerLevel getMinecraftWorld() {
throw unsupported("ServerLevel");
}
@Override
public long getSeed() {
return options.seed();
}
@Override
public boolean ensureCanWrite(BlockPos position) {
return !isOutsideBuildHeight(position) && hasChunk(position.getX() >> 4, position.getZ() >> 4);
}
@Override
public ServerLevel getLevel() {
throw unsupported("ServerLevel");
}
@Override
public DifficultyInstance getCurrentDifficultyAt(BlockPos position) {
throw unsupported("getCurrentDifficultyAt");
}
@Override
public long nextSubTickCount() {
return subTick++;
}
@Override
public LevelTickAccess<Block> getBlockTicks() {
return blockTicks;
}
@Override
public LevelTickAccess<Fluid> getFluidTicks() {
return fluidTicks;
}
@Override
public LevelData getLevelData() {
throw unsupported("getLevelData");
}
@Override
public MinecraftServer getServer() {
throw unsupported("getServer");
}
@Override
public ChunkSource getChunkSource() {
throw unsupported("getChunkSource");
}
@Override
public RandomSource getRandom() {
return random;
}
@Override
public void updateNeighborsAt(BlockPos position, Block block) {
throw unsupported("updateNeighborsAt");
}
@Override
public void neighborShapeChanged(Direction direction, BlockPos position, BlockPos neighbourPosition, BlockState neighbourState, int updateFlags, int updateLimit) {
throw unsupported("neighborShapeChanged");
}
@Override
public void playSound(Entity source, BlockPos position, SoundEvent sound, SoundSource soundSource, float volume, float pitch) {
throw unsupported("playSound");
}
@Override
public void addParticle(ParticleOptions particle, double x, double y, double z, double velocityX, double velocityY, double velocityZ) {
throw unsupported("addParticle");
}
@Override
public void levelEvent(Entity source, int eventId, BlockPos position, int data) {
throw unsupported("levelEvent");
}
@Override
public void gameEvent(Holder<GameEvent> event, Vec3 position, GameEvent.Context context) {
throw unsupported("gameEvent");
}
@Override
public ChunkAccess getChunk(int chunkX, int chunkZ) {
return chunk(chunkX, chunkZ);
}
@Override
public ChunkAccess getChunk(int chunkX, int chunkZ, ChunkStatus status, boolean create) {
ProtoChunk chunk = chunks.get(ChunkPos.pack(chunkX, chunkZ));
if (chunk == null && !create) {
return null;
}
chunk = chunk(chunkX, chunkZ);
if (!chunk.getPersistedStatus().isOrAfter(status)) {
throw unsupported("chunk status " + status);
}
return chunk;
}
@Override
public ChunkAccess getChunkIfLoadedImmediately(int chunkX, int chunkZ) {
return chunks.get(ChunkPos.pack(chunkX, chunkZ));
}
@Override
public boolean hasChunk(int chunkX, int chunkZ) {
return chunks.containsKey(ChunkPos.pack(chunkX, chunkZ));
}
@Override
public int getHeight(Heightmap.Types type, int x, int z) {
return chunk(x >> 4, z >> 4).getHeight(type, x & 15, z & 15) + 1;
}
@Override
public int getSkyDarken() {
throw unsupported("getSkyDarken");
}
@Override
public BiomeManager getBiomeManager() {
return biomeManager;
}
@Override
public Holder<Biome> getNoiseBiome(int quartX, int quartY, int quartZ) {
return getUncachedNoiseBiome(quartX, quartY, quartZ);
}
@Override
public Holder<Biome> getUncachedNoiseBiome(int quartX, int quartY, int quartZ) {
return chunk(quartX >> 2, quartZ >> 2).getNoiseBiome(quartX, quartY, quartZ);
}
@Override
public boolean isClientSide() {
return false;
}
@Override
public int getSeaLevel() {
return options.seaLevel();
}
@Override
public DimensionType dimensionType() {
return options.dimension();
}
@Override
public int getMinY() {
return options.dimension().minY();
}
@Override
public int getHeight() {
return options.dimension().height();
}
@Override
public RegistryAccess registryAccess() {
return options.registries();
}
@Override
public FeatureFlagSet enabledFeatures() {
return FeatureFlags.DEFAULT_FLAGS;
}
@Override
public EnvironmentAttributeReader environmentAttributes() {
throw unsupported("environmentAttributes");
}
@Override
public LevelLightEngine getLightEngine() {
throw unsupported("getLightEngine");
}
@Override
public int getBrightness(LightLayer layer, BlockPos position) {
throw unsupported("getBrightness");
}
@Override
public int getRawBrightness(BlockPos position, int ambientDarkening) {
throw unsupported("getRawBrightness");
}
@Override
public boolean canSeeSky(BlockPos position) {
throw unsupported("canSeeSky");
}
@Override
public WorldBorder getWorldBorder() {
return border;
}
@Override
public BlockGetter getChunkForCollisions(int chunkX, int chunkZ) {
return chunk(chunkX, chunkZ);
}
@Override
public BlockEntity getBlockEntity(BlockPos position) {
return chunk(position).getBlockEntity(position);
}
@Override
public BlockState getBlockState(BlockPos position) {
return chunk(position).getBlockState(position);
}
@Override
public BlockState getBlockStateIfLoaded(BlockPos position) {
return hasChunk(position.getX() >> 4, position.getZ() >> 4) ? getBlockState(position) : null;
}
@Override
public FluidState getFluidState(BlockPos position) {
return getBlockState(position).getFluidState();
}
@Override
public FluidState getFluidIfLoaded(BlockPos position) {
BlockState state = getBlockStateIfLoaded(position);
return state == null ? null : state.getFluidState();
}
@Override
public List<Entity> getEntities(Entity source, AABB area, Predicate<? super Entity> predicate) {
throw unsupported("getEntities");
}
@Override
public <T extends Entity> List<T> getEntities(EntityTypeTest<Entity, T> type, AABB area, Predicate<? super T> predicate) {
throw unsupported("getEntities");
}
@Override
public List<? extends Player> players() {
return List.of();
}
@Override
public boolean isStateAtPosition(BlockPos position, Predicate<BlockState> predicate) {
return predicate.test(getBlockState(position));
}
@Override
public boolean isFluidAtPosition(BlockPos position, Predicate<FluidState> predicate) {
return predicate.test(getFluidState(position));
}
@Override
public BlockPos getHeightmapPos(Heightmap.Types type, BlockPos position) {
return position.atY(getHeight(type, position.getX(), position.getZ()));
}
@Override
public boolean setBlock(BlockPos position, BlockState state, int updateFlags, int updateLimit) {
if (!ensureCanWrite(position)) {
throw new IllegalStateException("Feature write outside prepared bounds: " + position);
}
ProtoChunk chunk = chunk(position);
BlockState previous = chunk.setBlockState(position, state, updateFlags);
if (previous == state) {
return false;
}
chunk.removeBlockEntity(position);
if (state.getBlock() instanceof EntityBlock block) {
BlockEntity entity = block.newBlockEntity(position, state);
if (entity != null) {
chunk.setBlockEntity(entity);
}
}
BlockPos postProcess = state.getPostProcessPos(this, position);
if (postProcess != null) {
chunk(postProcess).markPosForPostProcessing(postProcess);
}
return true;
}
@Override
public boolean removeBlock(BlockPos position, boolean move) {
return setBlock(position, Blocks.AIR.defaultBlockState(), 2, 0);
}
@Override
public boolean destroyBlock(BlockPos position, boolean drop, Entity source, int updateLimit) {
throw unsupported("destroyBlock");
}
@Override
public boolean addFreshEntity(Entity entity) {
throw unsupported("addFreshEntity");
}
@Override
public boolean addFreshEntity(Entity entity, CreatureSpawnEvent.SpawnReason reason) {
throw unsupported("addFreshEntity");
}
private ProtoChunk chunk(BlockPos position) {
return chunk(position.getX() >> 4, position.getZ() >> 4);
}
private ProtoChunk chunk(int x, int z) {
ProtoChunk chunk = chunks.get(ChunkPos.pack(x, z));
if (chunk == null) {
throw new IllegalStateException("Feature escaped prepared chunk region: " + x + "," + z);
}
return chunk;
}
private <T> LevelTickAccess<T> ticks(Function<ProtoChunk, TickContainerAccess<T>> containers) {
return new LevelTickAccess<>() {
@Override
public void schedule(ScheduledTick<T> tick) {
containers.apply(chunk(tick.pos())).schedule(tick);
}
@Override
public boolean hasScheduledTick(BlockPos position, T type) {
return containers.apply(chunk(position)).hasScheduledTick(position, type);
}
@Override
public int count() {
int count = 0;
for (ProtoChunk chunk : chunks.values()) {
count += containers.apply(chunk).count();
}
return count;
}
@Override
public boolean willTickThisTick(BlockPos position, T type) {
throw unsupported("live tick execution");
}
};
}
private static UnsupportedOperationException unsupported(String capability) {
return new UnsupportedOperationException("Offline feature fixture has no " + capability);
}
record Options(RegistryAccess registries, DimensionType dimension, long seed, int seaLevel) {
}
}
@@ -0,0 +1,227 @@
package art.arcane.iris.probe;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.Registries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.biome.Biomes;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.entity.ChestBlockEntity;
import net.minecraft.world.level.chunk.PalettedContainerFactory;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.UpgradeData;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.levelgen.placement.PlacedFeature;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.structures.StrongholdPieces;
import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece;
import net.minecraft.world.level.material.Fluids;
import org.bukkit.Bukkit;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public final class OfflineNativeFeatureFeasibilityTest {
private static HeadlessNativeRegistries loaded;
private static RegistryAccess registries;
private static DimensionType dimension;
@BeforeClass
public static void loadImportedFeatures() throws Exception {
assertNull(Bukkit.getServer());
loaded = HeadlessNativeRegistries.load(Map.of(
"data/iris/worldgen/placed_feature/offline_spring.json",
"{\"feature\":\"minecraft:spring_water\",\"placement\":[]}",
"data/iris/worldgen/placed_feature/offline_chest.json",
"{\"feature\":{\"type\":\"minecraft:simple_block\",\"to_place\":{\"id\":\"minecraft:chest\"}},\"placement\":[]}"
));
registries = loaded.registries();
dimension = registries.lookupOrThrow(Registries.DIMENSION_TYPE).getOrThrow(ResourceKey.create(
Registries.DIMENSION_TYPE, Identifier.parse("minecraft:overworld"))).value();
}
@AfterClass
public static void closeRegistries() throws Exception {
if (loaded != null) {
loaded.close();
}
assertNull(Bukkit.getServer());
}
@Test
public void registryConfiguredSpringCapturesNativeFluidTickAcrossChunkBoundary() {
Fixture fixture = fixture();
BlockPos spring = new BlockPos(-1, 20, 0);
for (Direction direction : Direction.values()) {
if (direction != Direction.EAST) {
fixture.world().setBlock(spring.relative(direction), Blocks.STONE.defaultBlockState(), 2);
}
}
assertTrue(feature("offline_spring").placement().isEmpty());
assertTrue(feature("offline_spring").feature().value().place(fixture.world(), null, RandomSource.create(42L), spring));
assertTrue(fixture.world().getFluidState(spring).is(Fluids.WATER));
assertTrue(fixture.world().getFluidTicks().hasScheduledTick(spring, Fluids.WATER));
assertEquals(1, fixture.world().getFluidTicks().count());
ProtoChunk source = fixture.chunk(-1, 0);
assertEquals(1, source.getTicksForSerialization(0L).fluids().size());
assertEquals(spring, source.getTicksForSerialization(0L).fluids().getFirst().pos());
assertFalse(source.isLightCorrect());
assertEquals(ChunkStatus.TERRAIN, source.getPersistedStatus());
assertThrows(UnsupportedOperationException.class,
() -> fixture.world().getFluidTicks().willTickThisTick(spring, Fluids.WATER));
assertNull(Bukkit.getServer());
}
@Test
public void registryConfiguredSimpleBlockCreatesRealNativeBlockEntity() {
Fixture fixture = fixture();
BlockPos chest = new BlockPos(1, 20, 1);
assertTrue(feature("offline_chest").placement().isEmpty());
assertTrue(feature("offline_chest").feature().value().place(fixture.world(), null, RandomSource.create(3L), chest));
assertTrue(fixture.world().getBlockState(chest).is(Blocks.CHEST));
assertTrue(fixture.world().getBlockEntity(chest) instanceof ChestBlockEntity);
CompoundTag saved = fixture.chunk(0, 0).getBlockEntityNbtForSaving(chest, registries);
assertNotNull(saved);
assertEquals("minecraft:chest", saved.getStringOr("id", ""));
assertEquals(1, saved.getIntOr("x", -1));
assertEquals(20, saved.getIntOr("y", -1));
assertEquals(ChunkStatus.TERRAIN, fixture.chunk(0, 0).getPersistedStatus());
assertNull(Bukkit.getServer());
}
@Test
public void importedPlacedFeatureWrapperRequiresServerLevelEvenWithoutModifiers() {
Fixture fixture = fixture();
BlockPos position = new BlockPos(1, 20, 1);
for (String key : List.of("offline_spring", "offline_chest")) {
PlacedFeature placed = feature(key);
assertTrue(placed.placement().isEmpty());
UnsupportedOperationException failure = assertThrows(UnsupportedOperationException.class,
() -> placed.place(fixture.world(), null, RandomSource.create(42L), position));
assertTrue(failure.getMessage(), failure.getMessage().contains("ServerLevel"));
assertTrue(hasFrame(failure, "net.minecraft.world.level.levelgen.placement.PlacementContext"));
}
assertTrue(fixture.world().getBlockState(position).isAir());
assertEquals(0, fixture.world().getFluidTicks().count());
assertEquals(ChunkStatus.TERRAIN, fixture.chunk(0, 0).getPersistedStatus());
}
@Test
public void realStrongholdPieceWritesAcrossPreparedChunksWithoutServer() {
Fixture fixture = fixture();
BoundingBox box = new BoundingBox(-2, 20, -2, 2, 24, 4);
for (BlockPos position : BlockPos.betweenClosed(-2, 20, -2, 2, 24, 4)) {
fixture.world().setBlock(position, Blocks.STONE.defaultBlockState(), 2);
}
StrongholdPieces.Straight piece = new StrongholdPieces.Straight(0, RandomSource.create(4L), box, Direction.SOUTH);
piece.postProcess(fixture.world(), null, null, RandomSource.create(5L),
box, new ChunkPos(0, 0), BlockPos.ZERO);
assertTrue(fixture.world().getBlockState(new BlockPos(0, 22, 0)).isAir());
assertFalse(fixture.world().getBlockState(new BlockPos(-2, 20, -2)).is(Blocks.STONE));
assertFalse(fixture.world().getBlockState(new BlockPos(2, 20, 4)).is(Blocks.STONE));
for (ProtoChunk chunk : fixture.chunks()) {
assertEquals(ChunkStatus.TERRAIN, chunk.getPersistedStatus());
}
assertNull(Bukkit.getServer());
}
@Test
public void nativeLootChestRequiresCraftBukkitWorldServices() {
Fixture fixture = fixture();
BoundingBox box = new BoundingBox(0, 20, 0, 4, 24, 6);
StrongholdPieces.ChestCorridor piece = new StrongholdPieces.ChestCorridor(
0, RandomSource.create(4L), box, Direction.SOUTH);
RuntimeException failure = assertThrows(RuntimeException.class,
() -> piece.postProcess(fixture.world(), null, null, RandomSource.create(5L),
box, new ChunkPos(0, 0), BlockPos.ZERO));
assertEquals("Failed to read non-placed BlockState", failure.getMessage());
assertTrue(failure.getCause() instanceof NullPointerException);
assertTrue(hasFrame(failure.getCause(), "net.minecraft.world.level.storage.TagValueOutput"));
assertTrue(hasFrame(failure, "org.bukkit.craftbukkit.block.CraftBlockStates"));
assertEquals(ChunkStatus.TERRAIN, fixture.chunk(0, 0).getPersistedStatus());
assertNull(Bukkit.getServer());
}
@Test
public void nativeHutPopulationRequiresServerLevelAndLeavesChunkUnpromoted() {
Fixture fixture = fixture();
for (BlockPos position : BlockPos.betweenClosed(-16, 63, -16, 31, 63, 31)) {
fixture.world().setBlock(position, Blocks.STONE.defaultBlockState(), 2);
}
SwampHutPiece piece = new SwampHutPiece(RandomSource.create(9L), 0, 0);
UnsupportedOperationException failure = assertThrows(UnsupportedOperationException.class,
() -> piece.postProcess(fixture.world(), null, null, RandomSource.create(6L),
new BoundingBox(-16, dimension.minY(), -16, 31, dimension.minY() + dimension.height() - 1, 31),
new ChunkPos(0, 0), BlockPos.ZERO));
assertTrue(failure.getMessage(), failure.getMessage().contains("ServerLevel"));
assertTrue(hasFrame(failure, SwampHutPiece.class.getName()));
for (ProtoChunk chunk : fixture.chunks()) {
assertEquals(ChunkStatus.TERRAIN, chunk.getPersistedStatus());
assertTrue(chunk.getEntities().isEmpty());
}
}
@Test
public void finiteFixtureRejectsEscapedWritesAndUnavailableStatus() {
Fixture fixture = fixture();
assertThrows(IllegalStateException.class,
() -> fixture.world().setBlock(new BlockPos(32, 20, 0), Blocks.STONE.defaultBlockState(), 2));
assertThrows(UnsupportedOperationException.class,
() -> fixture.world().getChunk(0, 0, ChunkStatus.FULL, true));
assertNull(fixture.world().getChunk(3, 3, ChunkStatus.TERRAIN, false));
}
private static PlacedFeature feature(String path) {
return registries.lookupOrThrow(Registries.PLACED_FEATURE).getOrThrow(ResourceKey.create(
Registries.PLACED_FEATURE, Identifier.fromNamespaceAndPath("iris", path))).value();
}
private static boolean hasFrame(Throwable failure, String className) {
for (StackTraceElement frame : failure.getStackTrace()) {
if (frame.getClassName().equals(className)) {
return true;
}
}
return false;
}
private static Fixture fixture() {
List<ProtoChunk> chunks = new ArrayList<>();
for (int z = -1; z <= 1; z++) {
for (int x = -1; x <= 1; x++) {
ProtoChunk chunk = new ProtoChunk(new ChunkPos(x, z), UpgradeData.EMPTY,
LevelHeightAccessor.create(dimension.minY(), dimension.height()),
PalettedContainerFactory.create(registries), null);
chunk.fillBiomesFromNoise((qx, qy, qz) -> registries.lookupOrThrow(Registries.BIOME).getOrThrow(Biomes.PLAINS));
chunk.setPersistedStatus(ChunkStatus.TERRAIN);
chunks.add(chunk);
}
}
return new Fixture(new OfflineFeatureWorld(new OfflineFeatureWorld.Options(
registries, dimension, 69420L, 63), chunks), List.copyOf(chunks));
}
private record Fixture(OfflineFeatureWorld world, List<ProtoChunk> chunks) {
ProtoChunk chunk(int x, int z) {
return (ProtoChunk) world.getChunk(x, z);
}
}
}