From 9c72f583a8e9261c85fdfb1505a95e2260e6c563 Mon Sep 17 00:00:00 2001 From: rfresh2 <89827146+rfresh2@users.noreply.github.com> Date: Sat, 13 Jul 2024 13:24:26 -0700 Subject: [PATCH] improve OW/Nether palette detection accuracy --- .../java/xaeroplus/module/ModuleManager.java | 2 +- .../{NewChunks.java => LiquidNewChunks.java} | 6 +- .../java/xaeroplus/module/impl/OldChunks.java | 19 ++--- .../module/impl/PaletteNewChunks.java | 75 ++++++++++++------- .../module/impl/PortalSkipDetection.java | 2 +- .../settings/XaeroPlusSettingRegistry.java | 24 +++--- 6 files changed, 73 insertions(+), 55 deletions(-) rename common/src/main/java/xaeroplus/module/impl/{NewChunks.java => LiquidNewChunks.java} (98%) diff --git a/common/src/main/java/xaeroplus/module/ModuleManager.java b/common/src/main/java/xaeroplus/module/ModuleManager.java index 743c9f31..76cde7f9 100644 --- a/common/src/main/java/xaeroplus/module/ModuleManager.java +++ b/common/src/main/java/xaeroplus/module/ModuleManager.java @@ -13,7 +13,7 @@ public static void init() { asList( new BaritoneGoalSync(), new FpsLimiter(), - new NewChunks(), + new LiquidNewChunks(), new OldChunks(), new PaletteNewChunks(), new Portals(), diff --git a/common/src/main/java/xaeroplus/module/impl/NewChunks.java b/common/src/main/java/xaeroplus/module/impl/LiquidNewChunks.java similarity index 98% rename from common/src/main/java/xaeroplus/module/impl/NewChunks.java rename to common/src/main/java/xaeroplus/module/impl/LiquidNewChunks.java index 1821c268..82ae8c71 100644 --- a/common/src/main/java/xaeroplus/module/impl/NewChunks.java +++ b/common/src/main/java/xaeroplus/module/impl/LiquidNewChunks.java @@ -35,7 +35,7 @@ import static xaeroplus.feature.render.ColorHelper.getColor; import static xaeroplus.util.ChunkUtils.getActualDimension; -public class NewChunks extends Module { +public class LiquidNewChunks extends Module { // chunks where liquid started flowing from source blocks after we loaded it private ChunkHighlightCache newChunksCache = new ChunkHighlightLocalCache(); // chunks where liquid was already flowing or flowed when we loaded it @@ -238,11 +238,11 @@ private int getInverseColor() { } public void setRgbColor(final int color) { - newChunksColor = ColorHelper.getColorWithAlpha(color, (int) XaeroPlusSettingRegistry.newChunksAlphaSetting.getValue()); + newChunksColor = ColorHelper.getColorWithAlpha(color, (int) XaeroPlusSettingRegistry.liquidNewChunksAlphaSetting.getValue()); } public void setInverseRgbColor(final int color) { - inverseColor = ColorHelper.getColorWithAlpha(color, (int) XaeroPlusSettingRegistry.newChunksAlphaSetting.getValue()); + inverseColor = ColorHelper.getColorWithAlpha(color, (int) XaeroPlusSettingRegistry.liquidNewChunksAlphaSetting.getValue()); } public void setAlpha(final float a) { diff --git a/common/src/main/java/xaeroplus/module/impl/OldChunks.java b/common/src/main/java/xaeroplus/module/impl/OldChunks.java index 6ea3414f..5cc4f7ca 100644 --- a/common/src/main/java/xaeroplus/module/impl/OldChunks.java +++ b/common/src/main/java/xaeroplus/module/impl/OldChunks.java @@ -137,25 +137,20 @@ private void searchChunkAsync(final ChunkAccess chunk) { }); } - private boolean searchChunk(final ChunkAccess chunk) throws InterruptedException { + private boolean searchChunk(final ChunkAccess chunk) { ResourceKey actualDimension = ChunkUtils.getActualDimension(); var x = chunk.getPos().x; var z = chunk.getPos().z; if (actualDimension == OVERWORLD || actualDimension == NETHER) { - if (ChunkScanner.chunkContainsBlocks(chunk, actualDimension == OVERWORLD ? OVERWORLD_BLOCKS : NETHER_BLOCKS, 5)) { - return modernChunksCache.addHighlight(x, z); - } else { - return oldChunksCache.addHighlight(x, z); - } + return ChunkScanner.chunkContainsBlocks(chunk, actualDimension == OVERWORLD ? OVERWORLD_BLOCKS : NETHER_BLOCKS, 5) + ? modernChunksCache.addHighlight(x, z) + : oldChunksCache.addHighlight(x, z); } else if (actualDimension == END) { Holder biome = mc.level.getBiome(new BlockPos(ChunkUtils.chunkCoordToCoord(x) + 8, 64, ChunkUtils.chunkCoordToCoord(z) + 8)); var biomeKey = biome.unwrapKey().get(); - if (biomeKey == Biomes.PLAINS) return false; // mitigate race condition where biomes aren't loaded yet for some reason - if (biomeKey == Biomes.THE_END) { - return oldChunksCache.addHighlight(x, z); - } else { - return modernChunksCache.addHighlight(x, z); - } + return biomeKey == Biomes.THE_END + ? oldChunksCache.addHighlight(x, z) + : modernChunksCache.addHighlight(x, z); } return true; } diff --git a/common/src/main/java/xaeroplus/module/impl/PaletteNewChunks.java b/common/src/main/java/xaeroplus/module/impl/PaletteNewChunks.java index 639644ec..3a38e2af 100644 --- a/common/src/main/java/xaeroplus/module/impl/PaletteNewChunks.java +++ b/common/src/main/java/xaeroplus/module/impl/PaletteNewChunks.java @@ -1,9 +1,12 @@ package xaeroplus.module.impl; +import it.unimi.dsi.fastutil.ints.IntOpenHashSet; +import it.unimi.dsi.fastutil.ints.IntSet; import it.unimi.dsi.fastutil.longs.Long2LongMap; import net.lenni0451.lambdaevents.EventHandler; import net.minecraft.core.Holder; import net.minecraft.resources.ResourceKey; +import net.minecraft.util.BitStorage; import net.minecraft.world.level.Level; import net.minecraft.world.level.biome.Biome; import net.minecraft.world.level.biome.Biomes; @@ -31,6 +34,7 @@ public class PaletteNewChunks extends Module { private ChunkHighlightCache newChunksCache = new ChunkHighlightLocalCache(); private int newChunksColor = getColor(255, 0, 0, 100); private static final String DATABASE_NAME = "XaeroPlusPaletteNewChunks"; + private final IntSet presentStateIdsBuf = new IntOpenHashSet(); public void setNewChunksCache(final boolean disk) { try { @@ -71,45 +75,53 @@ public void onChunkData(ChunkDataEvent event) { /** * MC generates chunks in multiple steps where each step progressively mutates the chunk data + * For more info see this explanation by Henrik Kniberg: https://youtu.be/ob3VwY4JyzE&t=453 * - * when generation is complete there can be block palette entries that no longer - * have corresponding blockstates present in the chunk data + * When a chunk is first generated it is populated first by air, then by additional block types like stone, water, etc + * By the end of these steps, the chunk's blockstate palette will still contain references to all states that were ever present + * For more info on what chunk palettes are see: https://wiki.vg/Chunk_Format#Paletted_Container_structure * - * when the MC server writes + reads the chunks to region files it also compacts the palette to save disk space - * the key is that this compaction occurs _after_ the chunk data is sent to players + * When the MC server writes + reads the chunks to region files it compacts the palette to save disk space + * the key is that this compaction occurs _after_ newly generated chunk data is sent to players * * compacting has 2 effects: * 1. palette entries without blockstates present in the chunk are removed - * 2. the order of ids in the palette changes + * 2. the order of ids in the palette can change as it is rebuilt in order of the actual blockstates present in the chunk * - * so we can simply check if the first entry of the lowest section's block palette is air. - * if the chunk has been generated before it will be removed entirely or its position in the palette changed + * So we are simply checking if the first entry of the lowest section's block palette is air + * The lowest section should always have bedrock as the first entry at the bottom section after compacting * - * there is a chance for false negatives depending on features like mineshafts, geodes, etc that generate on the bottom section. - * but it should be possible to repeat similar checks on other sections to get more accurate results + * However, there is a chance for false negatives if the chunk's palette generates with more than 16 different blockstates + * The palette gets resized to a HashMapPalette which does not retain the original entry ordering + * Usually this happens when features like mineshafts or the deep dark generates + * To catch these we fall back to checking if the palette has more entries than what is actually present in the chunk data */ private boolean checkNewChunkOverworldOrNether(LevelChunk chunk) { var sections = chunk.getSections(); if (sections.length == 0) return false; var firstSection = sections[0]; Palette firstPalette = firstSection.getStates().data.palette(); - if (firstPalette.getSize() < 1 - || firstPalette instanceof SingleValuePalette - || firstPalette instanceof GlobalPalette) - return false; - try { + if (isNotLinearOrHashMapPalette(firstPalette)) return false; + if (firstPalette instanceof LinearPalette) { return firstPalette.valueFor(0).getBlock() == Blocks.AIR; - } catch (final MissingPaletteEntryException e) { - // fall through + } else { // HashMapPalette + // we could iterate through more sections but this is good enough in most cases + // checking every blockstate is relatively expensive + for (int i = 0; i < Math.min(sections.length, 3); i++) { + var section = sections[i]; + var paletteContainerData = section.getStates().data; + var palette = paletteContainerData.palette(); + if (isNotLinearOrHashMapPalette(palette)) continue; + if (checkForExtraPaletteEntries(paletteContainerData)) return true; + } } return false; } /** - * Similar concept to Overworld/Nether but here we check the biome palette + * Similar to Overworld/Nether but we check the biome palette instead * - * for some reason end generation sets the first palette entry to plains before compaction - * so we check if the first entry is the correct void biome + * New chunks generated in the end will set the first biome palette entry to plains before compaction */ private boolean checkNewChunkEnd(LevelChunk chunk) { var sections = chunk.getSections(); @@ -118,18 +130,29 @@ private boolean checkNewChunkEnd(LevelChunk chunk) { var biomes = firstSection.getBiomes(); if (biomes instanceof PalettedContainer> biomesPaletteContainer) { Palette> firstPalette = biomesPaletteContainer.data.palette(); - // singleton palette will never have more than 1 value - // and we should never have enough entries for a hashmap or global palette - // so we only care about linear palettes - if (firstPalette instanceof LinearPalette> linearPalette - && linearPalette.getSize() > 0) { - Holder firstId = linearPalette.valueFor(0); - return firstId.unwrapKey().filter(k -> k.equals(Biomes.THE_VOID)).isEmpty(); + // chunks already generated in the end will not have more than 1 biome present (stored in SingleValuePalette) + // so just checking the palette size is sufficient + // but we also check if the first entry is plains to be extra sure + if (firstPalette.getSize() > 1) { + Holder firstBiome = firstPalette.valueFor(0); + return firstBiome.unwrapKey().filter(k -> k.equals(Biomes.PLAINS)).isPresent(); } } return false; } + private boolean isNotLinearOrHashMapPalette(Palette palette) { + return palette.getSize() <= 0 || !(palette instanceof LinearPalette || palette instanceof HashMapPalette); + } + + private synchronized boolean checkForExtraPaletteEntries(PalettedContainer.Data paletteContainer) { + presentStateIdsBuf.clear(); // reusing to reduce gc pressure + var palette = paletteContainer.palette(); + BitStorage storage = paletteContainer.storage(); + storage.getAll(presentStateIdsBuf::add); + return palette.getSize() > presentStateIdsBuf.size(); + } + @EventHandler public void onXaeroWorldChangeEvent(final XaeroWorldChangeEvent event) { newChunksCache.handleWorldChange(); diff --git a/common/src/main/java/xaeroplus/module/impl/PortalSkipDetection.java b/common/src/main/java/xaeroplus/module/impl/PortalSkipDetection.java index 07781aad..83c35414 100644 --- a/common/src/main/java/xaeroplus/module/impl/PortalSkipDetection.java +++ b/common/src/main/java/xaeroplus/module/impl/PortalSkipDetection.java @@ -194,7 +194,7 @@ private boolean isNewishChunk(final int chunkPosX, final int chunkPosZ, final Re } private boolean isNewChunk(final int chunkPosX, final int chunkPosZ, final ResourceKey currentlyViewedDimension) { - if (XaeroPlusSettingRegistry.newChunksEnabledSetting.getValue() && newChunksModule != null) + if (XaeroPlusSettingRegistry.paletteNewChunksSaveLoadToDisk.getValue() && newChunksModule != null) return newChunksModule.isNewChunk(chunkPosX, chunkPosZ, currentlyViewedDimension); else return false; diff --git a/common/src/main/java/xaeroplus/settings/XaeroPlusSettingRegistry.java b/common/src/main/java/xaeroplus/settings/XaeroPlusSettingRegistry.java index 18a5512f..7c68017d 100644 --- a/common/src/main/java/xaeroplus/settings/XaeroPlusSettingRegistry.java +++ b/common/src/main/java/xaeroplus/settings/XaeroPlusSettingRegistry.java @@ -216,48 +216,48 @@ public final class XaeroPlusSettingRegistry { ColorHelper.HighlightColor.values(), ColorHelper.HighlightColor.RED, SettingLocation.CHUNK_HIGHLIGHTS); - public static final XaeroPlusBooleanSetting newChunksEnabledSetting = XaeroPlusBooleanSetting.create( + public static final XaeroPlusBooleanSetting liquidNewChunksEnabledSetting = XaeroPlusBooleanSetting.create( "NewChunks Highlighting", "setting.world_map.new_chunks_highlighting", "setting.world_map.new_chunks_highlighting.tooltip", - (b) -> ModuleManager.getModule(NewChunks.class).setEnabled(b), + (b) -> ModuleManager.getModule(LiquidNewChunks.class).setEnabled(b), false, SettingLocation.CHUNK_HIGHLIGHTS); - public static final XaeroPlusBooleanSetting newChunksSaveLoadToDisk = XaeroPlusBooleanSetting.create( + public static final XaeroPlusBooleanSetting liquidNewChunksSaveLoadToDisk = XaeroPlusBooleanSetting.create( "Save/Load NewChunks to Disk", "setting.world_map.new_chunks_save_load_to_disk", "setting.world_map.new_chunks_save_load_to_disk.tooltip", - (b) -> ModuleManager.getModule(NewChunks.class).setNewChunksCache(b), + (b) -> ModuleManager.getModule(LiquidNewChunks.class).setNewChunksCache(b), true, SettingLocation.CHUNK_HIGHLIGHTS); - public static final XaeroPlusFloatSetting newChunksAlphaSetting = XaeroPlusFloatSetting.create( + public static final XaeroPlusFloatSetting liquidNewChunksAlphaSetting = XaeroPlusFloatSetting.create( "New Chunks Opacity", "setting.world_map.new_chunks_opacity", 0f, 255f, 10f, "setting.world_map.new_chunks_opacity.tooltip", - (b) -> ModuleManager.getModule(NewChunks.class).setAlpha(b), + (b) -> ModuleManager.getModule(LiquidNewChunks.class).setAlpha(b), 100, SettingLocation.CHUNK_HIGHLIGHTS); - public static final XaeroPlusEnumSetting newChunksColorSetting = XaeroPlusEnumSetting.create( + public static final XaeroPlusEnumSetting liquidNewChunksColorSetting = XaeroPlusEnumSetting.create( "New Chunks Color", "setting.world_map.new_chunks_color", "setting.world_map.new_chunks_color.tooltip", - (b) -> ModuleManager.getModule(NewChunks.class).setRgbColor(b.getColor()), + (b) -> ModuleManager.getModule(LiquidNewChunks.class).setRgbColor(b.getColor()), ColorHelper.HighlightColor.values(), ColorHelper.HighlightColor.RED, SettingLocation.CHUNK_HIGHLIGHTS); - public static final XaeroPlusBooleanSetting newChunksInverseHighlightsSetting = XaeroPlusBooleanSetting.create( + public static final XaeroPlusBooleanSetting liquidNewChunksInverseHighlightsSetting = XaeroPlusBooleanSetting.create( "New Chunks Render Inverse", "setting.world_map.new_chunks_inverse_enabled", "setting.world_map.new_chunks_inverse_enabled.tooltip", - (b) -> ModuleManager.getModule(NewChunks.class).setInverseRenderEnabled(b), + (b) -> ModuleManager.getModule(LiquidNewChunks.class).setInverseRenderEnabled(b), false, SettingLocation.CHUNK_HIGHLIGHTS); - public static final XaeroPlusEnumSetting newChunksInverseColorSetting = XaeroPlusEnumSetting.create( + public static final XaeroPlusEnumSetting liquidNewChunksInverseColorSetting = XaeroPlusEnumSetting.create( "New Chunks Inverse Color", "setting.world_map.new_chunks_inverse_color", "setting.world_map.new_chunks_inverse_color.tooltip", - (b) -> ModuleManager.getModule(NewChunks.class).setInverseRgbColor(b.getColor()), + (b) -> ModuleManager.getModule(LiquidNewChunks.class).setInverseRgbColor(b.getColor()), ColorHelper.HighlightColor.values(), ColorHelper.HighlightColor.GREEN, SettingLocation.CHUNK_HIGHLIGHTS);