/* * Decompiled with CFR 0.152. */ package com.vinlanx.explosionoverhaul; import net.minecraft.core.registries.BuiltInRegistries; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; import com.mojang.serialization.DynamicOps; import com.vinlanx.explosionoverhaul.Config; import com.vinlanx.explosionoverhaul.ExplosionOverhaul; import com.vinlanx.explosionoverhaul.PacketHandler; import com.vinlanx.explosionoverhaul.ScanLoadPromptPacket; import com.vinlanx.explosionoverhaul.ScanProgressPacket; import com.vinlanx.explosionoverhaul.ScanPromptPacket; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.ByteArrayInputStream; import java.io.DataInput; import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; import java.io.RandomAccessFile; import java.io.Reader; import java.lang.reflect.Type; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.OpenOption; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.FileAttribute; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.LongAdder; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.zip.GZIPInputStream; import java.util.zip.InflaterInputStream; import net.minecraft.core.BlockPos; import net.minecraft.core.IdMap; import net.minecraft.core.registries.Registries; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; import net.minecraft.nbt.NbtIo; import net.minecraft.nbt.NbtOps; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.entity.player.Player; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.Level; import net.minecraft.world.level.LevelAccessor; 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.PalettedContainer; import net.minecraft.world.level.storage.LevelResource; import net.neoforged.neoforge.event.TickEvent; import net.neoforged.neoforge.event.entity.player.PlayerEvent; import net.neoforged.neoforge.event.level.BlockEvent; import net.neoforged.neoforge.event.level.ChunkEvent; import net.neoforged.neoforge.event.server.ServerStartedEvent; import net.neoforged.neoforge.event.server.ServerStoppedEvent; import net.neoforged.bus.api.SubscribeEvent; import net.neoforged.fml.common.Mod; import net.neoforged.fml.common.EventBusSubscriber; import net.neoforged.neoforge.server.ServerLifecycleHooks; @EventBusSubscriber public class BlockIndexManager { private static volatile MinecraftServer currentServer; private static final Map, ConcurrentHashMap>> indexByDimension; private static final ConcurrentLinkedQueue chunkScanQueue; private static final Map, Set> pendingChunkKeys; public static volatile boolean ENABLED; private static final boolean SCAN_ONLY_ON_START = true; private static volatile boolean INITIAL_SCAN_ENQUEUED; private static volatile int totalChunksToScan; private static volatile int chunksScanned; private static volatile boolean scanningComplete; private static volatile boolean abortCurrentScan; private static volatile int lampsFound; private static volatile int dripstonesFound; private static volatile int glassBlocksFound; private static volatile ResourceKey lastScannedDimension; private static volatile boolean waitingForUserInput; private static volatile ServerLevel pendingScanLevel; private static volatile ResourceKey currentScannedWorld; private static volatile boolean isSingleplayer; private static volatile boolean serverScanPromptShown; private static volatile boolean serverScanDecisionMade; private static volatile boolean isRescanMode; private static volatile ServerPlayer rescanRequestingPlayer; private static volatile boolean userDeclinedScan; private static volatile boolean waitingForLoadDecision; private static volatile boolean hasSaveFile; private static volatile String currentWorldId; private static final List DEFAULT_REINFORCED_GLASS_BLACKLIST; private static final Path MOD_CONFIG_DIRECTORY; private static final Path GLASS_BLACKLIST_FILE; private static final CopyOnWriteArrayList GLASS_BLACKLIST; private static final AtomicReference> GLASS_BLACKLIST_LOOKUP; private static ConcurrentHashMap> getOrCreateDimIndex(ServerLevel level) { ResourceKey dim = level.m_46472_(); return indexByDimension.computeIfAbsent((ResourceKey)dim, k -> new ConcurrentHashMap()); } private static Set getOrCreatePendingSet(ServerLevel level) { ResourceKey dim = level.m_46472_(); return pendingChunkKeys.computeIfAbsent((ResourceKey)dim, k -> Collections.newSetFromMap(new ConcurrentHashMap())); } private static long chunkKey(ChunkPos cp) { return (long)cp.f_45578_ & 0xFFFFFFFFL | ((long)cp.f_45579_ & 0xFFFFFFFFL) << 32; } private static long packPos(BlockPos pos) { return pos.m_121878_(); } private static BlockPos unpackPos(long packed) { return BlockPos.m_122022_((long)packed); } public static int getTotalChunksToScan() { return totalChunksToScan; } public static int getChunksScanned() { return chunksScanned; } public static float getScanProgress() { if (totalChunksToScan == 0) { return 1.0f; } return Math.min(1.0f, (float)chunksScanned / (float)totalChunksToScan); } public static boolean isScanningComplete() { return scanningComplete; } public static int getLampsFound() { return lampsFound; } public static int getDripstonesFound() { return dripstonesFound; } public static int getGlassBlocksFound() { return glassBlocksFound; } private static void clearScanDataSilent() { indexByDimension.clear(); chunkScanQueue.clear(); pendingChunkKeys.clear(); totalChunksToScan = 0; chunksScanned = 0; scanningComplete = false; currentScannedWorld = null; lastScannedDimension = null; lampsFound = 0; dripstonesFound = 0; glassBlocksFound = 0; } private static void clearScanData() { BlockIndexManager.clearScanDataSilent(); BlockIndexManager.sendProgressUpdate(); ExplosionOverhaul.LOGGER.info("BlockIndexManager: cleared all scan data for world switch"); } private static void clearAndSaveData() { if (!indexByDimension.isEmpty() && scanningComplete) { try { MinecraftServer server = ServerLifecycleHooks.getCurrentServer(); if (server != null) { BlockIndexManager.saveIndexData(server); ExplosionOverhaul.LOGGER.info("BlockIndexManager: saved data before clearing"); } } catch (Exception e) { ExplosionOverhaul.LOGGER.warn("Failed to save data before clearing: {}", (Object)e.getMessage()); } } BlockIndexManager.clearScanData(); } private static void startSingleplayerScan(ServerLevel level) { ExplosionOverhaul.LOGGER.info("BlockIndexManager: starting singleplayer scan for ALL dimensions"); BlockIndexManager.clearScanDataSilent(); MinecraftServer server = level.m_7654_(); BlockIndexManager.scanExistingChunksOnServer(server); } private static void startServerScan(MinecraftServer server) { ExplosionOverhaul.LOGGER.info("BlockIndexManager: starting server-wide scan for all dimensions"); BlockIndexManager.clearScanDataSilent(); BlockIndexManager.scanExistingChunksOnServer(server); } private static void scanSingleWorld(ServerLevel level) { try { chunksScanned = 0; scanningComplete = false; lastScannedDimension = level.m_46472_(); lampsFound = 0; dripstonesFound = 0; glassBlocksFound = 0; String dimensionName = level.m_46472_().m_135782_().toString(); ExplosionOverhaul.LOGGER.info("BlockIndexManager: scanning existing chunks in world: {}", (Object)dimensionName); Set existingChunks = BlockIndexManager.getAllExistingChunks(level); totalChunksToScan = existingChunks.size(); ExplosionOverhaul.LOGGER.info("BlockIndexManager: found {} chunks to scan", (Object)totalChunksToScan); ExplosionOverhaul.LOGGER.info("BlockIndexManager: using safe server-thread scanning for live chunks"); BlockIndexManager.scanWithSingleThread(level, existingChunks); } catch (Exception ex) { ExplosionOverhaul.LOGGER.warn("BlockIndexManager: failed to start scan: {}", (Object)ex.toString()); } } private static void scanWithSingleThread(ServerLevel level, Set existingChunks) { for (ChunkPos cp : existingChunks) { chunkScanQueue.add(new ScanTask((ResourceKey)level.m_46472_(), cp)); } BlockIndexManager.sendProgressUpdate(); } private static Set getAllExistingChunks(ServerLevel level) { Set existingChunks; block18: { existingChunks = Collections.newSetFromMap(new ConcurrentHashMap()); try { ExplosionOverhaul.LOGGER.info("BlockIndexManager: scanning for all existing chunks..."); MinecraftServer server = level.m_7654_(); String dimPath = level.m_46472_().m_135782_().m_135815_(); Path worldPath = server.m_129843_(LevelResource.f_78182_); Path regionPath = "overworld".equals(dimPath) ? worldPath.resolve("region") : ("the_nether".equals(dimPath) ? worldPath.resolve("DIM-1").resolve("region") : ("the_end".equals(dimPath) ? worldPath.resolve("DIM1").resolve("region") : worldPath.resolve("dimensions").resolve(level.m_46472_().m_135782_().m_135827_()).resolve(level.m_46472_().m_135782_().m_135815_()).resolve("region"))); ExplosionOverhaul.LOGGER.info("BlockIndexManager: scanning region files in: {}", (Object)regionPath); if (Files.exists(regionPath, new LinkOption[0])) { try (Stream regionFiles = Files.list(regionPath);){ List mcaFiles = regionFiles.filter(path -> path.toString().endsWith(".mca")).collect(Collectors.toList()); ExplosionOverhaul.LOGGER.info("BlockIndexManager: found {} .mca files in region directory", (Object)mcaFiles.size()); mcaFiles.parallelStream().forEach(regionFile -> { block13: { try { int chunksInThisRegion; int regionZ; int regionX; block14: { String[] parts; String fileName = regionFile.getFileName().toString(); if (!fileName.startsWith("r.") || !fileName.endsWith(".mca") || (parts = fileName.substring(2, fileName.length() - 4).split("\\.")).length != 2) break block13; regionX = Integer.parseInt(parts[0]); regionZ = Integer.parseInt(parts[1]); chunksInThisRegion = 0; try (InputStream is = Files.newInputStream(regionFile, new OpenOption[0]);){ byte[] header = new byte[4096]; int bytesRead = is.read(header); if (bytesRead < 4096) break block14; for (int i = 0; i < 1024; ++i) { int offset = i * 4; int chunkOffset = (header[offset] & 0xFF) << 16 | (header[offset + 1] & 0xFF) << 8 | header[offset + 2] & 0xFF; if (chunkOffset <= 0) continue; int cx = i % 32; int cz = i / 32; int chunkX = regionX * 32 + cx; int chunkZ = regionZ * 32 + cz; ChunkPos cp = new ChunkPos(chunkX, chunkZ); existingChunks.add(cp); ++chunksInThisRegion; } } catch (Exception readEx) { ExplosionOverhaul.LOGGER.warn("BlockIndexManager: failed to read region file {}: {}", (Object)fileName, (Object)readEx.getMessage()); for (int cx = 0; cx < 32; ++cx) { for (int cz = 0; cz < 32; ++cz) { int chunkX = regionX * 32 + cx; int chunkZ = regionZ * 32 + cz; ChunkPos cp = new ChunkPos(chunkX, chunkZ); existingChunks.add(cp); ++chunksInThisRegion; } } } } ExplosionOverhaul.LOGGER.info("BlockIndexManager: region {},{} - found {} existing chunks (read from .mca header)", new Object[]{regionX, regionZ, chunksInThisRegion}); } catch (Exception e) { ExplosionOverhaul.LOGGER.warn("BlockIndexManager: failed to parse region file {}: {}", (Object)regionFile.getFileName(), (Object)e.getMessage()); } } }); } ExplosionOverhaul.LOGGER.info("BlockIndexManager: scanned region files and found {} existing chunks", (Object)existingChunks.size()); break block18; } ExplosionOverhaul.LOGGER.warn("BlockIndexManager: region directory not found: {}", (Object)regionPath); } catch (Exception e) { ExplosionOverhaul.LOGGER.error("BlockIndexManager: failed to scan world storage: {}", (Object)e.getMessage()); ExplosionOverhaul.LOGGER.info("BlockIndexManager: using fallback - estimate chunks from .mca files"); try { String dimPath = level.m_46472_().m_135782_().m_135815_(); MinecraftServer server = level.m_7654_(); Path worldPath = server.m_129843_(LevelResource.f_78182_); Path regionPath = "overworld".equals(dimPath) ? worldPath.resolve("region") : ("the_nether".equals(dimPath) ? worldPath.resolve("DIM-1").resolve("region") : ("the_end".equals(dimPath) ? worldPath.resolve("DIM1").resolve("region") : worldPath.resolve("dimensions").resolve(level.m_46472_().m_135782_().m_135827_()).resolve(level.m_46472_().m_135782_().m_135815_()).resolve("region"))); if (!Files.exists(regionPath, new LinkOption[0])) break block18; try (Stream regionFiles = Files.list(regionPath);){ regionFiles.filter(path -> path.toString().endsWith(".mca")).forEach(regionFile -> { String[] parts; String fileName = regionFile.getFileName().toString(); if (fileName.startsWith("r.") && fileName.endsWith(".mca") && (parts = fileName.substring(2, fileName.length() - 4).split("\\.")).length == 2) { int regionX = Integer.parseInt(parts[0]); int regionZ = Integer.parseInt(parts[1]); for (int cx = 0; cx < 32; ++cx) { for (int cz = 0; cz < 32; ++cz) { int chunkX = regionX * 32 + cx; int chunkZ = regionZ * 32 + cz; ChunkPos cp = new ChunkPos(chunkX, chunkZ); existingChunks.add(cp); } } ExplosionOverhaul.LOGGER.info("BlockIndexManager: fallback added ~1024 chunks from region {},{}", (Object)regionX, (Object)regionZ); } }); } ExplosionOverhaul.LOGGER.info("BlockIndexManager: fallback found {} estimated chunks", (Object)existingChunks.size()); } catch (Exception fallbackEx) { ExplosionOverhaul.LOGGER.error("BlockIndexManager: fallback also failed: {}", (Object)fallbackEx.getMessage()); } } } return existingChunks; } private static void saveIndexDataAfterScan(MinecraftServer server) { try { ExplosionOverhaul.LOGGER.info("BlockIndexManager: Starting auto-save after scan completion..."); BlockIndexManager.saveIndexData(server); } catch (Exception e) { ExplosionOverhaul.LOGGER.warn("Failed to auto-save scan data: {}", (Object)e.getMessage()); e.printStackTrace(); } } private static void sendProgressUpdate() { try { ExplosionOverhaul.LOGGER.debug("sendProgressUpdate: {}/{} chunks, complete={}, L:{} D:{} G:{}", new Object[]{chunksScanned, totalChunksToScan, scanningComplete, lampsFound, dripstonesFound, glassBlocksFound}); if (scanningComplete) { ExplosionOverhaul.LOGGER.info("BlockIndexManager: sending final progress update to OP players - {}/{} chunks, lamps={}, dripstones={}, glass={}", new Object[]{chunksScanned, totalChunksToScan, lampsFound, dripstonesFound, glassBlocksFound}); } ScanProgressPacket packet = new ScanProgressPacket(totalChunksToScan, chunksScanned, scanningComplete, lampsFound, dripstonesFound, glassBlocksFound); MinecraftServer server = ServerLifecycleHooks.getCurrentServer(); if (server != null) { for (ServerPlayer player : server.m_6846_().m_11314_()) { if (!BlockIndexManager.isPlayerAuthorized(player)) continue; PacketHandler.sendToPlayer(player, packet); } } } catch (Exception e) { ExplosionOverhaul.LOGGER.warn("Failed to send scan progress update: {}", (Object)e.getMessage()); } } private static void sendMultiThreadProgressUpdate(int currentProcessed, int currentLamps, int currentDripstones, int currentGlass) { try { ScanProgressPacket packet = new ScanProgressPacket(totalChunksToScan, currentProcessed, false, currentLamps, currentDripstones, currentGlass); MinecraftServer server = ServerLifecycleHooks.getCurrentServer(); if (server != null) { for (ServerPlayer player : server.m_6846_().m_11314_()) { if (!BlockIndexManager.isPlayerAuthorized(player)) continue; PacketHandler.sendToPlayer(player, packet); } } } catch (Exception e) { ExplosionOverhaul.LOGGER.warn("Failed to send multi-thread progress update: {}", (Object)e.getMessage()); } } private static void sendScanPromptPacket(ServerPlayer player) { try { ScanPromptPacket packet = new ScanPromptPacket(true); PacketHandler.sendToPlayer(player, packet); ExplosionOverhaul.LOGGER.info("Sent scan prompt to authorized player: {}", (Object)player.m_7755_().getString()); } catch (Exception e) { ExplosionOverhaul.LOGGER.warn("Failed to send scan prompt: {}", (Object)e.getMessage()); } } public static void startManualScan() { if (BlockIndexManager.isChunkScanningDisabled()) { ExplosionOverhaul.LOGGER.info("BlockIndexManager: manual scan cancelled - chunk scanning is disabled in config"); BlockIndexManager.cancelManualScan(); return; } if (waitingForUserInput && pendingScanLevel != null) { waitingForUserInput = false; serverScanDecisionMade = true; userDeclinedScan = false; if (isRescanMode) { ExplosionOverhaul.LOGGER.info("BlockIndexManager: clearing data for rescan as user confirmed"); BlockIndexManager.clearScanDataSilent(); try { MinecraftServer server = pendingScanLevel.m_7654_(); Path saveFile = BlockIndexManager.getSaveFilePath(server); if (Files.exists(saveFile, new LinkOption[0])) { Files.delete(saveFile); ExplosionOverhaul.LOGGER.info("BlockIndexManager: deleted old save file for rescan"); } } catch (Exception e) { ExplosionOverhaul.LOGGER.warn("Failed to delete old save file: {}", (Object)e.getMessage()); } } if (isSingleplayer || isRescanMode) { BlockIndexManager.startSingleplayerScan(pendingScanLevel); } else { BlockIndexManager.startServerScan(pendingScanLevel.m_7654_()); } ServerPlayer requestingPlayer = rescanRequestingPlayer; pendingScanLevel = null; isRescanMode = false; rescanRequestingPlayer = null; try { if (requestingPlayer != null && requestingPlayer.m_6084_()) { ScanPromptPacket packet = new ScanPromptPacket(false); PacketHandler.sendToPlayer(requestingPlayer, packet); } else { MinecraftServer server = ServerLifecycleHooks.getCurrentServer(); if (server != null) { for (ServerPlayer player : server.m_6846_().m_11314_()) { if (!BlockIndexManager.isPlayerAuthorized(player)) continue; ScanPromptPacket packet = new ScanPromptPacket(false); PacketHandler.sendToPlayer(player, packet); } } } } catch (Exception e) { ExplosionOverhaul.LOGGER.warn("Failed to hide scan prompt: {}", (Object)e.getMessage()); } } } /* * Unable to fully structure code */ public static void loadExistingData() { if (BlockIndexManager.waitingForLoadDecision && BlockIndexManager.pendingScanLevel != null) { BlockIndexManager.waitingForLoadDecision = false; BlockIndexManager.serverScanDecisionMade = true; server = BlockIndexManager.pendingScanLevel.m_7654_(); loaded = BlockIndexManager.loadIndexData(server); if (loaded) { ExplosionOverhaul.LOGGER.info("BlockIndexManager: Successfully loaded existing scan data"); BlockIndexManager.sendProgressUpdate(); try { currentServer = ServerLifecycleHooks.getCurrentServer(); if (currentServer != null) { for (ServerPlayer serverPlayer : currentServer.m_6846_().m_11314_()) { if (!BlockIndexManager.isPlayerAuthorized(serverPlayer)) continue; packet = new ScanLoadPromptPacket(false); PacketHandler.sendToPlayer(serverPlayer, packet); ExplosionOverhaul.LOGGER.debug("Hidden load prompt for authorized player: {}", (Object)serverPlayer.m_7755_().getString()); } } } catch (Exception e) { ExplosionOverhaul.LOGGER.warn("Failed to hide load prompt: {}", (Object)e.getMessage()); } } else { ExplosionOverhaul.LOGGER.warn("BlockIndexManager: Failed to load existing data, falling back to new scan"); BlockIndexManager.startNewScan(); } BlockIndexManager.pendingScanLevel = null; } } public static void startNewScan() { if (waitingForLoadDecision && pendingScanLevel != null) { waitingForLoadDecision = false; serverScanDecisionMade = true; userDeclinedScan = false; try { MinecraftServer server = ServerLifecycleHooks.getCurrentServer(); if (server != null) { for (ServerPlayer serverPlayer : server.m_6846_().m_11314_()) { if (!BlockIndexManager.isPlayerAuthorized(serverPlayer)) continue; ScanLoadPromptPacket hideLoadPacket = new ScanLoadPromptPacket(false); PacketHandler.sendToPlayer(serverPlayer, hideLoadPacket); ScanPromptPacket showScanPacket = new ScanPromptPacket(true); PacketHandler.sendToPlayer(serverPlayer, showScanPacket); ExplosionOverhaul.LOGGER.debug("Switched prompts for authorized player: {}", (Object)serverPlayer.m_7755_().getString()); } } } catch (Exception e) { ExplosionOverhaul.LOGGER.warn("Failed to switch prompts: {}", (Object)e.getMessage()); } waitingForUserInput = true; serverScanDecisionMade = false; } } public static void cancelManualScan() { if (waitingForUserInput) { waitingForUserInput = false; serverScanDecisionMade = true; userDeclinedScan = true; pendingScanLevel = null; ServerPlayer requestingPlayer = rescanRequestingPlayer; isRescanMode = false; rescanRequestingPlayer = null; chunkScanQueue.clear(); totalChunksToScan = 0; chunksScanned = 0; scanningComplete = false; ExplosionOverhaul.LOGGER.info("BlockIndexManager: manual scan cancelled by user - cleared scan queue"); try { if (requestingPlayer != null && requestingPlayer.m_6084_()) { ScanPromptPacket packet = new ScanPromptPacket(false); PacketHandler.sendToPlayer(requestingPlayer, packet); } else { MinecraftServer server = ServerLifecycleHooks.getCurrentServer(); if (server != null) { for (ServerPlayer player : server.m_6846_().m_11314_()) { if (!BlockIndexManager.isPlayerAuthorized(player)) continue; ScanPromptPacket packet = new ScanPromptPacket(false); PacketHandler.sendToPlayer(player, packet); } } } } catch (Exception e) { ExplosionOverhaul.LOGGER.warn("Failed to hide scan prompt: {}", (Object)e.getMessage()); } } } public static void resetScanState() { waitingForUserInput = false; waitingForLoadDecision = false; pendingScanLevel = null; serverScanPromptShown = false; serverScanDecisionMade = false; isRescanMode = false; rescanRequestingPlayer = null; userDeclinedScan = false; hasSaveFile = false; INITIAL_SCAN_ENQUEUED = false; BlockIndexManager.clearAndSaveData(); ExplosionOverhaul.LOGGER.info("BlockIndexManager: scan state reset by command"); } public static void forceServerScan(MinecraftServer server) { BlockIndexManager.resetScanState(); BlockIndexManager.startServerScan(server); } public static void forceSingleplayerScan(ServerLevel level) { BlockIndexManager.resetScanState(); BlockIndexManager.startSingleplayerScan(level); } public static void showRescanPrompt(ServerPlayer player) { isRescanMode = true; waitingForUserInput = true; pendingScanLevel = player.m_284548_(); rescanRequestingPlayer = player; serverScanPromptShown = true; serverScanDecisionMade = false; try { ScanPromptPacket packet = new ScanPromptPacket(true); PacketHandler.sendToPlayer(player, packet); ExplosionOverhaul.LOGGER.info("BlockIndexManager: showing rescan prompt to player: {}", (Object)player.m_7755_().getString()); } catch (Exception e) { ExplosionOverhaul.LOGGER.warn("Failed to send rescan prompt: {}", (Object)e.getMessage()); } } private static void sendScanPromptPacket(boolean show) { try { MinecraftServer server = ServerLifecycleHooks.getCurrentServer(); if (server != null) { for (ServerPlayer player : server.m_6846_().m_11314_()) { if (!BlockIndexManager.isPlayerAuthorized(player)) continue; ScanPromptPacket packet = new ScanPromptPacket(show); PacketHandler.sendToPlayer(player, packet); if (!show) continue; ExplosionOverhaul.LOGGER.debug("Sent scan prompt to authorized player: {}", (Object)player.m_7755_().getString()); } } } catch (Exception e) { ExplosionOverhaul.LOGGER.warn("Failed to send scan prompt packet: {}", (Object)e.getMessage()); } } private static boolean isGlassBlock(BlockState state) { try { String blockName = BuiltInRegistries.BLOCK.getKey(state.m_60734_()).toString().toLowerCase(Locale.ROOT); if (BlockIndexManager.isGlassProtected(blockName)) { return false; } return blockName.contains("glass"); } catch (Exception e) { return state.m_60713_(Blocks.f_50058_) || state.m_60713_(Blocks.f_152498_); } } private static boolean isLampName(String name) { return "minecraft:redstone_lamp".equals(name); } private static boolean isDripstoneName(String name) { return "minecraft:pointed_dripstone".equals(name); } private static boolean isGlassName(String name) { if (name == null) { return false; } String normalized = name.toLowerCase(Locale.ROOT); return normalized.contains("glass") && !BlockIndexManager.isGlassProtected(normalized); } public static boolean isReinforcedGlass(BlockState state) { try { String blockName = BuiltInRegistries.BLOCK.getKey(state.m_60734_()).toString().toLowerCase(Locale.ROOT); return BlockIndexManager.isGlassProtected(blockName); } catch (Exception e) { return false; } } public static List getReinforcedGlassBlacklist() { return new ArrayList(GLASS_BLACKLIST); } public static List getDefaultReinforcedGlassBlacklist() { return new ArrayList(DEFAULT_REINFORCED_GLASS_BLACKLIST); } public static void setReinforcedGlassBlacklistFromList(List list) { BlockIndexManager.applyGlassBlacklistList(list); BlockIndexManager.saveGlassBlacklistToJson(); } private static void ensureModConfigDirectory() { try { Files.createDirectories(MOD_CONFIG_DIRECTORY, new FileAttribute[0]); } catch (IOException e) { ExplosionOverhaul.LOGGER.warn("BlockIndexManager: failed to create config directory for glass blacklist", (Throwable)e); } } private static void loadGlassBlacklistFromJson() { BlockIndexManager.ensureModConfigDirectory(); Gson gson = new GsonBuilder().setPrettyPrinting().create(); if (!Files.exists(GLASS_BLACKLIST_FILE, new LinkOption[0])) { BlockIndexManager.applyGlassBlacklistList(DEFAULT_REINFORCED_GLASS_BLACKLIST); BlockIndexManager.saveGlassBlacklistToJson(); return; } try (BufferedReader reader = Files.newBufferedReader(GLASS_BLACKLIST_FILE);){ List list = (List)gson.fromJson((Reader)reader, new TypeToken>(){}.getType()); if (list == null) { BlockIndexManager.applyGlassBlacklistList(DEFAULT_REINFORCED_GLASS_BLACKLIST); return; } BlockIndexManager.applyGlassBlacklistList(list); } catch (Exception e) { ExplosionOverhaul.LOGGER.warn("BlockIndexManager: failed to load GlassBlacklist.json, falling back to defaults", (Throwable)e); BlockIndexManager.applyGlassBlacklistList(DEFAULT_REINFORCED_GLASS_BLACKLIST); } } private static void saveGlassBlacklistToJson() { BlockIndexManager.ensureModConfigDirectory(); Gson gson = new GsonBuilder().setPrettyPrinting().create(); try (BufferedWriter writer = Files.newBufferedWriter(GLASS_BLACKLIST_FILE, new OpenOption[0]);){ gson.toJson(new ArrayList(GLASS_BLACKLIST), (Appendable)writer); } catch (IOException e) { ExplosionOverhaul.LOGGER.warn("BlockIndexManager: failed to save GlassBlacklist.json", (Throwable)e); } } private static void applyGlassBlacklistList(List list) { List normalized = BlockIndexManager.normalizeBlacklist(list); GLASS_BLACKLIST.clear(); GLASS_BLACKLIST.addAll(normalized); GLASS_BLACKLIST_LOOKUP.set(new HashSet(normalized)); } private static List normalizeBlacklist(List entries) { if (entries == null) { return new ArrayList(); } ArrayList result = new ArrayList(); HashSet seen = new HashSet(); for (String raw : entries) { String candidate; if (raw == null || (candidate = raw.trim().toLowerCase(Locale.ROOT)).isEmpty() || !seen.add(candidate)) continue; result.add(candidate); } return result; } private static boolean isGlassProtected(String name) { return GLASS_BLACKLIST_LOOKUP.get().contains(name); } private static String generateWorldId(MinecraftServer server) { try { Path worldPath = server.m_129843_(LevelResource.f_78182_); String worldName = server.m_129910_().m_5462_(); String folderName = worldPath.getFileName().toString(); ExplosionOverhaul.LOGGER.info("BlockIndexManager: World path: {}", (Object)worldPath); ExplosionOverhaul.LOGGER.info("BlockIndexManager: World name from level.dat: '{}'", (Object)worldName); ExplosionOverhaul.LOGGER.info("BlockIndexManager: Folder name: '{}'", (Object)folderName); if (folderName == null || folderName.trim().isEmpty()) { folderName = "world"; } if (worldName == null || worldName.trim().isEmpty()) { worldName = "Unknown"; } String combined = folderName + "_" + worldName; String result = combined.replaceAll("[^a-zA-Z0-9_-]", "_").toLowerCase(); ExplosionOverhaul.LOGGER.info("BlockIndexManager: Generated world ID: '{}'", (Object)result); return result; } catch (Exception e) { ExplosionOverhaul.LOGGER.warn("Failed to generate world ID, using fallback: {}", (Object)e.getMessage()); String fallback = "unknown_world_" + System.currentTimeMillis(); ExplosionOverhaul.LOGGER.info("BlockIndexManager: Using fallback ID: '{}'", (Object)fallback); return fallback; } } private static Path getSaveFilePath(MinecraftServer server) { String worldId = BlockIndexManager.generateWorldId(server); Path configDir = server.m_129843_(LevelResource.f_78182_).getParent().resolve("config").resolve("explosionoverhaul"); try { Files.createDirectories(configDir, new FileAttribute[0]); ExplosionOverhaul.LOGGER.debug("BlockIndexManager: Using config directory: {}", (Object)configDir); } catch (IOException e) { ExplosionOverhaul.LOGGER.error("Failed to create config directory: {}", (Object)e.getMessage()); } Path saveFile = configDir.resolve("block_index_" + worldId + ".json"); ExplosionOverhaul.LOGGER.debug("BlockIndexManager: Save file path: {}", (Object)saveFile); return saveFile; } public static void saveIndexData(MinecraftServer server) { ExplosionOverhaul.LOGGER.info("BlockIndexManager: Attempting to save data..."); ExplosionOverhaul.LOGGER.info("BlockIndexManager: IndexByDimension size: {}", (Object)indexByDimension.size()); try { Path saveFile = BlockIndexManager.getSaveFilePath(server); if (!scanningComplete) { ExplosionOverhaul.LOGGER.warn("BlockIndexManager: Scan incomplete - deleting save file instead of saving corrupted data"); if (Files.exists(saveFile, new LinkOption[0])) { Files.delete(saveFile); ExplosionOverhaul.LOGGER.info("BlockIndexManager: Deleted incomplete save file: {}", (Object)saveFile); } return; } if (indexByDimension.isEmpty()) { ExplosionOverhaul.LOGGER.warn("BlockIndexManager: No data to save - index is empty!"); return; } ExplosionOverhaul.LOGGER.info("BlockIndexManager: Saving to file: {}", (Object)saveFile); HashMap saveData = new HashMap(); int totalBlocks = 0; for (Map.Entry, ConcurrentHashMap>> dimEntry : indexByDimension.entrySet()) { String dimKey = dimEntry.getKey().m_135782_().toString(); HashMap> chunkMap = new HashMap>(); ExplosionOverhaul.LOGGER.info("BlockIndexManager: Processing dimension: {}", (Object)dimKey); ExplosionOverhaul.LOGGER.info("BlockIndexManager: Chunks in dimension: {}", (Object)dimEntry.getValue().size()); for (Map.Entry> chunkEntry : dimEntry.getValue().entrySet()) { Set blocks = chunkEntry.getValue(); if (blocks.isEmpty()) continue; chunkMap.put(chunkEntry.getKey().toString(), blocks); totalBlocks += blocks.size(); } if (chunkMap.isEmpty()) continue; saveData.put(dimKey, chunkMap); ExplosionOverhaul.LOGGER.info("BlockIndexManager: Added {} chunks with blocks to save data for dimension {}", (Object)chunkMap.size(), (Object)dimKey); } ExplosionOverhaul.LOGGER.info("BlockIndexManager: Total blocks to save: {}", (Object)totalBlocks); ExplosionOverhaul.LOGGER.info("BlockIndexManager: Dimensions to save: {}", (Object)saveData.size()); Gson gson = new GsonBuilder().setPrettyPrinting().create(); String json = gson.toJson(saveData); ExplosionOverhaul.LOGGER.info("BlockIndexManager: JSON length: {}", (Object)json.length()); if (json.length() < 20) { ExplosionOverhaul.LOGGER.warn("BlockIndexManager: JSON too small ({} bytes) - skipping save to prevent corruption", (Object)json.length()); return; } Files.write(saveFile, json.getBytes(), new OpenOption[0]); ExplosionOverhaul.LOGGER.info("BlockIndexManager: Successfully saved index data to {}", (Object)saveFile); } catch (Exception e) { ExplosionOverhaul.LOGGER.error("BlockIndexManager: Failed to save index data: {}", (Object)e.getMessage()); e.printStackTrace(); } } public static boolean loadIndexData(MinecraftServer server) { try { Type saveDataType; Path saveFile = BlockIndexManager.getSaveFilePath(server); if (!Files.exists(saveFile, new LinkOption[0])) { ExplosionOverhaul.LOGGER.debug("BlockIndexManager: No save file found at {}", (Object)saveFile); return false; } Gson gson = new Gson(); String json = new String(Files.readAllBytes(saveFile)); Map saveData = (Map)gson.fromJson(json, saveDataType = new TypeToken>>>(){}.getType()); if (saveData == null || saveData.isEmpty()) { ExplosionOverhaul.LOGGER.warn("BlockIndexManager: Save file is empty or corrupted"); return false; } indexByDimension.clear(); for (Map.Entry dimEntry : saveData.entrySet()) { try { ResourceKey dimKey = ResourceKey.m_135785_((ResourceKey)Registries.f_256858_, (ResourceLocation)ResourceLocation.parse((String)((String)dimEntry.getKey()))); ConcurrentHashMap chunkMap = new ConcurrentHashMap(); for (Map.Entry chunkEntry : ((Map)dimEntry.getValue()).entrySet()) { Long chunkKey = Long.parseLong((String)chunkEntry.getKey()); Set blockSet = Collections.newSetFromMap(new ConcurrentHashMap()); blockSet.addAll((Collection)chunkEntry.getValue()); chunkMap.put(chunkKey, blockSet); } indexByDimension.put((ResourceKey)dimKey, chunkMap); } catch (Exception e) { ExplosionOverhaul.LOGGER.warn("BlockIndexManager: Failed to load dimension {}: {}", dimEntry.getKey(), (Object)e.getMessage()); } } ExplosionOverhaul.LOGGER.info("BlockIndexManager: Successfully loaded index data from {}", (Object)saveFile); ExplosionOverhaul.LOGGER.info("BlockIndexManager: Loaded {} dimensions with indexed blocks", (Object)indexByDimension.size()); scanningComplete = true; totalChunksToScan = 0; chunksScanned = 0; lampsFound = 0; dripstonesFound = 0; glassBlocksFound = 0; return true; } catch (Exception e) { ExplosionOverhaul.LOGGER.error("BlockIndexManager: Failed to load index data: {}", (Object)e.getMessage()); e.printStackTrace(); return false; } } public static boolean hasSaveFile(MinecraftServer server) { Path saveFile = BlockIndexManager.getSaveFilePath(server); return Files.exists(saveFile, new LinkOption[0]) && Files.isRegularFile(saveFile, new LinkOption[0]); } private static int getChunksToScanPerTick() { int cpuPercent = (Integer)Config.COMMON.scan.cpuUsagePercent.get(); int baseChunks = 500; return Math.max(1, baseChunks * cpuPercent / 100); } private static boolean isChunkScanningDisabled() { return (Boolean)Config.COMMON.scan.enableBlockIndexing.get() == false; } private static boolean isScanning() { return !chunkScanQueue.isEmpty() && totalChunksToScan > 0 && !scanningComplete; } public static boolean isRescanMode() { return isRescanMode; } public static void register(ServerLevel level, BlockPos pos, BlockType type) { ConcurrentHashMap> dimIndex = BlockIndexManager.getOrCreateDimIndex(level); ChunkPos cp = new ChunkPos(pos); long ck = BlockIndexManager.chunkKey(cp); dimIndex.computeIfAbsent(ck, k -> Collections.newSetFromMap(new ConcurrentHashMap())).add(BlockIndexManager.packPos(pos)); } public static void unregister(ServerLevel level, BlockPos pos, BlockType type) { ChunkPos cp; long ck; ConcurrentHashMap> dimIndex = BlockIndexManager.getOrCreateDimIndex(level); Set set = dimIndex.get(ck = BlockIndexManager.chunkKey(cp = new ChunkPos(pos))); if (set != null) { set.remove(BlockIndexManager.packPos(pos)); if (set.isEmpty()) { dimIndex.remove(ck); } } } public static void scanChunk(ServerLevel level, ChunkPos cp) { long ck; ConcurrentHashMap> dimIndex = BlockIndexManager.getOrCreateDimIndex(level); if (dimIndex.containsKey(ck = BlockIndexManager.chunkKey(cp))) { return; } int baseX = cp.m_45604_(); int baseZ = cp.m_45605_(); Set set = Collections.newSetFromMap(new ConcurrentHashMap()); int chunkLamps = 0; int chunkDripstones = 0; int chunkGlass = 0; for (int lx = 0; lx < 16; ++lx) { for (int lz = 0; lz < 16; ++lz) { for (int y = level.m_141937_(); y < level.m_151558_(); ++y) { BlockPos pos = new BlockPos(baseX + lx, y, baseZ + lz); if (!level.m_46749_(pos)) continue; BlockState state = level.m_8055_(pos); if (state.m_60713_(Blocks.f_50261_)) { set.add(BlockIndexManager.packPos(pos)); ++chunkLamps; continue; } if (state.m_60713_(Blocks.f_152588_)) { set.add(BlockIndexManager.packPos(pos)); ++chunkDripstones; continue; } if (!BlockIndexManager.isGlassBlock(state)) continue; set.add(BlockIndexManager.packPos(pos)); ++chunkGlass; } } } lampsFound += chunkLamps; dripstonesFound += chunkDripstones; glassBlocksFound += chunkGlass; if (!set.isEmpty()) { dimIndex.put(ck, set); ExplosionOverhaul.LOGGER.info("BlockIndexManager: Found {} blocks in chunk {} {}: {}", new Object[]{set.size(), cp.f_45578_, cp.f_45579_, set.size() > 10 ? set.size() + " blocks" : set}); } if (++chunksScanned >= totalChunksToScan && totalChunksToScan > 0) { MinecraftServer server; scanningComplete = true; int totalBlocksFound = 0; for (Map.Entry, ConcurrentHashMap>> dimEntry : indexByDimension.entrySet()) { for (Map.Entry> chunkEntry : dimEntry.getValue().entrySet()) { totalBlocksFound += chunkEntry.getValue().size(); } } ExplosionOverhaul.LOGGER.info("BlockIndexManager: chunk scanning completed! Scanned {}/{} chunks", (Object)chunksScanned, (Object)totalChunksToScan); ExplosionOverhaul.LOGGER.info("BlockIndexManager: Found {} total blocks across {} dimensions", (Object)totalBlocksFound, (Object)indexByDimension.size()); try { server = ServerLifecycleHooks.getCurrentServer(); if (server != null) { ExplosionOverhaul.LOGGER.info("BlockIndexManager: Starting auto-save after scan completion..."); BlockIndexManager.saveIndexData(server); } } catch (Exception e) { ExplosionOverhaul.LOGGER.warn("Failed to auto-save scan data: {}", (Object)e.getMessage()); e.printStackTrace(); } try { server = ServerLifecycleHooks.getCurrentServer(); if (server != null) { server.execute(() -> BlockIndexManager.sendProgressUpdate()); } } catch (Exception e) { ExplosionOverhaul.LOGGER.warn("Failed to enqueue final progress update: {}", (Object)e.getMessage()); } } if (chunksScanned % 10 == 0 || scanningComplete) { BlockIndexManager.sendProgressUpdate(); } } public static void removeChunk(ServerLevel level, ChunkPos cp) { ConcurrentHashMap> dimIndex = BlockIndexManager.getOrCreateDimIndex(level); long ck = BlockIndexManager.chunkKey(cp); dimIndex.remove(ck); Set pending = BlockIndexManager.getOrCreatePendingSet(level); pending.remove(ck); } public static List getNearby(ServerLevel level, BlockPos center, int radius, BlockType type) { ArrayList result = new ArrayList(); int minX = center.m_123341_() - radius; int maxX = center.m_123341_() + radius; int minZ = center.m_123343_() - radius; int maxZ = center.m_123343_() + radius; int minChunkX = minX >> 4; int maxChunkX = maxX >> 4; int minChunkZ = minZ >> 4; int maxChunkZ = maxZ >> 4; ConcurrentHashMap> dimIndex = BlockIndexManager.getOrCreateDimIndex(level); long r2 = (long)radius * (long)radius; for (int cx = minChunkX; cx <= maxChunkX; ++cx) { for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) { long ck = BlockIndexManager.chunkKey(new ChunkPos(cx, cz)); Set set = dimIndex.get(ck); if (set == null || set.isEmpty()) continue; for (long packed : set) { long dz; long dy; BlockPos pos = BlockIndexManager.unpackPos(packed); long dx = pos.m_123341_() - center.m_123341_(); if (dx * dx + (dy = (long)(pos.m_123342_() - center.m_123342_())) * dy + (dz = (long)(pos.m_123343_() - center.m_123343_())) * dz > r2) continue; BlockState state = level.m_8055_(pos); if (type == BlockType.LAMP && state.m_60713_(Blocks.f_50261_)) { result.add(pos); } if (type == BlockType.DRIPSTONE && state.m_60713_(Blocks.f_152588_)) { result.add(pos); } if (type != BlockType.GLASS || !BlockIndexManager.isGlassBlock(state)) continue; result.add(pos); } } } return result; } @SubscribeEvent public static void onChunkLoad(ChunkEvent.Load event) { if (!ENABLED) { return; } if (BlockIndexManager.isChunkScanningDisabled()) { return; } LevelAccessor levelAccessor = event.getLevel(); if (!(levelAccessor instanceof ServerLevel)) { return; } ServerLevel level = (ServerLevel)levelAccessor; if (userDeclinedScan) { return; } if (!isSingleplayer && !serverScanDecisionMade) { return; } if (INITIAL_SCAN_ENQUEUED) { return; } ResourceKey chunkDimension = level.m_46472_(); if (lastScannedDimension != null && !lastScannedDimension.equals((Object)chunkDimension)) { return; } if (scanningComplete) { return; } long ck = BlockIndexManager.chunkKey(event.getChunk().m_7697_()); Set pending = BlockIndexManager.getOrCreatePendingSet(level); if (pending.add(ck)) { chunkScanQueue.add(new ScanTask((ResourceKey)level.m_46472_(), event.getChunk().m_7697_())); } } @SubscribeEvent public static void onChunkUnload(ChunkEvent.Unload event) { if (!ENABLED) { return; } LevelAccessor levelAccessor = event.getLevel(); if (!(levelAccessor instanceof ServerLevel)) { return; } ServerLevel level = (ServerLevel)levelAccessor; BlockIndexManager.removeChunk(level, event.getChunk().m_7697_()); chunkScanQueue.removeIf(t -> t.dim().equals((Object)level.m_46472_()) && t.chunkPos().equals((Object)event.getChunk().m_7697_())); } @SubscribeEvent public static void onBlockPlaced(BlockEvent.EntityPlaceEvent event) { if (!ENABLED) { return; } LevelAccessor levelAccessor = event.getLevel(); if (!(levelAccessor instanceof ServerLevel)) { return; } ServerLevel level = (ServerLevel)levelAccessor; BlockPos pos = event.getPos(); BlockState state = level.m_8055_(pos); if (state.m_60713_(Blocks.f_50261_)) { BlockIndexManager.register(level, pos, BlockType.LAMP); } else if (state.m_60713_(Blocks.f_152588_)) { BlockIndexManager.register(level, pos, BlockType.DRIPSTONE); } else if (BlockIndexManager.isGlassBlock(state)) { BlockIndexManager.register(level, pos, BlockType.GLASS); } } @SubscribeEvent public static void onBlockBroken(BlockEvent.BreakEvent event) { if (!ENABLED) { return; } LevelAccessor levelAccessor = event.getLevel(); if (!(levelAccessor instanceof ServerLevel)) { return; } ServerLevel level = (ServerLevel)levelAccessor; BlockPos pos = event.getPos(); BlockIndexManager.unregister(level, pos, BlockType.LAMP); BlockIndexManager.unregister(level, pos, BlockType.DRIPSTONE); BlockIndexManager.unregister(level, pos, BlockType.GLASS); } public static boolean isPlayerAuthorized(ServerPlayer player) { if (player == null) { return false; } return player.hasPermissions(2) || isSingleplayer; } @SubscribeEvent public static void onPlayerJoinServer(PlayerEvent.PlayerLoggedInEvent event) { if (!ENABLED) { return; } if (BlockIndexManager.isChunkScanningDisabled()) { return; } Player player = event.getEntity(); if (!(player instanceof ServerPlayer)) { return; } ServerPlayer player2 = (ServerPlayer)player; if (BlockIndexManager.isPlayerAuthorized(player2)) { ServerLevel level = player2.m_284548_(); MinecraftServer server = level.m_7654_(); boolean saveFileExists = BlockIndexManager.hasSaveFile(server); if (isSingleplayer) { if (!(waitingForUserInput || waitingForLoadDecision || scanningComplete)) { chunkScanQueue.clear(); totalChunksToScan = 0; chunksScanned = 0; } if (!(waitingForUserInput || waitingForLoadDecision || BlockIndexManager.isScanning() || scanningComplete || userDeclinedScan)) { pendingScanLevel = level; if (saveFileExists) { waitingForLoadDecision = true; hasSaveFile = true; try { ScanLoadPromptPacket packet = new ScanLoadPromptPacket(true); PacketHandler.sendToPlayer(player2, packet); ExplosionOverhaul.LOGGER.info("BlockIndexManager: showing load prompt for existing save file in world: {}", (Object)level.m_46472_().m_135782_()); } catch (Exception e) { ExplosionOverhaul.LOGGER.warn("Failed to send load prompt: {}", (Object)e.getMessage()); } } else { waitingForUserInput = true; hasSaveFile = false; BlockIndexManager.sendScanPromptPacket(true); ExplosionOverhaul.LOGGER.info("BlockIndexManager: waiting for user input to start scan in world: {}", (Object)level.m_46472_().m_135782_()); } } else if (BlockIndexManager.isScanning() || !chunkScanQueue.isEmpty() && totalChunksToScan > 0) { try { ScanProgressPacket packet = new ScanProgressPacket(totalChunksToScan, chunksScanned, scanningComplete, lampsFound, dripstonesFound, glassBlocksFound); PacketHandler.sendToPlayer(player2, packet); ExplosionOverhaul.LOGGER.debug("Sent scan progress to rejoining OP player: {}/{} chunks", (Object)chunksScanned, (Object)totalChunksToScan); } catch (Exception e) { ExplosionOverhaul.LOGGER.warn("Failed to send scan progress to rejoining OP player: {}", (Object)e.getMessage()); } } } else if (!serverScanDecisionMade && !userDeclinedScan) { if (!serverScanPromptShown) { serverScanPromptShown = true; ExplosionOverhaul.LOGGER.info("BlockIndexManager: first OP joined server, waiting for scan decision from: {}", (Object)player2.m_7755_().getString()); } else { ExplosionOverhaul.LOGGER.info("BlockIndexManager: additional OP joined server, showing pending decision to: {}", (Object)player2.m_7755_().getString()); } pendingScanLevel = level; if (saveFileExists) { waitingForLoadDecision = true; hasSaveFile = true; try { ScanLoadPromptPacket packet = new ScanLoadPromptPacket(true); PacketHandler.sendToPlayer(player2, packet); ExplosionOverhaul.LOGGER.info("BlockIndexManager: showing load prompt for existing save file to OP: {}", (Object)player2.m_7755_().getString()); } catch (Exception e) { ExplosionOverhaul.LOGGER.warn("Failed to send load prompt: {}", (Object)e.getMessage()); } } else { waitingForUserInput = true; hasSaveFile = false; try { ScanPromptPacket packet = new ScanPromptPacket(true); PacketHandler.sendToPlayer(player2, packet); ExplosionOverhaul.LOGGER.info("BlockIndexManager: showing scan prompt to OP: {}", (Object)player2.m_7755_().getString()); } catch (Exception e) { ExplosionOverhaul.LOGGER.warn("Failed to send scan prompt: {}", (Object)e.getMessage()); } } } else if (totalChunksToScan > 0 && !scanningComplete) { try { ScanProgressPacket packet = new ScanProgressPacket(totalChunksToScan, chunksScanned, scanningComplete, lampsFound, dripstonesFound, glassBlocksFound); PacketHandler.sendToPlayer(player2, packet); ExplosionOverhaul.LOGGER.debug("Sent scan progress to joining OP player: {}/{} chunks", (Object)chunksScanned, (Object)totalChunksToScan); } catch (Exception e) { ExplosionOverhaul.LOGGER.warn("Failed to send scan progress to joining OP player: {}", (Object)e.getMessage()); } } } } @SubscribeEvent public static void onServerTick(TickEvent.ServerTickEvent event) { ScanTask task; if (event.phase != TickEvent.Phase.END) { return; } if (BlockIndexManager.isChunkScanningDisabled()) { return; } if (userDeclinedScan) { return; } if (!isSingleplayer && !serverScanDecisionMade) { return; } int chunksToScan = BlockIndexManager.getChunksToScanPerTick(); for (int processed = 0; processed < chunksToScan && (task = chunkScanQueue.poll()) != null; ++processed) { MinecraftServer server = event.getServer(); ServerLevel level = server.m_129880_(task.dim()); if (level == null) continue; try { BlockIndexManager.scanChunk(level, task.chunkPos()); long ck = BlockIndexManager.chunkKey(task.chunkPos()); Set pending = BlockIndexManager.getOrCreatePendingSet(level); pending.remove(ck); continue; } catch (Exception ex) { ExplosionOverhaul.LOGGER.warn("BlockIndexManager: error scanning chunk {} {}: {}", new Object[]{task.chunkPos().f_45578_, task.chunkPos().f_45579_, ex.toString()}); } } } @SubscribeEvent public static void onServerStarted(ServerStartedEvent event) { currentServer = event.getServer(); abortCurrentScan = false; if (!ENABLED) { return; } if (BlockIndexManager.isChunkScanningDisabled()) { return; } if (INITIAL_SCAN_ENQUEUED) { return; } MinecraftServer server = event.getServer(); isSingleplayer = server.m_129792_(); if (!isSingleplayer) { userDeclinedScan = false; } serverScanPromptShown = false; serverScanDecisionMade = false; if (isSingleplayer) { ExplosionOverhaul.LOGGER.info("BlockIndexManager: singleplayer detected, will scan on world entry"); INITIAL_SCAN_ENQUEUED = true; return; } ExplosionOverhaul.LOGGER.info("BlockIndexManager: multiplayer server detected, will wait for user decision before scanning"); INITIAL_SCAN_ENQUEUED = true; } private static void scanExistingChunksOnServer(MinecraftServer server) { try { int totalChunks; chunksScanned = 0; scanningComplete = false; lampsFound = 0; dripstonesFound = 0; glassBlocksFound = 0; chunkScanQueue.clear(); pendingChunkKeys.clear(); HyperScanner scanner = new HyperScanner(server); totalChunksToScan = totalChunks = scanner.prepareTasks(); INITIAL_SCAN_ENQUEUED = true; BlockIndexManager.sendProgressUpdate(); CompletableFuture.runAsync(() -> { try { scanner.run(); chunksScanned = scanner.getProcessedChunks(); lampsFound = scanner.getLampsFound(); dripstonesFound = scanner.getDripstonesFound(); glassBlocksFound = scanner.getGlassFound(); scanningComplete = true; server.execute(() -> { BlockIndexManager.sendProgressUpdate(); BlockIndexManager.saveIndexDataAfterScan(server); ExplosionOverhaul.LOGGER.info("HyperScanner: background scan complete. Found {} lamps, {} dripstones, {} glass.", new Object[]{lampsFound, dripstonesFound, glassBlocksFound}); }); } catch (Exception e) { ExplosionOverhaul.LOGGER.error("HyperScanner: background scan failed", (Throwable)e); } }); } catch (Exception ex) { ExplosionOverhaul.LOGGER.warn("BlockIndexManager: failed to start scan: {}", (Object)ex.toString()); } } @SubscribeEvent public static void onServerStopped(ServerStoppedEvent event) { if (!ENABLED) { return; } abortCurrentScan = true; if (!indexByDimension.isEmpty() && scanningComplete) { try { BlockIndexManager.saveIndexData(event.getServer()); ExplosionOverhaul.LOGGER.info("BlockIndexManager: saved data before server shutdown"); } catch (Exception e) { ExplosionOverhaul.LOGGER.warn("Failed to save data on server stop: {}", (Object)e.getMessage()); } } userDeclinedScan = false; serverScanPromptShown = false; serverScanDecisionMade = false; waitingForUserInput = false; waitingForLoadDecision = false; pendingScanLevel = null; isRescanMode = false; rescanRequestingPlayer = null; hasSaveFile = false; currentWorldId = null; INITIAL_SCAN_ENQUEUED = false; try { ScanProgressPacket packet = new ScanProgressPacket(0, 0, true, 0, 0, 0); PacketHandler.sendToAll(packet); } catch (Exception exception) { // empty catch block } currentServer = null; scanningComplete = false; totalChunksToScan = 0; chunksScanned = 0; ExplosionOverhaul.LOGGER.info("BlockIndexManager: reset scan state and aborted background tasks on server stop"); } @SubscribeEvent public static void onPlayerLogout(PlayerEvent.PlayerLoggedOutEvent event) { Player player; if (!ENABLED) { return; } if (isSingleplayer) { abortCurrentScan = true; waitingForUserInput = false; waitingForLoadDecision = false; pendingScanLevel = null; userDeclinedScan = false; hasSaveFile = false; scanningComplete = false; totalChunksToScan = 0; chunksScanned = 0; BlockIndexManager.clearAndSaveData(); ExplosionOverhaul.LOGGER.info("BlockIndexManager: saved and cleared scan data on singleplayer logout"); } if ((player = event.getEntity()) instanceof ServerPlayer) { ServerPlayer player2 = (ServerPlayer)player; try { ScanProgressPacket packet = new ScanProgressPacket(0, 0, true, 0, 0, 0); PacketHandler.sendToPlayer(player2, packet); } catch (Exception exception) { // empty catch block } } } private static Path resolveRegionPath(ServerLevel lvl) { try { Path worldPath = lvl.m_7654_().m_129843_(LevelResource.f_78182_); String dimPath = lvl.m_46472_().m_135782_().m_135815_(); if ("overworld".equals(dimPath)) { return worldPath.resolve("region"); } if ("the_nether".equals(dimPath)) { return worldPath.resolve("DIM-1").resolve("region"); } if ("the_end".equals(dimPath)) { return worldPath.resolve("DIM1").resolve("region"); } return worldPath.resolve("dimensions").resolve(lvl.m_46472_().m_135782_().m_135827_()).resolve(lvl.m_46472_().m_135782_().m_135815_()).resolve("region"); } catch (Exception e) { ExplosionOverhaul.LOGGER.warn("HyperScanner: failed to resolve region path: {}", (Object)e.getMessage()); return null; } } private static /* synthetic */ ServerPlayer lambda$loadExistingData$11(ServerPlayer serverPlayer) { return serverPlayer; } static { indexByDimension = new ConcurrentHashMap, ConcurrentHashMap>>(); chunkScanQueue = new ConcurrentLinkedQueue(); pendingChunkKeys = new ConcurrentHashMap, Set>(); ENABLED = true; INITIAL_SCAN_ENQUEUED = false; totalChunksToScan = 0; chunksScanned = 0; scanningComplete = false; abortCurrentScan = false; lampsFound = 0; dripstonesFound = 0; glassBlocksFound = 0; lastScannedDimension = null; waitingForUserInput = false; pendingScanLevel = null; currentScannedWorld = null; isSingleplayer = false; serverScanPromptShown = false; serverScanDecisionMade = false; isRescanMode = false; rescanRequestingPlayer = null; userDeclinedScan = false; waitingForLoadDecision = false; hasSaveFile = false; currentWorldId = null; DEFAULT_REINFORCED_GLASS_BLACKLIST = List.of("securitycraft:reinforced_white_stained_glass", "securitycraft:reinforced_orange_stained_glass", "securitycraft:reinforced_magenta_stained_glass", "securitycraft:reinforced_light_blue_stained_glass", "securitycraft:reinforced_yellow_stained_glass", "securitycraft:reinforced_lime_stained_glass", "securitycraft:reinforced_pink_stained_glass", "securitycraft:reinforced_gray_stained_glass", "securitycraft:reinforced_light_gray_stained_glass", "securitycraft:reinforced_cyan_stained_glass", "securitycraft:reinforced_purple_stained_glass", "securitycraft:reinforced_blue_stained_glass", "securitycraft:reinforced_brown_stained_glass", "securitycraft:reinforced_green_stained_glass", "securitycraft:reinforced_red_stained_glass", "securitycraft:reinforced_black_stained_glass", "securitycraft:reinforced_white_stained_glass_pane", "securitycraft:reinforced_orange_stained_glass_pane", "securitycraft:reinforced_magenta_stained_glass_pane", "securitycraft:reinforced_light_blue_stained_glass_pane", "securitycraft:reinforced_yellow_stained_glass_pane", "securitycraft:reinforced_lime_stained_glass_pane", "securitycraft:reinforced_pink_stained_glass_pane", "securitycraft:reinforced_gray_stained_glass_pane", "securitycraft:reinforced_light_gray_stained_glass_pane", "securitycraft:reinforced_cyan_stained_glass_pane", "securitycraft:reinforced_purple_stained_glass_pane", "securitycraft:reinforced_blue_stained_glass_pane", "securitycraft:reinforced_brown_stained_glass_pane", "securitycraft:reinforced_green_stained_glass_pane", "securitycraft:reinforced_red_stained_glass_pane", "securitycraft:reinforced_black_stained_glass_pane", "securitycraft:reinforced_glass", "securitycraft:reinforced_glass_pane", "create_tank_defenses:titanium_morning_reinforced_blast_glass", "mofus_better_end_:reinforced_glass", "crusty_chunks:reinforced_glass", "createnuclear:reinforced_glass", "mekanismgenerators:reactor_glass", "mekanism:structural_glass"); MOD_CONFIG_DIRECTORY = Paths.get("config", "explosionoverhaul"); GLASS_BLACKLIST_FILE = MOD_CONFIG_DIRECTORY.resolve("GlassBlacklist.json"); GLASS_BLACKLIST = new CopyOnWriteArrayList(DEFAULT_REINFORCED_GLASS_BLACKLIST); GLASS_BLACKLIST_LOOKUP = new AtomicReference>(new HashSet(DEFAULT_REINFORCED_GLASS_BLACKLIST)); BlockIndexManager.loadGlassBlacklistFromJson(); } private record ScanTask(ResourceKey dim, ChunkPos chunkPos) { } public static enum BlockType { LAMP, DRIPSTONE, GLASS; } private static class HyperScanner { private static final int SECTOR_BYTES = 4096; private final MinecraftServer server; private final ConcurrentLinkedQueue tasks = new ConcurrentLinkedQueue(); private final AtomicInteger processedChunks = new AtomicInteger(); private final LongAdder lamps = new LongAdder(); private final LongAdder drips = new LongAdder(); private final LongAdder glass = new LongAdder(); private final int workerCount; HyperScanner(MinecraftServer server) { this.server = server; this.workerCount = this.resolveWorkerCount(); } int prepareTasks() { int total = 0; for (ServerLevel level : this.server.m_129785_()) { Path regionPath = BlockIndexManager.resolveRegionPath(level); if (regionPath == null || !Files.exists(regionPath, new LinkOption[0])) { ExplosionOverhaul.LOGGER.info("HyperScanner: region path not found for {}", (Object)level.m_46472_().m_135782_()); continue; } try { Stream regionFiles = Files.list(regionPath); try { for (Path regionFile : regionFiles.filter(p -> p.toString().endsWith(".mca")).toList()) { RegionInfo info = this.parseRegion(regionFile); if (info == null || info.chunkCount <= 0) continue; this.tasks.add(new RegionTask(level, regionFile, info.regionX, info.regionZ)); total += info.chunkCount; } } finally { if (regionFiles == null) continue; regionFiles.close(); } } catch (Exception e) { ExplosionOverhaul.LOGGER.warn("HyperScanner: failed to enumerate region files in {}: {}", (Object)regionPath, (Object)e.getMessage()); } } ExplosionOverhaul.LOGGER.info("HyperScanner: prepared {} region tasks, total chunks {}", (Object)this.tasks.size(), (Object)total); return total; } void run() { if (this.tasks.isEmpty()) { ExplosionOverhaul.LOGGER.info("HyperScanner: no tasks to run"); return; } ExecutorService pool = Executors.newFixedThreadPool(this.workerCount, r -> { Thread t = new Thread(r, "EO-HyperScanner"); t.setDaemon(true); return t; }); for (int i = 0; i < this.workerCount; ++i) { pool.execute(this::workerLoop); } pool.shutdown(); try { while (!pool.isTerminated()) { pool.awaitTermination(1L, TimeUnit.SECONDS); } } catch (InterruptedException ie) { Thread.currentThread().interrupt(); } } int getProcessedChunks() { return this.processedChunks.get(); } int getLampsFound() { return this.lamps.intValue(); } int getDripstonesFound() { return this.drips.intValue(); } int getGlassFound() { return this.glass.intValue(); } private void workerLoop() { RegionTask task; while (!abortCurrentScan && (task = this.tasks.poll()) != null) { this.scanRegion(task); } } private void scanRegion(RegionTask task) { try (RandomAccessFile raf = new RandomAccessFile(task.file().toFile(), "r");){ byte[] header = new byte[4096]; int read = raf.read(header); if (read < 4096) { return; } for (int i = 0; i < 1024; ++i) { if (abortCurrentScan) { return; } int offsetIndex = i * 4; int chunkOffset = (header[offsetIndex] & 0xFF) << 16 | (header[offsetIndex + 1] & 0xFF) << 8 | header[offsetIndex + 2] & 0xFF; int sectorCount = header[offsetIndex + 3] & 0xFF; if (chunkOffset == 0 || sectorCount == 0) continue; int localX = i % 32; int localZ = i / 32; int chunkX = task.regionX() * 32 + localX; int chunkZ = task.regionZ() * 32 + localZ; try { this.processChunk(raf, chunkOffset, sectorCount, new ChunkPos(chunkX, chunkZ), task.level()); continue; } catch (Exception e) { ExplosionOverhaul.LOGGER.warn("HyperScanner: failed to process chunk {} {} in {}: {}", new Object[]{chunkX, chunkZ, task.file().getFileName(), e.getMessage()}); } } } catch (Exception e) { ExplosionOverhaul.LOGGER.warn("HyperScanner: failed to scan region {}: {}", (Object)task.file().getFileName(), (Object)e.getMessage()); } } private void processChunk(RandomAccessFile raf, int chunkOffsetSectors, int sectorCount, ChunkPos chunkPos, ServerLevel level) throws IOException { InflaterInputStream in; long filePos = (long)chunkOffsetSectors * 4096L; raf.seek(filePos); int length = raf.readInt(); if (length <= 0 || length > sectorCount * 4096) { return; } int compressionType = raf.readUnsignedByte(); byte[] data = new byte[length - 1]; raf.readFully(data); ByteArrayInputStream base = new ByteArrayInputStream(data); if (compressionType == 2) { in = new InflaterInputStream(base); } else if (compressionType == 1) { in = new GZIPInputStream(base); } else { return; } CompoundTag root = NbtIo.m_128928_((DataInput)new DataInputStream(in)); if (root == null) { return; } CompoundTag levelTag = root.m_128425_("Level", 10) ? root.m_128469_("Level") : (root.m_128425_("level", 10) ? root.m_128469_("level") : root); ListTag sections = levelTag.m_128437_("sections", 10); HashSet found = new HashSet(); for (int i = 0; i < sections.size(); ++i) { CompoundTag section = sections.m_128728_(i); this.processSection(section, chunkPos, level, found); } if (!found.isEmpty()) { ConcurrentHashMap> dimIndex = BlockIndexManager.getOrCreateDimIndex(level); dimIndex.put(BlockIndexManager.chunkKey(chunkPos), found); } if (abortCurrentScan) { return; } int processed = this.processedChunks.incrementAndGet(); this.maybeSendProgress(processed); } private void processSection(CompoundTag section, ChunkPos chunkPos, ServerLevel level, Set out) { if (!section.m_128425_("block_states", 10)) { return; } CompoundTag blockStatesTag = section.m_128469_("block_states"); if (blockStatesTag.m_128425_("palette", 9)) { ListTag palette = blockStatesTag.m_128437_("palette", 10); boolean hasInterestingBlock = false; for (int i = 0; i < palette.size(); ++i) { String name = palette.m_128728_(i).m_128461_("Name"); if (!name.equals("minecraft:redstone_lamp") && !name.equals("minecraft:pointed_dripstone") && (!name.contains("glass") || BlockIndexManager.isGlassProtected(name))) continue; hasInterestingBlock = true; break; } if (!hasInterestingBlock) { return; } } try { DataResult result = PalettedContainer.m_238371_((IdMap)Block.f_49791_, (Codec)BlockState.f_61039_, (PalettedContainer.Strategy)PalettedContainer.Strategy.f_188137_, (Object)Blocks.f_50016_.m_49966_()).parse((DynamicOps)NbtOps.f_128958_, (Object)blockStatesTag); result.resultOrPartial(err -> {}).ifPresent(container -> { byte sectionY = section.m_128445_("Y"); int baseY = sectionY * 16; if (baseY < level.m_141937_() || baseY >= level.m_151558_()) { return; } IdentityHashMap stateTypeCache = new IdentityHashMap(); for (int y = 0; y < 16; ++y) { int worldY = baseY + y; if (worldY < level.m_141937_() || worldY >= level.m_151558_()) continue; for (int z = 0; z < 16; ++z) { for (int x = 0; x < 16; ++x) { BlockState state = (BlockState)container.m_63087_(x, y, z); Byte type = (Byte)stateTypeCache.get(state); if (type == null) { type = state.m_60713_(Blocks.f_50261_) ? Byte.valueOf((byte)1) : (state.m_60713_(Blocks.f_152588_) ? Byte.valueOf((byte)2) : (BlockIndexManager.isGlassBlock(state) ? Byte.valueOf((byte)3) : Byte.valueOf((byte)0))); stateTypeCache.put(state, type); } if (type == 0) continue; int worldX = chunkPos.m_45604_() + x; int worldZ = chunkPos.m_45605_() + z; out.add(BlockPos.m_121882_((int)worldX, (int)worldY, (int)worldZ)); if (type == 1) { this.lamps.increment(); continue; } if (type == 2) { this.drips.increment(); continue; } if (type != 3) continue; this.glass.increment(); } } } }); } catch (Exception exception) { // empty catch block } } private void maybeSendProgress(int processed) { if (processed % 50 == 0 || processed == totalChunksToScan) { int l = this.lamps.intValue(); int d = this.drips.intValue(); int g = this.glass.intValue(); this.server.execute(() -> BlockIndexManager.sendMultiThreadProgressUpdate(processed, l, d, g)); } } private RegionInfo parseRegion(Path regionFile) { int regionZ; int regionX; String fileName = regionFile.getFileName().toString(); if (!fileName.startsWith("r.") || !fileName.endsWith(".mca")) { return null; } String[] parts = fileName.substring(2, fileName.length() - 4).split("\\."); if (parts.length != 2) { return null; } try { regionX = Integer.parseInt(parts[0]); regionZ = Integer.parseInt(parts[1]); } catch (NumberFormatException nfe) { return null; } int chunkCount = this.countChunksInRegion(regionFile); return new RegionInfo(regionX, regionZ, chunkCount); } /* * Enabled aggressive block sorting * Enabled unnecessary exception pruning * Enabled aggressive exception aggregation */ private int countChunksInRegion(Path regionFile) { int count = 0; try (RandomAccessFile raf = new RandomAccessFile(regionFile.toFile(), "r");){ byte[] header = new byte[4096]; int read = raf.read(header); if (read < 4096) { int n = 0; return n; } int i = 0; while (i < 1024) { int offset = i * 4; int chunkOffset = (header[offset] & 0xFF) << 16 | (header[offset + 1] & 0xFF) << 8 | header[offset + 2] & 0xFF; int sectorCount = header[offset + 3] & 0xFF; if (chunkOffset > 0 && sectorCount > 0) { ++count; } ++i; } return count; } catch (Exception e) { ExplosionOverhaul.LOGGER.warn("HyperScanner: failed to read region header {}: {}", (Object)regionFile.getFileName(), (Object)e.getMessage()); } return count; } private int resolveWorkerCount() { int configured = (Integer)Config.COMMON.scan.maxScanThreads.get(); int cores = Runtime.getRuntime().availableProcessors(); if (configured <= 0) { return Math.max(1, cores); } return Math.max(1, Math.min(configured, cores)); } } private record RegionInfo(int regionX, int regionZ, int chunkCount) { } private record RegionTask(ServerLevel level, Path file, int regionX, int regionZ) { } }