generated from MrSphay/codex-agent-repository-kit
1802 lines
86 KiB
Java
1802 lines
86 KiB
Java
/*
|
|
* 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<ResourceKey<Level>, ConcurrentHashMap<Long, Set<Long>>> indexByDimension;
|
|
private static final ConcurrentLinkedQueue<ScanTask> chunkScanQueue;
|
|
private static final Map<ResourceKey<Level>, Set<Long>> 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<Level> lastScannedDimension;
|
|
private static volatile boolean waitingForUserInput;
|
|
private static volatile ServerLevel pendingScanLevel;
|
|
private static volatile ResourceKey<Level> 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<String> DEFAULT_REINFORCED_GLASS_BLACKLIST;
|
|
private static final Path MOD_CONFIG_DIRECTORY;
|
|
private static final Path GLASS_BLACKLIST_FILE;
|
|
private static final CopyOnWriteArrayList<String> GLASS_BLACKLIST;
|
|
private static final AtomicReference<Set<String>> GLASS_BLACKLIST_LOOKUP;
|
|
|
|
private static ConcurrentHashMap<Long, Set<Long>> getOrCreateDimIndex(ServerLevel level) {
|
|
ResourceKey dim = level.m_46472_();
|
|
return indexByDimension.computeIfAbsent((ResourceKey<Level>)dim, k -> new ConcurrentHashMap());
|
|
}
|
|
|
|
private static Set<Long> getOrCreatePendingSet(ServerLevel level) {
|
|
ResourceKey dim = level.m_46472_();
|
|
return pendingChunkKeys.computeIfAbsent((ResourceKey<Level>)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<ChunkPos> 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<ChunkPos> existingChunks) {
|
|
for (ChunkPos cp : existingChunks) {
|
|
chunkScanQueue.add(new ScanTask((ResourceKey<Level>)level.m_46472_(), cp));
|
|
}
|
|
BlockIndexManager.sendProgressUpdate();
|
|
}
|
|
|
|
private static Set<ChunkPos> getAllExistingChunks(ServerLevel level) {
|
|
Set<ChunkPos> 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<Path> 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<Path> 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<String> getReinforcedGlassBlacklist() {
|
|
return new ArrayList<String>(GLASS_BLACKLIST);
|
|
}
|
|
|
|
public static List<String> getDefaultReinforcedGlassBlacklist() {
|
|
return new ArrayList<String>(DEFAULT_REINFORCED_GLASS_BLACKLIST);
|
|
}
|
|
|
|
public static void setReinforcedGlassBlacklistFromList(List<String> 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<List<String>>(){}.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<String>(GLASS_BLACKLIST), (Appendable)writer);
|
|
}
|
|
catch (IOException e) {
|
|
ExplosionOverhaul.LOGGER.warn("BlockIndexManager: failed to save GlassBlacklist.json", (Throwable)e);
|
|
}
|
|
}
|
|
|
|
private static void applyGlassBlacklistList(List<String> list) {
|
|
List<String> normalized = BlockIndexManager.normalizeBlacklist(list);
|
|
GLASS_BLACKLIST.clear();
|
|
GLASS_BLACKLIST.addAll(normalized);
|
|
GLASS_BLACKLIST_LOOKUP.set(new HashSet<String>(normalized));
|
|
}
|
|
|
|
private static List<String> normalizeBlacklist(List<String> entries) {
|
|
if (entries == null) {
|
|
return new ArrayList<String>();
|
|
}
|
|
ArrayList<String> result = new ArrayList<String>();
|
|
HashSet<String> seen = new HashSet<String>();
|
|
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<ResourceKey<Level>, ConcurrentHashMap<Long, Set<Long>>> dimEntry : indexByDimension.entrySet()) {
|
|
String dimKey = dimEntry.getKey().m_135782_().toString();
|
|
HashMap<String, Set<Long>> chunkMap = new HashMap<String, Set<Long>>();
|
|
ExplosionOverhaul.LOGGER.info("BlockIndexManager: Processing dimension: {}", (Object)dimKey);
|
|
ExplosionOverhaul.LOGGER.info("BlockIndexManager: Chunks in dimension: {}", (Object)dimEntry.getValue().size());
|
|
for (Map.Entry<Long, Set<Long>> chunkEntry : dimEntry.getValue().entrySet()) {
|
|
Set<Long> 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<Map<String, Map<String, Set<Long>>>>(){}.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<Level>)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<Long, Set<Long>> 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<Long, Set<Long>> dimIndex = BlockIndexManager.getOrCreateDimIndex(level);
|
|
Set<Long> 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<Long, Set<Long>> 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<ResourceKey<Level>, ConcurrentHashMap<Long, Set<Long>>> dimEntry : indexByDimension.entrySet()) {
|
|
for (Map.Entry<Long, Set<Long>> 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<Long, Set<Long>> dimIndex = BlockIndexManager.getOrCreateDimIndex(level);
|
|
long ck = BlockIndexManager.chunkKey(cp);
|
|
dimIndex.remove(ck);
|
|
Set<Long> pending = BlockIndexManager.getOrCreatePendingSet(level);
|
|
pending.remove(ck);
|
|
}
|
|
|
|
public static List<BlockPos> getNearby(ServerLevel level, BlockPos center, int radius, BlockType type) {
|
|
ArrayList<BlockPos> result = new ArrayList<BlockPos>();
|
|
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<Long, Set<Long>> 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<Long> 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<Long> pending = BlockIndexManager.getOrCreatePendingSet(level);
|
|
if (pending.add(ck)) {
|
|
chunkScanQueue.add(new ScanTask((ResourceKey<Level>)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<Long> 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<ResourceKey<Level>, ConcurrentHashMap<Long, Set<Long>>>();
|
|
chunkScanQueue = new ConcurrentLinkedQueue();
|
|
pendingChunkKeys = new ConcurrentHashMap<ResourceKey<Level>, Set<Long>>();
|
|
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<String>(DEFAULT_REINFORCED_GLASS_BLACKLIST);
|
|
GLASS_BLACKLIST_LOOKUP = new AtomicReference<HashSet<String>>(new HashSet<String>(DEFAULT_REINFORCED_GLASS_BLACKLIST));
|
|
BlockIndexManager.loadGlassBlacklistFromJson();
|
|
}
|
|
|
|
private record ScanTask(ResourceKey<Level> 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<RegionTask> 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<Path> 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<Long> found = new HashSet<Long>();
|
|
for (int i = 0; i < sections.size(); ++i) {
|
|
CompoundTag section = sections.m_128728_(i);
|
|
this.processSection(section, chunkPos, level, found);
|
|
}
|
|
if (!found.isEmpty()) {
|
|
ConcurrentHashMap<Long, Set<Long>> 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<Long> 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<BlockState, Byte> stateTypeCache = new IdentityHashMap<BlockState, Byte>();
|
|
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) {
|
|
}
|
|
}
|