Re-add delchunks command (#481)

The new command now writes a json file to WorldEdit's working directory with instructions on which chunks to delete, which is read by the plugin/mod at startup and calls the ChunkDeleter.
The chunk deleter parses the json and iterates the instructions, backing up .mca files as it goes and overwriting the offset headers with 0 wherever a chunk needs to be deleted.
This allows Minecraft to reclaim the space used for that chunk, as well as forcing it to be generated from scratch next time the area is loaded.
This commit is contained in:
wizjany 2019-06-22 14:20:14 -04:00 committed by GitHub
parent 902754ce8a
commit d763ab374c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 650 additions and 104 deletions

View File

@ -53,6 +53,7 @@ import org.slf4j.Logger;
import javax.annotation.Nullable;
import java.lang.ref.WeakReference;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.HashMap;
@ -158,6 +159,11 @@ public class BukkitWorld extends AbstractWorld {
return getWorld().getName();
}
@Override
public Path getStoragePath() {
return getWorld().getWorldFolder().toPath();
}
@Override
public int getBlockLightLevel(BlockVector3 pt) {
return getWorld().getBlockAt(pt.getBlockX(), pt.getBlockY(), pt.getBlockZ()).getLightLevel();

View File

@ -38,6 +38,7 @@ import com.sk89q.worldedit.extension.platform.Capability;
import com.sk89q.worldedit.extension.platform.Platform;
import com.sk89q.worldedit.extent.inventory.BlockBag;
import com.sk89q.worldedit.internal.command.CommandUtil;
import com.sk89q.worldedit.internal.anvil.ChunkDeleter;
import com.sk89q.worldedit.registry.state.Property;
import com.sk89q.worldedit.world.biome.BiomeType;
import com.sk89q.worldedit.world.block.BlockCategory;
@ -73,6 +74,9 @@ import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@ -82,6 +86,7 @@ import java.util.logging.Level;
import java.util.zip.ZipEntry;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.sk89q.worldedit.internal.anvil.ChunkDeleter.DELCHUNKS_FILE_NAME;
/**
* Plugin for Bukkit.
@ -109,6 +114,11 @@ public class WorldEditPlugin extends JavaPlugin implements TabCompleter {
// Setup platform
server = new BukkitServerInterface(this, getServer());
worldEdit.getPlatformManager().register(server);
Path delChunks = Paths.get(getDataFolder().getPath(), DELCHUNKS_FILE_NAME);
if (Files.exists(delChunks)) {
ChunkDeleter.runFromFile(delChunks, true);
}
}
/**

View File

@ -156,9 +156,6 @@ public class BukkitImplLoader {
if (cls.isSynthetic()) continue;
if (BukkitImplAdapter.class.isAssignableFrom(cls)) {
return (BukkitImplAdapter) cls.newInstance();
} else {
log.debug("Failed to load the Bukkit adapter class '" + className +
"' because it does not implement " + BukkitImplAdapter.class.getCanonicalName());
}
} catch (ClassNotFoundException e) {
log.warn("Failed to load the Bukkit adapter class '" + className +

View File

@ -19,10 +19,7 @@
package com.sk89q.worldedit.command;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.sk89q.worldedit.command.util.Logging.LogMode.REGION;
import com.sk89q.worldedit.LocalConfiguration;
import com.google.gson.JsonIOException;
import com.sk89q.worldedit.LocalSession;
import com.sk89q.worldedit.WorldEdit;
import com.sk89q.worldedit.WorldEditException;
@ -30,22 +27,36 @@ import com.sk89q.worldedit.command.util.CommandPermissions;
import com.sk89q.worldedit.command.util.CommandPermissionsConditionGenerator;
import com.sk89q.worldedit.command.util.Logging;
import com.sk89q.worldedit.entity.Player;
import com.sk89q.worldedit.internal.anvil.ChunkDeleter;
import com.sk89q.worldedit.internal.anvil.ChunkDeletionInfo;
import com.sk89q.worldedit.math.BlockVector2;
import com.sk89q.worldedit.math.MathUtils;
import com.sk89q.worldedit.regions.CuboidRegion;
import com.sk89q.worldedit.regions.Region;
import com.sk89q.worldedit.util.Location;
import com.sk89q.worldedit.util.formatting.component.PaginationBox;
import com.sk89q.worldedit.util.formatting.text.TextComponent;
import com.sk89q.worldedit.util.formatting.text.event.ClickEvent;
import com.sk89q.worldedit.util.formatting.text.format.TextColor;
import com.sk89q.worldedit.world.storage.LegacyChunkStore;
import com.sk89q.worldedit.world.storage.McRegionChunkStore;
import org.enginehub.piston.annotation.Command;
import org.enginehub.piston.annotation.CommandContainer;
import org.enginehub.piston.annotation.param.ArgFlag;
import org.enginehub.piston.exception.StopExecutionException;
import java.io.FileOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Set;
import java.util.stream.Collectors;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.sk89q.worldedit.command.util.Logging.LogMode.REGION;
import static com.sk89q.worldedit.internal.anvil.ChunkDeleter.DELCHUNKS_FILE_NAME;
/**
* Commands for working with chunks.
*/
@ -69,15 +80,10 @@ public class ChunkCommands {
int chunkX = (int) Math.floor(pos.getBlockX() / 16.0);
int chunkZ = (int) Math.floor(pos.getBlockZ() / 16.0);
String folder1 = Integer.toString(MathUtils.divisorMod(chunkX, 64), 36);
String folder2 = Integer.toString(MathUtils.divisorMod(chunkZ, 64), 36);
String filename = "c." + Integer.toString(chunkX, 36)
+ "." + Integer.toString(chunkZ, 36) + ".dat";
final BlockVector2 chunkPos = BlockVector2.at(chunkX, chunkZ);
player.print("Chunk: " + chunkX + ", " + chunkZ);
player.print("Old format: " + folder1 + "/" + folder2 + "/" + filename);
player.print("McRegion: region/" + McRegionChunkStore.getFilename(
BlockVector2.at(chunkX, chunkZ)));
player.print("Old format: " + LegacyChunkStore.getFilename(chunkPos));
player.print("McRegion: region/" + McRegionChunkStore.getFilename(chunkPos));
}
@Command(
@ -86,7 +92,7 @@ public class ChunkCommands {
)
@CommandPermissions("worldedit.listchunks")
public void listChunks(Player player, LocalSession session,
@ArgFlag(name = 'p', desc = "Page number.", def = "1") int page) throws WorldEditException {
@ArgFlag(name = 'p', desc = "Page number.", def = "1") int page) throws WorldEditException {
Set<BlockVector2> chunks = session.getSelection(player.getWorld()).getChunks();
PaginationBox paginationBox = PaginationBox.fromStrings("Selected Chunks", "/listchunks -p %page%",
@ -100,82 +106,64 @@ public class ChunkCommands {
)
@CommandPermissions("worldedit.delchunks")
@Logging(REGION)
public void deleteChunks(Player player, LocalSession session) throws WorldEditException {
player.print("Note that this command does not yet support the mcregion format.");
LocalConfiguration config = worldEdit.getConfiguration();
Set<BlockVector2> chunks = session.getSelection(player.getWorld()).getChunks();
FileOutputStream out = null;
if (config.shellSaveType == null) {
player.printError("Shell script type must be configured: 'bat' or 'bash' expected.");
} else if (config.shellSaveType.equalsIgnoreCase("bat")) {
try {
out = new FileOutputStream("worldedit-delchunks.bat");
OutputStreamWriter writer = new OutputStreamWriter(out, "UTF-8");
writer.write("@ECHO off\r\n");
writer.write("ECHO This batch file was generated by WorldEdit.\r\n");
writer.write("ECHO It contains a list of chunks that were in the selected region\r\n");
writer.write("ECHO at the time that the /delchunks command was used. Run this file\r\n");
writer.write("ECHO in order to delete the chunk files listed in this file.\r\n");
writer.write("ECHO.\r\n");
writer.write("PAUSE\r\n");
for (BlockVector2 chunk : chunks) {
String filename = LegacyChunkStore.getFilename(chunk);
writer.write("ECHO " + filename + "\r\n");
writer.write("DEL \"world/" + filename + "\"\r\n");
}
writer.write("ECHO Complete.\r\n");
writer.write("PAUSE\r\n");
writer.close();
player.print("worldedit-delchunks.bat written. Run it when no one is near the region.");
} catch (IOException e) {
player.printError("Error occurred: " + e.getMessage());
} finally {
if (out != null) {
try {
out.close();
} catch (IOException ignored) { }
}
}
} else if (config.shellSaveType.equalsIgnoreCase("bash")) {
try {
out = new FileOutputStream("worldedit-delchunks.sh");
OutputStreamWriter writer = new OutputStreamWriter(out, "UTF-8");
writer.write("#!/bin/bash\n");
writer.write("echo This shell file was generated by WorldEdit.\n");
writer.write("echo It contains a list of chunks that were in the selected region\n");
writer.write("echo at the time that the /delchunks command was used. Run this file\n");
writer.write("echo in order to delete the chunk files listed in this file.\n");
writer.write("echo\n");
writer.write("read -p \"Press any key to continue...\"\n");
for (BlockVector2 chunk : chunks) {
String filename = LegacyChunkStore.getFilename(chunk);
writer.write("echo " + filename + "\n");
writer.write("rm \"world/" + filename + "\"\n");
}
writer.write("echo Complete.\n");
writer.write("read -p \"Press any key to continue...\"\n");
writer.close();
player.print("worldedit-delchunks.sh written. Run it when no one is near the region.");
player.print("You will have to chmod it to be executable.");
} catch (IOException e) {
player.printError("Error occurred: " + e.getMessage());
} finally {
if (out != null) {
try {
out.close();
} catch (IOException ignored) {
}
}
}
} else {
player.printError("Shell script type must be configured: 'bat' or 'bash' expected.");
public void deleteChunks(Player player, LocalSession session,
@ArgFlag(name = 'o', desc = "Only delete chunks older than the specified time.", def = "")
ZonedDateTime beforeTime) throws WorldEditException {
Path worldDir = player.getWorld().getStoragePath();
if (worldDir == null) {
throw new StopExecutionException(TextComponent.of("Couldn't find world folder for this world."));
}
File chunkFile = worldEdit.getWorkingDirectoryFile(DELCHUNKS_FILE_NAME);
Path chunkPath = chunkFile.toPath();
ChunkDeletionInfo currentInfo = null;
if (Files.exists(chunkPath)) {
try {
currentInfo = ChunkDeleter.readInfo(chunkFile.toPath());
} catch (IOException e) {
throw new StopExecutionException(TextComponent.of("Error reading existing chunk file."));
}
}
if (currentInfo == null) {
currentInfo = new ChunkDeletionInfo();
currentInfo.batches = new ArrayList<>();
}
ChunkDeletionInfo.ChunkBatch newBatch = new ChunkDeletionInfo.ChunkBatch();
newBatch.worldPath = worldDir.toAbsolutePath().normalize().toString();
newBatch.backup = true;
final Region selection = session.getSelection(player.getWorld());
int chunkCount;
if (selection instanceof CuboidRegion) {
newBatch.minChunk = BlockVector2.at(selection.getMinimumPoint().getBlockX() >> 4, selection.getMinimumPoint().getBlockZ() >> 4);
newBatch.maxChunk = BlockVector2.at(selection.getMaximumPoint().getBlockX() >> 4, selection.getMaximumPoint().getBlockZ() >> 4);
final BlockVector2 dist = newBatch.maxChunk.subtract(newBatch.minChunk).add(1, 1);
chunkCount = dist.getBlockX() * dist.getBlockZ();
} else {
// this has a possibility to OOM for very large selections still
Set<BlockVector2> chunks = selection.getChunks();
newBatch.chunks = new ArrayList<>(chunks);
chunkCount = chunks.size();
}
if (beforeTime != null) {
newBatch.deletionPredicates = new ArrayList<>();
ChunkDeletionInfo.DeletionPredicate timePred = new ChunkDeletionInfo.DeletionPredicate();
timePred.property = "modification";
timePred.comparison = "<";
timePred.value = String.valueOf((int) beforeTime.toOffsetDateTime().toEpochSecond());
newBatch.deletionPredicates.add(timePred);
}
currentInfo.batches.add(newBatch);
try {
ChunkDeleter.writeInfo(currentInfo, chunkPath);
} catch (IOException | JsonIOException e) {
throw new StopExecutionException(TextComponent.of("Failed to write chunk list: " + e.getMessage()));
}
player.print(String.format("%d chunk(s) have been marked for deletion and will be deleted the next time the server starts.", chunkCount));
player.print(TextComponent.of("You can mark more chunks for deletion, or to stop the server now, run: ", TextColor.LIGHT_PURPLE)
.append(TextComponent.of("/stop", TextColor.AQUA).clickEvent(ClickEvent.of(ClickEvent.Action.SUGGEST_COMMAND, "/stop"))));
}
}

View File

@ -0,0 +1,350 @@
/*
* 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 Lesser 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 Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://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.math.BlockVector2;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.nio.file.StandardOpenOption;
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 = LoggerFactory.getLogger(ChunkDeleter.class);
private static final Comparator<BlockVector2> chunkSorter = Comparator.comparing(
pos -> (pos.getBlockX() & 31) + (pos.getBlockZ() & 31) * 32);
private static 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, StandardOpenOption.CREATE)) {
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 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) {
logger.debug("Processing deletion batch.");
final Map<Path, Stream<BlockVector2>> regionToChunkList = groupChunks(chunkBatch);
BiPredicate<RegionAccess, BlockVector2> predicate = createPredicates(chunkBatch.deletionPredicates);
shouldPreload = chunkBatch.chunks == null;
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) {
deletionsRequested += chunkBatch.chunks.size();
debugRate = chunkBatch.chunks.size() / 10;
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);
}
}
final BlockVector2 dist = maxChunk.subtract(minChunk).add(1, 1);
final int batchSize = dist.getBlockX() * dist.getBlockZ();
debugRate = batchSize / 10;
this.deletionsRequested += batchSize;
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 (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 + ")";
}
}
}

View File

@ -0,0 +1,48 @@
/*
* 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 Lesser 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 Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.sk89q.worldedit.internal.anvil;
import com.sk89q.worldedit.math.BlockVector2;
import java.util.List;
/**
* Internal class. Subject to changes.
*/
public class ChunkDeletionInfo {
public List<ChunkBatch> batches;
public static class ChunkBatch {
public String worldPath;
public boolean backup;
public List<DeletionPredicate> deletionPredicates;
// specify either list of chunks, or min-max
public List<BlockVector2> chunks;
public BlockVector2 minChunk;
public BlockVector2 maxChunk;
}
public static class DeletionPredicate {
public String property;
public String comparison;
public String value;
}
}

View File

@ -0,0 +1,101 @@
/*
* 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 Lesser 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 Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.sk89q.worldedit.internal.anvil;
import com.sk89q.worldedit.math.BlockVector2;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.file.Path;
/**
* Internal class. Subject to changes.
*/
class RegionAccess implements AutoCloseable {
private RandomAccessFile raf;
private int[] offsets;
private int[] timestamps;
RegionAccess(Path file) throws IOException {
this(file, false);
}
RegionAccess(Path file, boolean preload) throws IOException {
raf = new RandomAccessFile(file.toFile(), "rw");
if (preload) {
readHeaders();
}
}
private void readHeaders() throws IOException {
offsets = new int[1024];
timestamps = new int[1024];
for (int idx = 0; idx < 1024; ++idx) {
offsets[idx] = raf.readInt();
}
for (int idx = 0; idx < 1024; ++idx) {
timestamps[idx] = raf.readInt();
}
}
private static int indexChunk(BlockVector2 pos) {
int x = pos.getBlockX() & 31;
int z = pos.getBlockZ() & 31;
return x + z * 32;
}
int getModificationTime(BlockVector2 pos) throws IOException {
int idx = indexChunk(pos);
if (timestamps != null) {
return timestamps[idx];
}
raf.seek(idx * 4L + 4096);
return raf.readInt();
}
int getChunkSize(BlockVector2 pos) throws IOException {
int idx = indexChunk(pos);
if (offsets != null) {
return offsets[idx] & 0xFF;
}
raf.seek(idx * 4L);
// 3 bytes for offset
raf.read();
raf.read();
raf.read();
// one byte for size - note, yes, could do raf.readInt() & 0xFF but that does extra checks
return raf.read();
}
void deleteChunk(BlockVector2 pos) throws IOException {
int idx = indexChunk(pos);
raf.seek(idx * 4L);
raf.writeInt(0);
if (offsets != null) {
offsets[idx] = 0;
}
}
@Override
public void close() throws IOException {
raf.close();
}
}

View File

@ -34,6 +34,7 @@ import com.sk89q.worldedit.world.block.BlockStateHolder;
import com.sk89q.worldedit.world.block.BlockType;
import com.sk89q.worldedit.world.block.BlockTypes;
import java.nio.file.Path;
import java.util.PriorityQueue;
import javax.annotation.Nullable;
@ -56,6 +57,11 @@ public abstract class AbstractWorld implements World {
return setBlock(pt, block, true);
}
@Override
public Path getStoragePath() {
return null;
}
@Override
public int getMaxY() {
return getMaximumPoint().getBlockY();

View File

@ -38,6 +38,9 @@ import com.sk89q.worldedit.world.block.BlockStateHolder;
import com.sk89q.worldedit.world.block.BlockType;
import com.sk89q.worldedit.world.weather.WeatherType;
import javax.annotation.Nullable;
import java.nio.file.Path;
/**
* Represents a world (dimension).
*/
@ -50,6 +53,15 @@ public interface World extends Extent {
*/
String getName();
/**
* Get the folder in which this world is stored. May return null if unknown
* or if this world is not serialized to disk.
*
* @return world storage path
*/
@Nullable
Path getStoragePath();
/**
* Get the maximum Y.
*

View File

@ -100,10 +100,9 @@ public class McRegionReader {
/**
* Read the header.
*
* @throws DataException
* @throws IOException
*/
private void readHeader() throws DataException, IOException {
private void readHeader() throws IOException {
offsets = new int[SECTOR_INTS];
for (int i = 0; i < SECTOR_INTS; ++i) {
@ -124,10 +123,6 @@ public class McRegionReader {
int x = position.getBlockX() & 31;
int z = position.getBlockZ() & 31;
if (x < 0 || x >= 32 || z < 0 || z >= 32) {
throw new DataException("MCRegion file does not contain " + x + "," + z);
}
int offset = getOffset(x, z);
// The chunk hasn't been generated
@ -138,7 +133,7 @@ public class McRegionReader {
int sectorNumber = offset >> 8;
int numSectors = offset & 0xFF;
stream.seek(sectorNumber * SECTOR_BYTES);
stream.seek((long) sectorNumber * SECTOR_BYTES);
int length = dataStream.readInt();
if (length > SECTOR_BYTES * numSectors) {

View File

@ -97,6 +97,7 @@ import net.minecraft.world.storage.WorldInfo;
import javax.annotation.Nullable;
import java.io.File;
import java.lang.ref.WeakReference;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
@ -167,6 +168,15 @@ public class ForgeWorld extends AbstractWorld {
return getWorld().getWorldInfo().getWorldName();
}
@Override
public Path getStoragePath() {
final World world = getWorld();
if (world instanceof ServerWorld) {
return ((ServerWorld) world).getSaveHandler().getWorldDirectory().toPath();
}
return null;
}
@Override
public <B extends BlockStateHolder<B>> boolean setBlock(BlockVector3 position, B block, boolean notifyAndLight) throws WorldEditException {
checkNotNull(position);

View File

@ -31,6 +31,7 @@ import com.sk89q.worldedit.forge.net.packet.LeftClickAirEventMessage;
import com.sk89q.worldedit.forge.proxy.ClientProxy;
import com.sk89q.worldedit.forge.proxy.CommonProxy;
import com.sk89q.worldedit.forge.proxy.ServerProxy;
import com.sk89q.worldedit.internal.anvil.ChunkDeleter;
import com.sk89q.worldedit.util.Location;
import com.sk89q.worldedit.world.biome.BiomeType;
import com.sk89q.worldedit.world.block.BlockCategory;
@ -45,7 +46,6 @@ import net.minecraft.tags.ItemTags;
import net.minecraft.util.Hand;
import net.minecraft.util.ResourceLocation;
import net.minecraft.world.World;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.CommandEvent;
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
@ -58,11 +58,10 @@ import net.minecraftforge.fml.ModContainer;
import net.minecraftforge.fml.ModLoadingContext;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
import net.minecraftforge.fml.event.lifecycle.FMLLoadCompleteEvent;
import net.minecraftforge.fml.event.server.FMLServerAboutToStartEvent;
import net.minecraftforge.fml.event.server.FMLServerStartedEvent;
import net.minecraftforge.fml.event.server.FMLServerStoppingEvent;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import net.minecraftforge.fml.loading.FMLLoader;
import net.minecraftforge.fml.loading.FMLPaths;
import net.minecraftforge.registries.ForgeRegistries;
import org.apache.logging.log4j.LogManager;
@ -76,6 +75,7 @@ import java.nio.file.Path;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.sk89q.worldedit.forge.ForgeAdapter.adaptPlayer;
import static com.sk89q.worldedit.internal.anvil.ChunkDeleter.DELCHUNKS_FILE_NAME;
/**
* The Forge implementation of WorldEdit.
@ -180,6 +180,14 @@ public class ForgeWorldEdit {
}
}
@SubscribeEvent
public void serverAboutToStart(FMLServerAboutToStartEvent event) {
final Path delChunks = workingDir.resolve(DELCHUNKS_FILE_NAME);
if (Files.exists(delChunks)) {
ChunkDeleter.runFromFile(delChunks, true);
}
}
@SubscribeEvent
public void serverStopping(FMLServerStoppingEvent event) {
WorldEdit worldEdit = WorldEdit.getInstance();

View File

@ -57,6 +57,7 @@ import org.spongepowered.api.world.World;
import org.spongepowered.api.world.weather.Weather;
import java.lang.ref.WeakReference;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@ -116,6 +117,11 @@ public abstract class SpongeWorld extends AbstractWorld {
return getWorld().getName();
}
@Override
public Path getStoragePath() {
return getWorld().getDirectory();
}
@SuppressWarnings("WeakerAccess")
protected BlockState getBlockState(BlockStateHolder<?> block) {
if (block instanceof com.sk89q.worldedit.world.block.BlockState) {

View File

@ -20,6 +20,7 @@
package com.sk89q.worldedit.sponge;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.sk89q.worldedit.internal.anvil.ChunkDeleter.DELCHUNKS_FILE_NAME;
import com.google.inject.Inject;
import com.sk89q.worldedit.LocalSession;
@ -29,6 +30,7 @@ import com.sk89q.worldedit.event.platform.PlatformReadyEvent;
import com.sk89q.worldedit.extension.platform.Actor;
import com.sk89q.worldedit.extension.platform.Capability;
import com.sk89q.worldedit.extension.platform.Platform;
import com.sk89q.worldedit.internal.anvil.ChunkDeleter;
import com.sk89q.worldedit.sponge.adapter.AdapterLoadException;
import com.sk89q.worldedit.sponge.adapter.SpongeImplAdapter;
import com.sk89q.worldedit.sponge.adapter.SpongeImplLoader;
@ -62,6 +64,8 @@ import org.spongepowered.api.world.World;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
@ -134,6 +138,11 @@ public class SpongeWorldEdit {
WorldEdit.getInstance().getPlatformManager().unregister(platform);
}
final Path delChunks = workingDir.toPath().resolve(DELCHUNKS_FILE_NAME);
if (Files.exists(delChunks)) {
ChunkDeleter.runFromFile(delChunks, true);
}
this.platform = new SpongePlatform(this);
this.provider = new SpongePermissionsProvider();