diff --git a/Bukkit/src/main/java/com/plotsquared/bukkit/BukkitPlatform.java b/Bukkit/src/main/java/com/plotsquared/bukkit/BukkitPlatform.java index 804d0376e2..fc16c32e36 100644 --- a/Bukkit/src/main/java/com/plotsquared/bukkit/BukkitPlatform.java +++ b/Bukkit/src/main/java/com/plotsquared/bukkit/BukkitPlatform.java @@ -145,6 +145,7 @@ import java.io.File; import java.io.IOException; import java.lang.reflect.Method; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -780,6 +781,24 @@ private void registerCommands() { return Bukkit.getWorldContainer(); } + @Override + public @NonNull Path getWorldContainer(final String namespace) { + Path root = Bukkit.getWorldContainer().toPath(); + if (MinecraftVersion.current().isOlderOrEqualThan(MinecraftVersion.TINY_TAKEOVER)) { + return root.resolve("world").resolve("dimensions").resolve(namespace); + } + return root; + } + + @Override + public @NonNull Path getWorldPath(final String namespace, final String world) { + Path root = Bukkit.getWorldContainer().toPath(); + if (MinecraftVersion.current().isOlderOrEqualThan(MinecraftVersion.TINY_TAKEOVER)) { + return root.resolve("world").resolve("dimensions").resolve(namespace).resolve(world); + } + return root.resolve(world); + } + @SuppressWarnings("deprecation") private void runEntityTask() { TaskManager.runTaskRepeat(() -> this.plotAreaManager.forEachPlotArea(plotArea -> { diff --git a/Core/src/main/java/com/plotsquared/core/PlotPlatform.java b/Core/src/main/java/com/plotsquared/core/PlotPlatform.java index ab1d825cbc..14481bb80e 100644 --- a/Core/src/main/java/com/plotsquared/core/PlotPlatform.java +++ b/Core/src/main/java/com/plotsquared/core/PlotPlatform.java @@ -50,6 +50,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; import java.io.File; +import java.nio.file.Path; /** * PlotSquared main utility class @@ -67,10 +68,44 @@ public interface PlotPlatform

extends LocaleHolder { /** * Gets the folder where all world data is stored. + * For legacy versions this is the server root, for 26.1+ this is {@code ./world} * * @return the world folder */ - @NonNull File worldContainer(); + @NonNull + File worldContainer(); + + /** + * Gets the path to the dimension directory for the given namespace, e.g. {@code ./world/dimension/}. + *

+ * Legacy versions ignore the namespace and return the default path (by default the server root). + * + * @param namespace the dimension namespace + * @return a path to the dimension directory. may not exist. + */ + @NonNull + Path getWorldContainer(String namespace); + + /** + * See {@link #getWorldPath(String, String)}. Uses the default {@code minecraft} namespace. + * + */ + @NonNull + default Path getWorldPath(String world) { + return getWorldPath("minecraft", world); + } + + /** + * Gets the path to the world directory for the given namespace, e.g. {@code ./world/dimension//}. + *

+ * Legacy versions ignore the namespace (by default {@code ./}) + * + * @param namespace the dimension namespace + * @param world the name of the world in the given dimension + * @return a path to the world directory. may not exist. + */ + @NonNull + Path getWorldPath(String namespace, String world); /** * Completely shuts down the plugin. diff --git a/Core/src/main/java/com/plotsquared/core/command/DatabaseCommand.java b/Core/src/main/java/com/plotsquared/core/command/DatabaseCommand.java index 5d24194c0d..3e7c06c3c0 100644 --- a/Core/src/main/java/com/plotsquared/core/command/DatabaseCommand.java +++ b/Core/src/main/java/com/plotsquared/core/command/DatabaseCommand.java @@ -43,9 +43,14 @@ import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.tag.Tag; import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.NonNull; import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.sql.SQLException; import java.util.ArrayList; import java.util.Arrays; @@ -61,6 +66,8 @@ usage = "/plot database [area] ") public class DatabaseCommand extends SubCommand { + private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + DatabaseCommand.class.getSimpleName()); + private final PlotAreaManager plotAreaManager; private final EventDispatcher eventDispatcher; private final PlotListener plotListener; @@ -93,7 +100,7 @@ public static void insertPlots( }); } catch (Exception e) { player.sendMessage(TranslatableCaption.of("database.conversion_failed")); - e.printStackTrace(); + LOGGER.error("Database conversion failed", e); } }); } @@ -171,18 +178,14 @@ public boolean onCommand(final PlotPlayer player, String[] args) { if (newPlot != null) { PlotId newId = newPlot.getId(); PlotId id = plot.getId(); - File worldFile = - new File( - PlotSquared.platform().worldContainer(), - id.toCommaSeparatedString() - ); - if (worldFile.exists()) { - File newFile = - new File( - PlotSquared.platform().worldContainer(), - newId.toCommaSeparatedString() - ); - worldFile.renameTo(newFile); + Path worldPath = PlotSquared.platform().getWorldPath(id.toCommaSeparatedString()); + if (Files.exists(worldPath)) { + Path newPath = PlotSquared.platform().getWorldPath(newId.toCommaSeparatedString()); + try { + Files.move(worldPath, newPath); + } catch (IOException e) { + LOGGER.error("Failed to rename world entry", e); + } } plot.setId(newId); plot.setArea(pa); @@ -257,7 +260,7 @@ public boolean onCommand(final PlotPlayer player, String[] args) { } catch (ClassNotFoundException | SQLException e) { player.sendMessage(TranslatableCaption.of("database.failed_to_save_plots")); player.sendMessage(TranslatableCaption.of("errors.stacktrace_begin")); - e.printStackTrace(); + LOGGER.error("Inserting plots failed", e); player.sendMessage(TranslatableCaption.of("errors.stacktrace_end")); player.sendMessage(TranslatableCaption.of("database.invalid_args")); return false; @@ -265,7 +268,7 @@ public boolean onCommand(final PlotPlayer player, String[] args) { } catch (ClassNotFoundException | SQLException e) { player.sendMessage(TranslatableCaption.of("database.failed_to_open")); player.sendMessage(TranslatableCaption.of("errors.stacktrace_begin")); - e.printStackTrace(); + LOGGER.error("Opening database connection failed", e); player.sendMessage(TranslatableCaption.of("errors.stacktrace_end")); player.sendMessage(TranslatableCaption.of("database.invalid_args")); return false; diff --git a/Core/src/main/java/com/plotsquared/core/command/DebugImportWorlds.java b/Core/src/main/java/com/plotsquared/core/command/DebugImportWorlds.java index 7bc10e8db4..429916efb5 100644 --- a/Core/src/main/java/com/plotsquared/core/command/DebugImportWorlds.java +++ b/Core/src/main/java/com/plotsquared/core/command/DebugImportWorlds.java @@ -21,8 +21,10 @@ import com.google.common.base.Charsets; import com.google.inject.Inject; import com.plotsquared.core.PlotSquared; +import com.plotsquared.core.configuration.caption.StaticCaption; import com.plotsquared.core.configuration.caption.TranslatableCaption; import com.plotsquared.core.player.PlotPlayer; +import com.plotsquared.core.plot.PlotArea; import com.plotsquared.core.plot.PlotId; import com.plotsquared.core.plot.world.PlotAreaManager; import com.plotsquared.core.plot.world.SinglePlotArea; @@ -30,11 +32,18 @@ import com.plotsquared.core.util.WorldUtil; import com.plotsquared.core.util.task.RunnableVal2; import com.plotsquared.core.util.task.RunnableVal3; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.NonNull; -import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Objects; import java.util.UUID; import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.stream.Stream; @CommandDeclaration(command = "debugimportworlds", permission = "plots.admin", @@ -42,6 +51,8 @@ category = CommandCategory.TELEPORT) public class DebugImportWorlds extends Command { + private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + DebugImportWorlds.class.getSimpleName()); + private final PlotAreaManager plotAreaManager; private final WorldUtil worldUtil; @@ -67,37 +78,65 @@ public CompletableFuture execute( return CompletableFuture.completedFuture(false); } SinglePlotArea area = ((SinglePlotAreaManager) this.plotAreaManager).getArea(); - PlotId id = PlotId.of(0, 0); - File container = PlotSquared.platform().worldContainer(); - if (container.equals(new File("."))) { + Path container = PlotSquared.platform().getWorldContainer("minecraft"); + if (container.equals(Path.of("."))) { player.sendMessage(TranslatableCaption.of("debugimportworlds.world_container")); return CompletableFuture.completedFuture(false); } - for (File folder : container.listFiles()) { - String name = folder.getName(); - if (!this.worldUtil.isWorld(name) && PlotId.fromStringOrNull(name) == null) { - UUID uuid; - if (name.length() > 16) { - uuid = UUID.fromString(name); - } else { - player.sendMessage(TranslatableCaption.of("players.fetching_player")); - uuid = PlotSquared.get().getImpromptuUUIDPipeline().getSingle(name, 60000L); - } - if (uuid == null) { - uuid = - UUID.nameUUIDFromBytes(("OfflinePlayer:" + name).getBytes(Charsets.UTF_8)); - } - while (new File(container, id.toCommaSeparatedString()).exists()) { - id = id.getNextId(); - } - File newDir = new File(container, id.toCommaSeparatedString()); - if (folder.renameTo(newDir)) { - area.getPlot(id).setOwner(uuid); - } - } + try (Stream stream = Files.walk(container, 1)) { + stream.map(path -> new PathWithName(path, path.getFileName().toString())) + .filter(p -> !this.worldUtil.isWorld(p.name())) + .filter(p -> PlotId.fromStringOrNull(p.name()) == null) + .forEach(new ImportAction(player, area, container)); + } catch (IOException e) { + LOGGER.error("Failed to import world", e); + throw new CommandException(StaticCaption.of("World import failed. Check console.")); } player.sendMessage(TranslatableCaption.of("players.done")); return CompletableFuture.completedFuture(true); } + private record PathWithName(Path path, String name) { + + } + + private static class ImportAction implements Consumer { + + private final PlotPlayer player; + private final PlotArea area; + private final Path container; + + private PlotId id = PlotId.of(0, 0); + + private ImportAction(final PlotPlayer player, final PlotArea area, final Path container) { + this.player = player; + this.area = area; + this.container = container; + } + + @Override + public void accept(final PathWithName p) { + UUID uuid; + if (p.name().length() > 16) { + uuid = UUID.fromString(p.name()); + } else { + this.player.sendMessage(TranslatableCaption.of("players.fetching_player")); + uuid = PlotSquared.get().getImpromptuUUIDPipeline().getSingle(p.name(), 60000L); + } + if (uuid == null) { + uuid = UUID.nameUUIDFromBytes(("OfflinePlayer:" + p.name()).getBytes(Charsets.UTF_8)); + } + Path target; + if (Files.exists(target = this.container.resolve(this.id.toCommaSeparatedString()))) { + this.id = this.id.getNextId(); + } + try { + Files.move(p.path(), target); + Objects.requireNonNull(this.area.getPlot(this.id)).setOwner(uuid); + } catch (IOException ignored) { + } + } + + } + } diff --git a/Core/src/main/java/com/plotsquared/core/plot/world/SinglePlotArea.java b/Core/src/main/java/com/plotsquared/core/plot/world/SinglePlotArea.java index d24d43d53a..227a6a5946 100644 --- a/Core/src/main/java/com/plotsquared/core/plot/world/SinglePlotArea.java +++ b/Core/src/main/java/com/plotsquared/core/plot/world/SinglePlotArea.java @@ -44,9 +44,12 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; -import java.io.File; import java.io.IOException; +import java.nio.file.FileVisitResult; import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; public class SinglePlotArea extends GridPlotWorld { @@ -135,45 +138,40 @@ public void loadWorld(final PlotId id) { .settingsNodesWrapper(new SettingsNodesWrapper(new ConfigurationNode[0], null)) .worldName(worldName); - File container = PlotSquared.platform().worldContainer(); - File destination = new File(container, worldName); + Path destination = PlotSquared.platform().getWorldPath(worldName); {// convert old - File oldFile = new File(container, id.toCommaSeparatedString()); - if (oldFile.exists()) { - oldFile.renameTo(destination); - } else { - oldFile = new File(container, id.toSeparatedString(".")); - if (oldFile.exists()) { - oldFile.renameTo(destination); + Path old = PlotSquared.platform().getWorldPath(id.toCommaSeparatedString()); + if (!Files.exists(old)) { + old = PlotSquared.platform().getWorldPath(id.toSeparatedString(".")); + } + if (Files.exists(old)) { + try { + Files.move(old, destination); + } catch (IOException e) { + throw new RuntimeException(e); } } } // Duplicate 0;0 if (builder.plotAreaType() != PlotAreaType.NORMAL) { - if (!destination.exists()) { - File src = new File(container, "0_0"); - if (src.exists()) { - if (!destination.exists()) { - destination.mkdirs(); + if (!Files.exists(destination)) { + Path src = PlotSquared.platform().getWorldPath("0_0"); + if (Files.exists(src)) { + try { + Files.createDirectories(destination); + } catch (IOException e) { + throw new RuntimeException(e); } - File levelDat = new File(src, "level.dat"); - if (levelDat.exists()) { + Path levelDat = src.resolve("level.dat"); + if (Files.exists(levelDat)) { try { - Files.copy( - levelDat.toPath(), - new File(destination, levelDat.getName()).toPath() - ); - File data = new File(src, "data"); - if (data.exists()) { - File dataDest = new File(destination, "data"); - dataDest.mkdirs(); - for (File file : data.listFiles()) { - Files.copy( - file.toPath(), - new File(dataDest, file.getName()).toPath() - ); - } + Files.copy(levelDat, destination.resolve(levelDat.getFileName())); + Path data = src.resolve("data"); + if (Files.exists(data)) { + Path dataDest = destination.resolve("data"); + Files.createDirectories(dataDest); + Files.walkFileTree(data, new RecursiveDirectoryCopyVisitor(dataDest)); } } catch (IOException exception) { exception.printStackTrace(); @@ -207,6 +205,37 @@ public void loadWorld(final PlotId id) { // return AsyncWorld.create(wc); } + private static final class RecursiveDirectoryCopyVisitor extends SimpleFileVisitor { + + private final Path target; + private Path base; + + public RecursiveDirectoryCopyVisitor(Path target) { + this.target = target; + } + + @Override + public @NonNull FileVisitResult preVisitDirectory( + @NonNull final Path dir, + @NonNull final BasicFileAttributes attrs + ) throws IOException { + if (this.base == null) { + // first iteration, root + this.base = dir; + } else { + Files.createDirectories(this.target.resolve(this.base.relativize(dir))); + } + return FileVisitResult.CONTINUE; + } + + @Override + public @NonNull FileVisitResult visitFile(@NonNull final Path file, @NonNull final BasicFileAttributes attrs) throws + IOException { + Files.copy(file, this.target.resolve(this.base.relativize(file))); + return FileVisitResult.CONTINUE; + } + + } @Override public ConfigurationNode[] getSettingNodes() { diff --git a/Core/src/main/java/com/plotsquared/core/plot/world/SinglePlotManager.java b/Core/src/main/java/com/plotsquared/core/plot/world/SinglePlotManager.java index 7c81fc2fff..6105fec10a 100644 --- a/Core/src/main/java/com/plotsquared/core/plot/world/SinglePlotManager.java +++ b/Core/src/main/java/com/plotsquared/core/plot/world/SinglePlotManager.java @@ -26,13 +26,14 @@ import com.plotsquared.core.plot.PlotId; import com.plotsquared.core.plot.PlotManager; import com.plotsquared.core.queue.QueueCoordinator; -import com.plotsquared.core.util.FileUtils; import com.plotsquared.core.util.task.TaskManager; import com.sk89q.worldedit.function.pattern.Pattern; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; -import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.List; public class SinglePlotManager extends PlotManager { @@ -71,9 +72,13 @@ public boolean clearPlot( @Nullable QueueCoordinator queue ) { PlotSquared.platform().setupUtils().unload(plot.getWorldName(), false); - final File worldFolder = new File(PlotSquared.platform().worldContainer(), plot.getWorldName()); + Path path = PlotSquared.platform().getWorldPath(plot.getWorldName()); TaskManager.getPlatformImplementation().taskAsync(() -> { - FileUtils.deleteDirectory(worldFolder); + try { + Files.deleteIfExists(path); + } catch (IOException e) { + throw new RuntimeException("Failed to delete directory", e); + } if (whenDone != null) { whenDone.run(); } diff --git a/Core/src/main/java/com/plotsquared/core/util/CloseShieldOutputStream.java b/Core/src/main/java/com/plotsquared/core/util/CloseShieldOutputStream.java new file mode 100644 index 0000000000..6a6b2dfa5a --- /dev/null +++ b/Core/src/main/java/com/plotsquared/core/util/CloseShieldOutputStream.java @@ -0,0 +1,36 @@ +/* + * PlotSquared, a land and world management plugin for Minecraft. + * Copyright (C) IntellectualSites + * Copyright (C) IntellectualSites 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 . + */ +package com.plotsquared.core.util; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +public final class CloseShieldOutputStream extends FilterOutputStream { + + public CloseShieldOutputStream(final OutputStream out) { + super(out); + } + + @Override + public void close() throws IOException { + // NOOP + } + +} diff --git a/Core/src/main/java/com/plotsquared/core/util/RegionManager.java b/Core/src/main/java/com/plotsquared/core/util/RegionManager.java index 765bdc7453..6c51ee089a 100644 --- a/Core/src/main/java/com/plotsquared/core/util/RegionManager.java +++ b/Core/src/main/java/com/plotsquared/core/util/RegionManager.java @@ -45,7 +45,9 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; -import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Collection; import java.util.Set; @@ -89,13 +91,15 @@ public static BlockVector2 getRegion(Location location) { public abstract int[] countEntities(Plot plot); public void deleteRegionFiles(final String world, final Collection chunks, final Runnable whenDone) { + Path regionRoot = PlotSquared.platform().getWorldPath(world).resolve("region"); TaskManager.runTaskAsync(() -> { for (BlockVector2 loc : chunks) { - String directory = world + File.separator + "region" + File.separator + "r." + loc.getX() + "." + loc.getZ() + ".mca"; - File file = new File(PlotSquared.platform().worldContainer(), directory); - LOGGER.info("- Deleting file: {} (max 1024 chunks)", file.getName()); - if (file.exists()) { - file.delete(); + Path path = regionRoot.resolve(String.format("r.%s.%s.mca", loc.getX(), loc.getZ())); + LOGGER.info("- Deleting file: {} (max 1024 chunks)", path.getFileName()); + try { + Files.deleteIfExists(path); + } catch (IOException e) { + throw new RuntimeException("Failed to delete region file", e); } } TaskManager.runTask(whenDone); diff --git a/Core/src/main/java/com/plotsquared/core/util/WorldUtil.java b/Core/src/main/java/com/plotsquared/core/util/WorldUtil.java index 98d0765a4e..d92432eb4c 100644 --- a/Core/src/main/java/com/plotsquared/core/util/WorldUtil.java +++ b/Core/src/main/java/com/plotsquared/core/util/WorldUtil.java @@ -25,7 +25,7 @@ import com.plotsquared.core.plot.Plot; import com.plotsquared.core.util.task.RunnableVal; import com.sk89q.jnbt.CompoundTag; -import com.sk89q.jnbt.IntTag; +import com.sk89q.jnbt.CompoundTagBuilder; import com.sk89q.jnbt.NBTInputStream; import com.sk89q.jnbt.NBTOutputStream; import com.sk89q.jnbt.Tag; @@ -37,24 +37,29 @@ import com.sk89q.worldedit.world.block.BlockType; import com.sk89q.worldedit.world.entity.EntityType; import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.checkerframework.checker.index.qual.NonNegative; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; import java.util.Collection; -import java.util.HashMap; import java.util.HashSet; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.function.Consumer; import java.util.function.IntConsumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; import java.util.zip.ZipEntry; @@ -62,6 +67,8 @@ public abstract class WorldUtil { + private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + WorldUtil.class.getSimpleName()); + /** * {@return whether the given location is valid in the world} * @param location the location to check @@ -257,40 +264,27 @@ public void upload( final @Nullable String file, final @NonNull RunnableVal whenDone ) { + boolean modern = MinecraftVersion.current().isNewerOrEqualThan(MinecraftVersion.TINY_TAKEOVER); + String relativeMcaRoot = modern ? "dimensions/minecraft/overworld/region" : "region"; plot.getHome(home -> SchematicHandler.upload(uuid, file, "zip", new RunnableVal<>() { @Override public void run(OutputStream output) { try (final ZipOutputStream zos = new ZipOutputStream(output)) { - File dat = getDat(plot.getWorldName()); + Path dat = getDat(plot.getWorldName()); Location spawn = getSpawn(plot.getWorldName()); if (dat != null) { - ZipEntry ze = new ZipEntry("world" + File.separator + dat.getName()); + ZipEntry ze = new ZipEntry("level.dat"); zos.putNextEntry(ze); - try (NBTInputStream nis = new NBTInputStream(new GZIPInputStream(new FileInputStream(dat)))) { - Map tag = ((CompoundTag) nis.readNamedTag().getTag()).getValue(); - Map newMap = new HashMap<>(); - for (Map.Entry entry : tag.entrySet()) { - if (!entry.getKey().equals("Data")) { - newMap.put(entry.getKey(), entry.getValue()); - continue; - } - Map data = new HashMap<>(((CompoundTag) entry.getValue()).getValue()); - data.put("SpawnX", new IntTag(home.getX())); - data.put("SpawnY", new IntTag(home.getY())); - data.put("SpawnZ", new IntTag(home.getZ())); - newMap.put("Data", new CompoundTag(data)); - } - try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { - try (NBTOutputStream out = new NBTOutputStream(new GZIPOutputStream(baos, true))) { - //TODO Find what this should be called - out.writeNamedTag("Schematic????", new CompoundTag(newMap)); - } - zos.write(baos.toByteArray()); + try (NBTInputStream nis = new NBTInputStream(new GZIPInputStream(Files.newInputStream(dat)))) { + CompoundTag levelData = modifyLevelData((CompoundTag) nis.readNamedTag().getTag(), home); + try (NBTOutputStream out = + new NBTOutputStream(new GZIPOutputStream(new CloseShieldOutputStream(zos), true))) { + out.writeNamedTag("", levelData); } } + zos.closeEntry(); } setSpawn(spawn); - byte[] buffer = new byte[1024]; Set added = new HashSet<>(); for (Plot current : plot.getConnectedPlots()) { Location bot = current.getBottomAbs(); @@ -303,18 +297,13 @@ public void run(OutputStream output) { for (BlockVector2 mca : files) { if (mca.getX() >= brx && mca.getX() <= trx && mca.getZ() >= brz && mca.getZ() <= trz && !added.contains( mca)) { - final File file = getMcr(plot.getWorldName(), mca.getX(), mca.getZ()); - if (file != null) { - //final String name = "r." + (x - cx) + "." + (z - cz) + ".mca"; - String name = file.getName(); - final ZipEntry ze = new ZipEntry("world" + File.separator + "region" + File.separator + name); + final Path path = getMca(plot.getWorldName(), mca.getX(), mca.getZ()); + if (path != null) { + final ZipEntry ze = new ZipEntry(relativeMcaRoot + "/" + path.getFileName().toString()); zos.putNextEntry(ze); added.add(mca); - try (FileInputStream in = new FileInputStream(file)) { - int len; - while ((len = in.read(buffer)) > 0) { - zos.write(buffer, 0, len); - } + try (InputStream in = Files.newInputStream(path)) { + in.transferTo(zos); } zos.closeEntry(); } @@ -331,49 +320,95 @@ public void run(OutputStream output) { }, whenDone)); } - final @Nullable File getDat(final @NonNull String world) { - File file = new File(PlotSquared.platform().worldContainer() + File.separator + world + File.separator + "level.dat"); - if (file.exists()) { - return file; + private @Nullable Path getDat(final @NonNull String world) { + Path path; + if (MinecraftVersion.current().isOlderOrEqualThan(MinecraftVersion.TINY_TAKEOVER)) { + // 26.1+ only has a global level.dat + path = PlotSquared.platform().worldContainer().toPath().resolve("world").resolve("level.dat"); + } else { + path = PlotSquared.platform().worldContainer().toPath().resolve(world).resolve("level.dat"); } - return null; + return Files.exists(path) ? path : null; } @Nullable - private File getMcr(final @NonNull String world, final int x, final int z) { - final File file = - new File( - PlotSquared.platform().worldContainer(), - world + File.separator + "region" + File.separator + "r." + x + '.' + z + ".mca" + private Path getMca(final @NonNull String world, final int x, final int z) { + Path path = PlotSquared.platform().getWorldPath(world).resolve("region"). + resolve(String.format("r.%s.%s.mca", x, z)); + return Files.exists(path) ? path : null; + } + + private CompoundTag modifyLevelData(CompoundTag input, Location home) { + Map root = input.getValue(); + if (!(root.get("Data") instanceof CompoundTag data)) { + return input; + } + CompoundTagBuilder dataBuilder = data.createBuilder(); + if (MinecraftVersion.current().isNewerOrEqualThan(MinecraftVersion.TINY_TAKEOVER)) { + if (data.getValue().get("spawn") instanceof CompoundTag spawn) { + dataBuilder.put( + "spawn", spawn.createBuilder() + .putString("dimension", "minecraft:overworld") + .putIntArray("pos", new int[]{home.getX(), home.getY(), home.getZ()}) + .build() ); - if (file.exists()) { - return file; + } + } else { + // legacy + dataBuilder + .putInt("SpawnX", home.getX()) + .putInt("SpawnY", home.getY()) + .putInt("SpawnZ", home.getZ()); } - return null; + return input.createBuilder().put("Data", dataBuilder.build()).build(); } public Set getChunkChunks(String world) { - File folder = new File(PlotSquared.platform().worldContainer(), world + File.separator + "region"); - File[] regionFiles = folder.listFiles(); - if (regionFiles == null) { - throw new RuntimeException("Could not find worlds folder: " + folder + " ? (no read access?)"); + Path regionRoot = PlotSquared.platform().getWorldPath(world).resolve("region"); + if (!Files.exists(regionRoot)) { + throw new RuntimeException("Could not find regions folder: " + regionRoot + " ? (no read access?)"); } - HashSet chunks = new HashSet<>(); - for (File file : regionFiles) { - String name = file.getName(); - if (name.endsWith("mca")) { - String[] split = name.split("\\."); - try { - int x = Integer.parseInt(split[1]); - int z = Integer.parseInt(split[2]); - BlockVector2 loc = BlockVector2.at(x, z); - chunks.add(loc); - } catch (NumberFormatException ignored) { - } - } + try (Stream stream = Files.find(regionRoot, 1, WorldUtil::isMcaRegionFile)) { + return stream.map(Path::getFileName) + .map(this::fromMcaFileName) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + } catch (IOException e) { + LOGGER.error("Failed to traverse region directory", e); + return Set.of(); + } + } + + /** + * checks if the given file, by its path and BasicFileAttributes, is a mca region file + * @param path full path to file + * @param bfa attributes of the given file + * @return {@code true} if the given file is a seemingly valid mca region file. {@code false} otherwise + */ + private static boolean isMcaRegionFile(Path path, BasicFileAttributes bfa) { + if (bfa.isDirectory()) { + return false; + } + String name = path.getFileName().toString(); + return name.startsWith("r.") && name.endsWith(".mca"); + } + + /** + * Retrieves the coordinates from a region mca file + * @param filename the filename part of the full path ({@link Path#getFileName()}) + * @return A BV2 containg the coordinates, or {@code null} if the filename does not match the expected format + */ + private BlockVector2 fromMcaFileName(Path filename) { + String[] parts = filename.toString().split("\\."); + if (parts.length < 3) { + return null; + } + try { + return BlockVector2.at(Integer.parseInt(parts[1]), Integer.parseInt(parts[2])); + } catch (NumberFormatException e) { + return null; } - return chunks; } /**