diff --git a/CommonCore/SQL/src/de/steamwar/sql/World.kt b/CommonCore/SQL/src/de/steamwar/sql/World.kt new file mode 100644 index 00000000..174feda2 --- /dev/null +++ b/CommonCore/SQL/src/de/steamwar/sql/World.kt @@ -0,0 +1,181 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.sql + +import de.steamwar.sql.internal.useDb +import org.jetbrains.exposed.v1.core.and +import org.jetbrains.exposed.v1.core.dao.id.EntityID +import org.jetbrains.exposed.v1.core.dao.id.UUIDTable +import org.jetbrains.exposed.v1.core.eq +import org.jetbrains.exposed.v1.core.not +import org.jetbrains.exposed.v1.dao.Entity +import org.jetbrains.exposed.v1.dao.EntityClass +import org.jetbrains.exposed.v1.javatime.CurrentTimestamp +import org.jetbrains.exposed.v1.javatime.timestamp +import java.io.File +import java.time.Instant +import java.time.temporal.ChronoUnit +import java.util.UUID + +object WorldTable : UUIDTable("world") { + val name = varchar("Name", 512) + val version = integer("Version") + val type = enumeration("Type") + val owner = reference("Owner", SteamwarUserTable).nullable() + val deleted = bool("Deleted").default(false) + val created = timestamp("Created").defaultExpression(CurrentTimestamp) + val lastUsed = timestamp("LastUsed").defaultExpression(CurrentTimestamp) +} + +enum class WorldType { + BAU, + BUILDER, +} + +class SteamwarWorld(id: EntityID) : Entity(id) { + var name by WorldTable.name + private set + var version by WorldTable.version + private set + var type by WorldTable.type + private set + var owner by WorldTable.owner + private set + var deleted by WorldTable.deleted + private set + val created by WorldTable.created + var lastUsed by WorldTable.lastUsed + private set + + val archived: Boolean + get() = !storageDirectory.exists() && archiveFile.exists() + + val shouldArchive: Boolean + get() = !archived && (deleted || lastUsed.plus(7, ChronoUnit.DAYS).isBefore(Instant.now())) + + val uuid: UUID + get() = id.value + + val storageDirectory: File + get() = File(WORLD_STORAGE, id.value.toString()) + + private val archiveFile: File + get() = File(ARCHIVE_WORLD_STORAGE, "${id.value}.zip") + + @JvmOverloads + fun setupAndGetStoragePath(prototype: File? = null): String { + if (archived) { + loadFromArchive() + } + + val needsInitialization = !storageDirectory.exists() || storageDirectory.list()?.isEmpty() == true + if (needsInitialization) { + if (prototype != null && prototype.exists()) { + prototype.copyRecursively(storageDirectory, overwrite = true) + } else { + storageDirectory.mkdirs() + } + } + + useDb { + lastUsed = Instant.now() + } + + return storageDirectory.path + } + + fun markDeleted() = useDb { + deleted = true + } + + fun rename(newName: String) = useDb { + name = newName + } + + private fun archiveWorld() = ProcessBuilder("zip", "-u9oymrqq", "$ARCHIVE_WORLD_STORAGE/${id.value}.zip", id.value.toString()) + .directory(File(WORLD_STORAGE)) + .inheritIO() + .start() + .waitFor() + + + private fun loadFromArchive() = ProcessBuilder("unzip", "-qq", "-o", "$ARCHIVE_WORLD_STORAGE/${id.value}.zip", "-d", WORLD_STORAGE) + .inheritIO() + .start() + .waitFor() + + companion object : EntityClass(WorldTable) { + const val WORLD_STORAGE = "/worlds/storage" + const val ARCHIVE_WORLD_STORAGE = "/mnt/storage/worlds/storage" + + @JvmStatic + fun getBauWorld(user: SteamwarUser, version: Int) = useDb { + find { + (WorldTable.owner eq user.id) and + (WorldTable.version eq version) and + (WorldTable.type eq WorldType.BAU) and + not(WorldTable.deleted) + }.firstOrNull() + } + + @JvmStatic + @JvmOverloads + fun getOrCreateBauWorld(user: SteamwarUser, version: Int, prototype: File? = null): SteamwarWorld = + getBauWorld(user, version) ?: createWorld(user, user.userName, version, WorldType.BAU, prototype) + + @JvmStatic + fun getBuilderWorld(name: String, version: Int) = useDb { + find { + (WorldTable.name eq name) and + (WorldTable.version eq version) and + (WorldTable.type eq WorldType.BUILDER) and + not(WorldTable.deleted) + }.firstOrNull() + } + + @JvmStatic + fun getBuilderWorlds(version: Int) = useDb { + find { + (WorldTable.version eq version) and + (WorldTable.type eq WorldType.BUILDER) and + not(WorldTable.deleted) + }.toList() + } + + @JvmStatic + @JvmOverloads + fun getOrCreateBuilderWorld(name: String, version: Int, prototype: File? = null): SteamwarWorld = + getBuilderWorld(name, version) ?: createWorld(null, name, version, WorldType.BUILDER, prototype) + + @JvmStatic + @JvmOverloads + fun createWorld(user: SteamwarUser?, name: String, version: Int, type: WorldType, prototype: File? = null): SteamwarWorld { + val world = useDb { new { + this.name = name + this.version = version + this.type = type + this.owner = user?.id + } } + + world.setupAndGetStoragePath(prototype) + return world + } + } +} diff --git a/VelocityCore/deployarena.py b/VelocityCore/deployarena.py index 2195e264..6cd5d62f 100755 --- a/VelocityCore/deployarena.py +++ b/VelocityCore/deployarena.py @@ -37,7 +37,7 @@ if __name__ == "__main__": with open(configfile, 'r') as file: gamemode = yaml.load(file) - builderworld = path.expanduser(f'/worlds/builder{version}/{worldname}') + builderworld = sys.argv[4] if len(sys.argv) > 4 else path.expanduser(f'/worlds/builder{version}/{worldname}') arenaworld = f'/servers/{gamemode["Server"]["Folder"]}/arenas/{worldname}' if path.exists(arenaworld): diff --git a/VelocityCore/src/de/steamwar/velocitycore/ServerStarter.java b/VelocityCore/src/de/steamwar/velocitycore/ServerStarter.java index 3ea195cd..dd7a952b 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/ServerStarter.java +++ b/VelocityCore/src/de/steamwar/velocitycore/ServerStarter.java @@ -52,11 +52,9 @@ public class ServerStarter { private static final String EVENT_PATH = TMP_DATA + "event/"; public static final String TEMP_WORLD_PATH = TMP_DATA + "arenaserver/"; - private static final String WORLDS_FOLDER = "/worlds"; - public static final String WORLDS_BASE_PATH = WORLDS_FOLDER + "/userworlds"; - public static final String BUILDER_BASE_PATH = WORLDS_FOLDER + "/builder"; - - private static final String WORLDS_STORAGE_BASE_PATH = "/mnt/storage/worlds/userworlds"; + public static final String WORLDS_BASE_PATH = SteamwarWorld.WORLD_STORAGE + "/"; + public static final String LEGACY_WORLDS_BASE_PATH = "/worlds/userworlds"; + public static final String LEGACY_BUILDER_BASE_PATH = "/worlds/builder"; private File directory = null; private String worldDir = null; @@ -152,23 +150,15 @@ public class ServerStarter { public ServerStarter build(ServerVersion version, UUID owner) { this.version = version; directory = version.getServerDirectory("Bau"); - worldDir = version.getWorldFolder(WORLDS_BASE_PATH); - worldName = String.valueOf(SteamwarUser.get(owner).getId()); + SteamwarUser user = SteamwarUser.get(owner); + SteamwarWorld world = SteamwarWorld.getOrCreateBauWorld(user, version.getVersionSuffix()); + worldDir = WORLDS_BASE_PATH; + worldName = world.getUuid().toString(); checkpoint = true; build(owner); - worldSetup = () -> { - File world = new File(worldDir, worldName); - if (!world.exists()) { - File storage = new File(version.getWorldFolder(WORLDS_STORAGE_BASE_PATH), worldName); - - if(storage.exists()) - node.execute("mv", storage.getPath(), world.getPath()); - else - copyWorld(node, new File(directory, "Bauwelt").getPath(), world.getPath()); - } - }; + worldSetup = () -> world.setupAndGetStoragePath(new File(directory, "Bauwelt")); // Send players to existing server startCondition = () -> { @@ -224,8 +214,9 @@ public class ServerStarter { public ServerStarter builder(ServerVersion version, String map, File generator) { this.version = version; directory = version.getServerDirectory("Builder"); - worldDir = version.getWorldFolder(BUILDER_BASE_PATH); - worldName = map; + SteamwarWorld world = SteamwarWorld.getOrCreateBuilderWorld(map, version.getVersionSuffix()); + worldDir = WORLDS_BASE_PATH; + worldName = world.getUuid().toString(); serverNameProvider = port -> "*" + map; checkpoint = true; constructor = (serverName, port, builder, shutdownCallback, failureCallback) -> new Builderserver(serverName, worldName, port, builder, shutdownCallback, failureCallback); @@ -243,13 +234,15 @@ public class ServerStarter { if(generator != null) { worldSetup = () -> { - File leveldat = new File(new File(worldDir, worldName), "level.dat"); + File leveldat = new File(world.setupAndGetStoragePath(), "level.dat"); try { Files.copy(generator.toPath(), leveldat.toPath()); } catch (IOException e) { throw new SecurityException(e); } }; + } else { + worldSetup = () -> world.setupAndGetStoragePath(legacyBuilderWorld(version, map)); } return this; @@ -350,6 +343,22 @@ public class ServerStarter { node.execute("cp", "-r", template, target); } + private static File legacyBauWorld(ServerVersion version, SteamwarUser user) { + File legacyIdWorld = new File(version.getWorldFolder(LEGACY_WORLDS_BASE_PATH), String.valueOf(user.getId())); + if(legacyIdWorld.exists()) + return legacyIdWorld; + + File legacyUuidWorld = new File(version.getWorldFolder(LEGACY_WORLDS_BASE_PATH), user.getUUID().toString()); + if(legacyUuidWorld.exists()) + return legacyUuidWorld; + + return new File(version.getServerDirectory("Bau"), "Bauwelt"); + } + + public static File legacyBuilderWorld(ServerVersion version, String map) { + return new File(version.getWorldFolder(LEGACY_BUILDER_BASE_PATH), map); + } + private interface ServerConstructor { Subserver construct(String serverName, int port, ProcessBuilder builder, Runnable shutdownCallback, Consumer failureCallback); } @@ -390,4 +399,4 @@ public class ServerStarter { } } -} \ No newline at end of file +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/BauCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/BauCommand.java index 77e5fae6..518376ea 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/BauCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/BauCommand.java @@ -34,6 +34,7 @@ import de.steamwar.network.packets.server.BaumemberUpdatePacket; import de.steamwar.persistent.Bauserver; import de.steamwar.sql.BauweltMember; import de.steamwar.sql.SteamwarUser; +import de.steamwar.sql.SteamwarWorld; import de.steamwar.velocitycore.*; import de.steamwar.velocitycore.inventory.SWInventory; import de.steamwar.velocitycore.inventory.SWItem; @@ -41,6 +42,7 @@ import de.steamwar.velocitycore.network.NetworkSender; import de.steamwar.velocitycore.util.BauLock; import de.steamwar.data.BauLockState; +import java.io.File; import java.util.Collection; import java.util.function.Consumer; @@ -220,14 +222,20 @@ public class BauCommand extends SWCommand { public void delete(PlayerChatter sender, ServerVersion version) { SWInventory inventory = new SWInventory(sender, 9, new Message("BAU_DELETE_GUI_NAME")); inventory.addItem(0, new SWItem(new Message("BAU_DELETE_GUI_DELETE"), 10), click -> { - String world = version.getWorldFolder(ServerStarter.WORLDS_BASE_PATH) + sender.user().getId(); + SteamwarWorld world = SteamwarWorld.getBauWorld(sender.user(), version.getVersionSuffix()); VelocityCore.schedule(() -> { Bauserver subserver = Bauserver.get(sender.user().getUUID()); if(subserver != null) subserver.stop(); - SubserverSystem.deleteFolder(VelocityCore.local, world); + if(world != null) { + world.markDeleted(); + SubserverSystem.deleteFolder(VelocityCore.local, world.getStorageDirectory().getPath()); + } else { + File legacyWorld = new File(version.getWorldFolder(ServerStarter.LEGACY_WORLDS_BASE_PATH), String.valueOf(sender.user().getId())); + SubserverSystem.deleteFolder(VelocityCore.local, legacyWorld.getPath()); + } sender.system("BAU_DELETE_DELETED"); }).schedule(); diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/BuilderCloudCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/BuilderCloudCommand.java index 5ae01fe7..7ba21d41 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/BuilderCloudCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/BuilderCloudCommand.java @@ -21,7 +21,7 @@ package de.steamwar.velocitycore.commands; import de.steamwar.sql.GameModeConfig; import de.steamwar.linkage.Linked; -import de.steamwar.sql.SchematicType; +import de.steamwar.sql.SteamwarWorld; import de.steamwar.velocitycore.ArenaMode; import de.steamwar.velocitycore.ServerStarter; import de.steamwar.velocitycore.ServerVersion; @@ -34,11 +34,11 @@ import de.steamwar.messages.PlayerChatter; import de.steamwar.sql.UserPerm; import java.io.File; -import java.io.IOException; -import java.nio.file.Files; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; @Linked public class BuilderCloudCommand extends SWCommand { @@ -49,13 +49,18 @@ public class BuilderCloudCommand extends SWCommand { @Register(value = "create", description = "BUILDERCLOUD_CREATE_USAGE") public void create(Chatter sender, @ErrorMessage("BUILDERCLOUD_VERSION") ServerVersion version, @Mapper("map") String map, @OptionalValue("") @Mapper("generator") @AllowNull File generator) { - mapFile(version, map).mkdir(); + if(mapExists(version, map)) { + sender.system("BUILDERCLOUD_EXISTING_MAP"); + return; + } + + SteamwarWorld.getOrCreateBuilderWorld(map, version.getVersionSuffix()).setupAndGetStoragePath(); sender.withPlayer(p -> new ServerStarter().builder(version, map, generator).send(p).start()); } @Register(description = "BUILDERCLOUD_USAGE") public void start(PlayerChatter sender, @ErrorMessage("BUILDERCLOUD_VERSION") ServerVersion version, @Mapper("map") String map) { - if(!mapFile(version, map).exists()) { + if(!mapExists(version, map)) { sender.system("BUILDERCLOUD_UNKNOWN_MAP"); return; } @@ -65,36 +70,32 @@ public class BuilderCloudCommand extends SWCommand { @Register(value = "rename", description = "BUILDERCLOUD_RENAME_USAGE") public void rename(Chatter sender, @ErrorMessage("BUILDERCLOUD_VERSION") ServerVersion version, @Mapper("map") String oldName, String newName) { - File oldMap = mapFile(version, oldName); - if(!oldMap.exists()) { + SteamwarWorld oldMap = builderWorld(version, oldName); + if(oldMap == null) { sender.system("BUILDERCLOUD_UNKNOWN_MAP"); return; } - File newMap = mapFile(version, newName); - if(newMap.exists()) { + if(mapExists(version, newName)) { sender.system("BUILDERCLOUD_EXISTING_MAP"); return; } - try { - Files.move(oldMap.toPath(), newMap.toPath()); - } catch (IOException e) { - throw new SecurityException(e); - } + oldMap.rename(newName); sender.system("BUILDERCLOUD_RENAMED"); } @Register(value = "deploy", description = "BUILDERCLOUD_DEPLOY_USAGE") public void deploy(Chatter sender, @Mapper("nonHistoricArenaMode") GameModeConfig arenaMode, @ErrorMessage("BUILDERCLOUD_VERSION") ServerVersion version, @Mapper("map") String map) { - if(!mapFile(version, map).exists()) { + SteamwarWorld builderWorld = builderWorld(version, map); + if(builderWorld == null) { sender.system("BUILDERCLOUD_UNKNOWN_MAP"); return; } VelocityCore.schedule(() -> { - VelocityCore.local.execute("deployarena.py", arenaMode.configFile.getName(), Integer.toString(version.getVersionSuffix()), map); + VelocityCore.local.execute("deployarena.py", arenaMode.configFile.getName(), Integer.toString(version.getVersionSuffix()), map, builderWorld.setupAndGetStoragePath()); ArenaMode.init(); sender.system("BUILDERCLOUD_DEPLOY_FINISHED"); }).schedule(); @@ -112,13 +113,23 @@ public class BuilderCloudCommand extends SWCommand { @Override public Collection tabCompletes(Chatter sender, PreviousArguments previousArguments, String s) { - File folder = getWorldFolder(previousArguments, 1); - - String[] files; - if(folder == null || (files = folder.list()) == null) + ServerVersion version = getVersion(previousArguments, 1); + if(version == null) return Collections.emptyList(); - return Arrays.stream(files).filter(file -> new File(folder, file).isDirectory()).filter(file -> s.startsWith(".") || !file.startsWith(".")).toList(); + Set maps = new LinkedHashSet<>(); + SteamwarWorld.getBuilderWorlds(version.getVersionSuffix()).stream() + .map(SteamwarWorld::getName) + .forEach(maps::add); + + File legacyFolder = new File(version.getWorldFolder(ServerStarter.LEGACY_BUILDER_BASE_PATH)); + String[] files = legacyFolder.list(); + if(files != null) + Arrays.stream(files).filter(file -> new File(legacyFolder, file).isDirectory()).forEach(maps::add); + + return maps.stream() + .filter(file -> s.startsWith(".") || !file.startsWith(".")) + .toList(); } }; } @@ -133,7 +144,7 @@ public class BuilderCloudCommand extends SWCommand { if(s.isEmpty()) return null; - File folder = getWorldFolder(previousArguments, 2); + File folder = getLegacyWorldFolder(previousArguments, 2); if(folder == null) throw new SecurityException(); @@ -147,7 +158,7 @@ public class BuilderCloudCommand extends SWCommand { @Override public Collection tabCompletes(Chatter sender, PreviousArguments previousArguments, String s) { - File folder = getWorldFolder(previousArguments, 2); + File folder = getLegacyWorldFolder(previousArguments, 2); String[] files; if(folder == null || (files = folder.list()) == null) @@ -158,14 +169,36 @@ public class BuilderCloudCommand extends SWCommand { }; } - private File mapFile(ServerVersion version, String map) { - return new File(version.getWorldFolder(ServerStarter.BUILDER_BASE_PATH), map); + private SteamwarWorld builderWorld(ServerVersion version, String map) { + SteamwarWorld world = SteamwarWorld.getBuilderWorld(map, version.getVersionSuffix()); + if(world != null) + return world; + + File legacyWorld = mapFile(version, map); + if(!legacyWorld.exists()) + return null; + + return SteamwarWorld.getOrCreateBuilderWorld(map, version.getVersionSuffix(), legacyWorld); } - private File getWorldFolder(PreviousArguments previousArguments, int offset) { - ServerVersion v = ServerVersion.get(previousArguments.userArgs[previousArguments.userArgs.length - offset]); + private boolean mapExists(ServerVersion version, String map) { + return SteamwarWorld.getBuilderWorld(map, version.getVersionSuffix()) != null || mapFile(version, map).exists(); + } + + private File mapFile(ServerVersion version, String map) { + return new File(version.getWorldFolder(ServerStarter.LEGACY_BUILDER_BASE_PATH), map); + } + + private File getLegacyWorldFolder(PreviousArguments previousArguments, int offset) { + ServerVersion v = getVersion(previousArguments, offset); if(v == null) return null; - return new File(v.getWorldFolder(ServerStarter.BUILDER_BASE_PATH)); + return new File(v.getWorldFolder(ServerStarter.LEGACY_BUILDER_BASE_PATH)); + } + + private ServerVersion getVersion(PreviousArguments previousArguments, int offset) { + if(previousArguments.userArgs.length < offset) + return null; + return ServerVersion.get(previousArguments.userArgs[previousArguments.userArgs.length - offset]); } } diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/GDPRQuery.java b/VelocityCore/src/de/steamwar/velocitycore/commands/GDPRQuery.java index 4cdbfc69..e75e6e4b 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/GDPRQuery.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/GDPRQuery.java @@ -24,11 +24,16 @@ import de.steamwar.linkage.Linked; import de.steamwar.messages.Chatter; import de.steamwar.messages.PlayerChatter; import de.steamwar.sql.SteamwarUser; +import de.steamwar.sql.SteamwarWorld; import de.steamwar.sql.UserPerm; import de.steamwar.sql.internal.Statement; +import de.steamwar.velocitycore.ServerStarter; +import de.steamwar.velocitycore.ServerVersion; import de.steamwar.velocitycore.VelocityCore; import java.io.*; +import java.util.LinkedHashSet; +import java.util.Set; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -64,12 +69,21 @@ public class GDPRQuery extends SWCommand { copy(getClass().getClassLoader().getResourceAsStream("GDPRQueryREADME.md"), out, "README.txt"); sender.system("GDPR_STATUS_WORLD"); - copyBauwelt(user, out, "/home/minecraft/userworlds/" + user.getUUID().toString(), "BuildWorld12"); - copyBauwelt(user, out, "/home/minecraft/userworlds15/" + user.getId(), "BuildWorld15"); + Set exportedVersions = new LinkedHashSet<>(); + for(ServerVersion version : ServerVersion.values()) { + int versionSuffix = version.getVersionSuffix(); + if(!exportedVersions.add(versionSuffix)) + continue; + + SteamwarWorld world = SteamwarWorld.getBauWorld(user, versionSuffix); + if(world != null) + copyBauwelt(user, out, world.setupAndGetStoragePath(), "BuildWorld" + versionSuffix); + else + copyLegacyBauwelt(user, out, version); + } sender.system("GDPR_STATUS_INVENTORIES"); - copyPlayerdata(user, out, "/home/minecraft/userworlds", "BuildInventories12"); - copyPlayerdata(user, out, "/home/minecraft/userworlds15", "BuildInventories15"); + copyPlayerdata(user, out, SteamwarWorld.WORLD_STORAGE, "BuildInventories"); sender.system("GDPR_STATUS_DATABASE"); sqlCSV(user, out, bannedIPs, "BannedIPs.csv"); @@ -230,6 +244,18 @@ public class GDPRQuery extends SWCommand { copy(playerdata, out, outDir + "/playerdata/" + user.getUUID().toString() + ".dat"); } + private void copyLegacyBauwelt(SteamwarUser user, ZipOutputStream out, ServerVersion version) throws IOException { + File legacyIdWorld = new File(version.getWorldFolder(ServerStarter.LEGACY_WORLDS_BASE_PATH), String.valueOf(user.getId())); + if(legacyIdWorld.exists()) { + copyBauwelt(user, out, legacyIdWorld.getPath(), "BuildWorld" + version.getVersionSuffix()); + return; + } + + File legacyUuidWorld = new File(version.getWorldFolder(ServerStarter.LEGACY_WORLDS_BASE_PATH), user.getUUID().toString()); + if(legacyUuidWorld.exists()) + copyBauwelt(user, out, legacyUuidWorld.getPath(), "BuildWorld" + version.getVersionSuffix()); + } + private void copyPlayerdata(SteamwarUser user, ZipOutputStream out, String inDir, String outDir) throws IOException { File worlds = new File(inDir); String path = "playerdata/" + user.getUUID().toString() + ".dat";