Plex-FAWE/worldedit-core/src/main/java/com/sk89q/worldedit/internal/anvil/ChunkDeleter.java

363 lines
14 KiB
Java

/*
* WorldEdit, a Minecraft world manipulation toolkit
* Copyright (C) sk89q <http://www.sk89q.com>
* Copyright (C) WorldEdit team and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.sk89q.worldedit.internal.anvil;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonIOException;
import com.google.gson.JsonSyntaxException;
import com.google.gson.TypeAdapter;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import com.sk89q.worldedit.internal.util.LogManagerCompat;
import com.sk89q.worldedit.math.BlockVector2;
import org.apache.logging.log4j.Logger;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiPredicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public final class ChunkDeleter {
public static final String DELCHUNKS_FILE_NAME = "delete_chunks.json";
private static final Logger LOGGER = LogManagerCompat.getLogger();;
private static final Comparator<BlockVector2> chunkSorter = Comparator.comparing(
pos -> (pos.getBlockX() & 31) + (pos.getBlockZ() & 31) * 32
);
private static final Gson chunkDeleterGson = new GsonBuilder()
.registerTypeAdapter(BlockVector2.class, new BlockVector2Adapter())
.setPrettyPrinting()
.create();
public static ChunkDeletionInfo readInfo(Path chunkFile) throws IOException, JsonSyntaxException {
String json = new String(Files.readAllBytes(chunkFile), StandardCharsets.UTF_8);
return chunkDeleterGson.fromJson(json, ChunkDeletionInfo.class);
}
public static void writeInfo(ChunkDeletionInfo info, Path chunkFile) throws IOException, JsonIOException {
String json = chunkDeleterGson.toJson(info, new TypeToken<ChunkDeletionInfo>() {}.getType());
try (BufferedWriter writer = Files.newBufferedWriter(chunkFile)) {
writer.write(json);
}
}
public static void runFromFile(Path chunkFile, boolean deleteOnSuccess) {
ChunkDeleter chunkDeleter;
try {
chunkDeleter = createFromFile(chunkFile);
} catch (JsonSyntaxException | IOException e) {
LOGGER.error("Could not parse chunk deletion file. Invalid file?", e);
return;
}
LOGGER.info("Found chunk deletions. Proceeding with deletion...");
long start = System.currentTimeMillis();
if (chunkDeleter.runDeleter()) {
LOGGER.info("Successfully deleted {} matching chunks (out of {}, taking {} ms).",
chunkDeleter.getDeletedChunkCount(), chunkDeleter.getDeletionsRequested(),
System.currentTimeMillis() - start);
if (deleteOnSuccess) {
boolean deletedFile = false;
try {
deletedFile = Files.deleteIfExists(chunkFile);
} catch (IOException ignored) {
}
if (!deletedFile) {
LOGGER.warn("Chunk deletion file could not be cleaned up. This may have unintended consequences"
+ " on next startup, or if /delchunks is used again.");
}
}
} else {
LOGGER.error("Error occurred while deleting chunks. "
+ "If world errors occur, stop the server and restore the *.bak backup files.");
}
}
private ChunkDeleter(ChunkDeletionInfo chunkDeletionInfo) {
this.chunkDeletionInfo = chunkDeletionInfo;
}
private static ChunkDeleter createFromFile(Path chunkFile) throws IOException {
ChunkDeletionInfo info = readInfo(chunkFile);
if (info == null) {
throw new IOException("Read null json. Empty file?");
}
return new ChunkDeleter(info);
}
private final ChunkDeletionInfo chunkDeletionInfo;
private final Set<Path> backedUpRegions = new HashSet<>();
private boolean shouldPreload;
private int debugRate = 100;
private int totalChunksDeleted = 0;
private int deletionsRequested = 0;
private boolean runDeleter() {
return chunkDeletionInfo.batches.stream().allMatch(this::runBatch);
}
private boolean runBatch(ChunkDeletionInfo.ChunkBatch chunkBatch) {
int chunkCount = chunkBatch.getChunkCount();
LOGGER.debug("Processing deletion batch with {} chunks.", chunkCount);
final Map<Path, Stream<BlockVector2>> regionToChunkList = groupChunks(chunkBatch);
BiPredicate<RegionAccess, BlockVector2> predicate = createPredicates(chunkBatch.deletionPredicates);
shouldPreload = chunkBatch.chunks == null;
deletionsRequested += chunkCount;
debugRate = chunkCount / 10;
return regionToChunkList.entrySet().stream().allMatch(entry -> {
Path regionPath = entry.getKey();
if (!Files.exists(regionPath)) {
return true;
}
if (chunkBatch.backup && !backedUpRegions.contains(regionPath)) {
try {
backupRegion(regionPath);
} catch (IOException e) {
LOGGER.warn("Error backing up region file: " + regionPath + ". Aborting the process.", e);
return false;
}
}
return deleteChunks(regionPath, entry.getValue(), predicate);
});
}
private Map<Path, Stream<BlockVector2>> groupChunks(ChunkDeletionInfo.ChunkBatch chunkBatch) {
Path worldPath = Paths.get(chunkBatch.worldPath);
if (chunkBatch.chunks != null) {
return chunkBatch.chunks.stream()
.collect(Collectors.groupingBy(RegionFilePos::new))
.entrySet().stream().collect(Collectors.toMap(
e -> worldPath.resolve("region").resolve(e.getKey().getFileName()),
e -> e.getValue().stream().sorted(chunkSorter)));
} else {
final BlockVector2 minChunk = chunkBatch.minChunk;
final BlockVector2 maxChunk = chunkBatch.maxChunk;
final RegionFilePos minRegion = new RegionFilePos(minChunk);
final RegionFilePos maxRegion = new RegionFilePos(maxChunk);
Map<Path, Stream<BlockVector2>> groupedChunks = new HashMap<>();
for (int regX = minRegion.getX(); regX <= maxRegion.getX(); regX++) {
for (int regZ = minRegion.getZ(); regZ <= maxRegion.getZ(); regZ++) {
final Path regionPath = worldPath.resolve("region").resolve(new RegionFilePos(regX, regZ).getFileName());
if (!Files.exists(regionPath)) {
continue;
}
int startX = regX << 5;
int endX = (regX << 5) + 31;
int startZ = regZ << 5;
int endZ = (regZ << 5) + 31;
int minX = Math.max(Math.min(startX, endX), minChunk.getBlockX());
int minZ = Math.max(Math.min(startZ, endZ), minChunk.getBlockZ());
int maxX = Math.min(Math.max(startX, endX), maxChunk.getBlockX());
int maxZ = Math.min(Math.max(startZ, endZ), maxChunk.getBlockZ());
Stream<BlockVector2> stream = Stream.iterate(BlockVector2.at(minX, minZ),
bv2 -> {
int nextX = bv2.getBlockX();
int nextZ = bv2.getBlockZ();
if (++nextX > maxX) {
nextX = minX;
if (++nextZ > maxZ) {
return null;
}
}
return BlockVector2.at(nextX, nextZ);
});
groupedChunks.put(regionPath, stream);
}
}
return groupedChunks;
}
}
private BiPredicate<RegionAccess, BlockVector2> createPredicates(List<ChunkDeletionInfo.DeletionPredicate> deletionPredicates) {
if (deletionPredicates == null) {
return (r, p) -> true;
}
return deletionPredicates.stream()
.map(this::createPredicate)
.reduce(BiPredicate::and)
.orElse((r, p) -> true);
}
private BiPredicate<RegionAccess, BlockVector2> createPredicate(ChunkDeletionInfo.DeletionPredicate deletionPredicate) {
if ("modification".equals(deletionPredicate.property)) {
int time;
try {
time = Integer.parseInt(deletionPredicate.value);
} catch (NumberFormatException e) {
throw new IllegalStateException("Modification time predicate specified invalid time: " + deletionPredicate.value);
}
switch (deletionPredicate.comparison) {
case "<":
return (r, p) -> {
try {
return r.getModificationTime(p) < time;
} catch (IOException e) {
return false;
}
};
case ">":
return (r, p) -> {
try {
return r.getModificationTime(p) > time;
} catch (IOException e) {
return false;
}
};
default:
throw new IllegalStateException("Unexpected comparison value: " + deletionPredicate.comparison);
}
}
throw new IllegalStateException("Unexpected property value: " + deletionPredicate.property);
}
private void backupRegion(Path regionFile) throws IOException {
Path backupFile = regionFile.resolveSibling(regionFile.getFileName() + ".bak");
Files.copy(regionFile, backupFile, StandardCopyOption.REPLACE_EXISTING);
backedUpRegions.add(backupFile);
}
private boolean deleteChunks(Path regionFile, Stream<BlockVector2> chunks,
BiPredicate<RegionAccess, BlockVector2> deletionPredicate) {
try (RegionAccess region = new RegionAccess(regionFile, shouldPreload)) {
for (Iterator<BlockVector2> iterator = chunks.iterator(); iterator.hasNext();) {
BlockVector2 chunk = iterator.next();
if (chunk == null) {
break;
}
if (deletionPredicate.test(region, chunk)) {
region.deleteChunk(chunk);
totalChunksDeleted++;
if (debugRate != 0 && totalChunksDeleted % debugRate == 0) {
LOGGER.debug("Deleted {} chunks so far.", totalChunksDeleted);
}
} else {
LOGGER.debug("Chunk did not match predicates: " + chunk);
}
}
return true;
} catch (IOException e) {
LOGGER.warn("Error deleting chunks from region: " + regionFile + ". Aborting the process.", e);
return false;
}
}
public int getDeletedChunkCount() {
return totalChunksDeleted;
}
public int getDeletionsRequested() {
return deletionsRequested;
}
private static class BlockVector2Adapter extends TypeAdapter<BlockVector2> {
@Override
public void write(JsonWriter out, BlockVector2 value) throws IOException {
out.beginArray();
out.value(value.getBlockX());
out.value(value.getBlockZ());
out.endArray();
}
@Override
public BlockVector2 read(JsonReader in) throws IOException {
in.beginArray();
int x = in.nextInt();
int z = in.nextInt();
in.endArray();
return BlockVector2.at(x, z);
}
}
private static class RegionFilePos {
private final int x;
private final int z;
RegionFilePos(BlockVector2 chunk) {
this.x = chunk.getBlockX() >> 5;
this.z = chunk.getBlockZ() >> 5;
}
RegionFilePos(int regX, int regZ) {
this.x = regX;
this.z = regZ;
}
public int getX() {
return x;
}
public int getZ() {
return z;
}
public String getFileName() {
return "r." + x + "." + z + ".mca";
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
RegionFilePos that = (RegionFilePos) o;
if (x != that.x) {
return false;
}
return z == that.z;
}
@Override
public int hashCode() {
int result = x;
result = 31 * result + z;
return result;
}
@Override
public String toString() {
return "(" + x + ", " + z + ")";
}
}
}