diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItem.java index 50d6b2866..7d448cd5d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItem.java @@ -8,6 +8,8 @@ import javafx.stage.FileChooser; import org.jackhuang.hmcl.game.World; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.wizard.SinglePageWizardProvider; +import org.jackhuang.hmcl.util.IntVersionNumber; +import org.jackhuang.hmcl.util.VersionNumber; import java.io.File; import java.util.Date; @@ -24,7 +26,7 @@ public class WorldListItem extends Control { this.world = world; title.set(world.getWorldName()); - subtitle.set(i18n("world.description", world.getFileName(), new Date(world.getLastPlayed()).toString(), world.getGameVersion())); + subtitle.set(i18n("world.description", world.getFileName(), new Date(world.getLastPlayed()).toString(), world.getGameVersion() == null ? i18n("message.unknown") : world.getGameVersion())); } @Override @@ -58,6 +60,12 @@ public class WorldListItem extends Control { } public void manageDatapacks() { + if (world.getGameVersion() == null || // old game will not write game version to level.dat + (IntVersionNumber.isIntVersionNumber(world.getGameVersion()) // we don't parse snapshot version + && VersionNumber.asVersion(world.getGameVersion()).compareTo(VersionNumber.asVersion("1.13")) < 0)) { + Controllers.dialog(i18n("world.datapack.1_13")); + return; + } Controllers.navigate(new DatapackListPage(world.getWorldName(), world.getFile())); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListPage.java index da1ce1806..40c9a7732 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListPage.java @@ -3,16 +3,20 @@ package org.jackhuang.hmcl.ui.versions; import javafx.stage.FileChooser; import org.jackhuang.hmcl.game.World; import org.jackhuang.hmcl.setting.Profile; +import org.jackhuang.hmcl.task.Schedulers; +import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.ListPage; import org.jackhuang.hmcl.util.Logging; import java.io.File; import java.io.IOException; +import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; import java.util.logging.Level; +import java.util.stream.Collectors; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; @@ -25,33 +29,38 @@ public class WorldListPage extends ListPage { public void loadVersion(Profile profile, String id) { this.savesDir = profile.getRepository().getRunDirectory(id).toPath().resolve("saves"); - itemsProperty().clear(); - try { - if (Files.exists(savesDir)) - for (Path worldDir : Files.newDirectoryStream(savesDir)) { - itemsProperty().add(new WorldListItem(new World(worldDir))); - } - } catch (IOException e) { - Logging.LOG.log(Level.WARNING, "Failed to read saves", e); - } + itemsProperty().setAll(World.getWorlds(savesDir).stream() + .map(WorldListItem::new).collect(Collectors.toList())); } @Override public void add() { FileChooser chooser = new FileChooser(); - chooser.setTitle(i18n("world.choose_world")); + chooser.setTitle(i18n("world.import.choose")); chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("world.extension"), "*.zip")); List res = chooser.showOpenMultipleDialog(Controllers.getStage()); - if (res == null) return; - res.forEach(it -> { - try { - World world = new World(it.toPath()); - world.install(savesDir, world.getWorldName()); - itemsProperty().add(new WorldListItem(new World(savesDir.resolve(world.getWorldName())))); - } catch (IOException | IllegalArgumentException e) { - Logging.LOG.log(Level.WARNING, "Unable to parse datapack file " + it, e); - } - }); + if (res == null || res.isEmpty()) return; + try { + // Only accept one world file because user is required to confirm the new world name + // Or too many input dialogs are popped. + World world = new World(res.get(0).toPath()); + + Controllers.inputDialog(i18n("world.name.enter"), (name, resolve, reject) -> { + Task.of(() -> world.install(savesDir, name)) + .finalized(Schedulers.javafx(), var -> { + itemsProperty().add(new WorldListItem(new World(savesDir.resolve(name)))); + resolve.run(); + }, e -> { + if (e instanceof FileAlreadyExistsException) + reject.accept(i18n("world.import.failed", i18n("world.import.already_exists"))); + else + reject.accept(i18n("world.import.failed", e.getClass().getName() + ": " + e.getLocalizedMessage())); + }).start(); + }).setInitialText(world.getWorldName()); + + } catch (IOException | IllegalArgumentException e) { + Logging.LOG.log(Level.WARNING, "Unable to parse datapack file " + res.get(0), e); + } } } diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 125194ec5..b915b9148 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -251,8 +251,8 @@ datapack.title=World %s - Datapacks datapack.remove=Remove world=Worlds/Datapacks -world.choose_world=Choose the save zip to be imported world.datapack=Manage data packs +world.datapack.1_13=Only Minecraft 1.13 and later versions support data packs. world.description=%s. Last played time: %s. Game version: %s. world.export=Export this world world.export.title=Choose a file location to hold your world @@ -260,7 +260,11 @@ world.export.location=Export to world.export.wizard=Export world %s world.extension=World zip world.game_version=Game Version +world.import.already_exists=This world already exists. +world.import.choose=Choose the save zip to be imported +world.import.failed=Unable to import this world: %s world.name=World Name +world.name.enter=Enter the world name profile=Game Directories profile.default=Current directory diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 35308fd13..66d704adb 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -244,23 +244,27 @@ mods.add.success=成功新增模組 %s。 mods.choose_mod=選擇模組 mods.remove=刪除 -datapack=數據檔 -datapack.add=添加數據檔 -datapack.choose_datapack=選擇要導入的數據包壓縮檔 -datapack.title=世界 %s - 數據檔 +datapack=資料包 +datapack.add=添加資料包 +datapack.choose_datapack=選擇要導入的資料包壓縮檔 +datapack.title=世界 %s - 資料包 datapack.remove=刪除 -world=世界/數據檔 -world.choose_world=選擇要導入的存檔壓縮檔 -world.datapack=管理數據檔 +world=世界/資料包 +world.datapack=管理資料包 +world.datapack.1_13=僅 Minecraft 1.13 及之後的版本支持資料包 world.description=%s. 上一次遊戲時間: %s. 遊戲版本: %s world.export=導出此世界 world.export.title=選擇該世界的存儲位置 world.export.location=保存到 world.export.wizard=導出世界 %s world.extension=存檔壓縮檔 +world.import.already_exists=此世界已經存在 +world.import.choose=選擇要導入的存檔壓縮檔 +world.import.failed=無法導入此世界: %s world.game_version=遊戲版本 world.name=世界名稱 +world.name.enter=輸入世界名稱 profile=遊戲目錄 profile.default=目前目錄 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 5867d2fa2..0ff0c93bf 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -251,8 +251,8 @@ datapack.title=世界 %s - 数据包 datapack.remove=删除 world=世界/数据包 -world.choose_world=选择要导入的存档压缩包 world.datapack=管理数据包 +world.datapack.1_13=仅 Minecraft 1.13 及之后的版本支持数据包 world.description=%s. 上一次游戏时间: %s. 游戏版本: %s world.export=导出此世界 world.export.title=选择该世界的存储位置 @@ -260,7 +260,11 @@ world.export.location=保存到 world.export.wizard=导出世界 %s world.extension=世界压缩包 world.game_version=游戏版本 +world.import.already_exists=此世界已经存在 +world.import.choose=选择要导入的存档压缩包 +world.import.failed=无法导入此世界:%s world.name=世界名称 +world.name.enter=输入世界名称 profile=游戏目录 profile.default=当前目录 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java index 464e9f2b6..c00e9dae4 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java @@ -5,18 +5,18 @@ import com.github.steveice10.opennbt.tag.builtin.CompoundTag; import com.github.steveice10.opennbt.tag.builtin.LongTag; import com.github.steveice10.opennbt.tag.builtin.StringTag; import com.github.steveice10.opennbt.tag.builtin.Tag; -import org.jackhuang.hmcl.util.CompressingUtils; -import org.jackhuang.hmcl.util.FileUtils; -import org.jackhuang.hmcl.util.Unzipper; -import org.jackhuang.hmcl.util.Zipper; +import org.jackhuang.hmcl.util.*; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.nio.file.FileAlreadyExistsException; import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import java.util.logging.Level; import java.util.stream.Collectors; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; @@ -27,7 +27,7 @@ public class World { private String fileName; private String worldName; private String gameVersion; - private long lastPlayed, seed; + private long lastPlayed; public World(Path file) throws IOException { this.file = file; @@ -62,10 +62,6 @@ public class World { return lastPlayed; } - public long getSeed() { - return seed; - } - public String getGameVersion() { return gameVersion; } @@ -88,12 +84,23 @@ public class World { CompoundTag nbt = parseLevelDat(levelDat); CompoundTag data = nbt.get("Data"); - String name = data.get("LevelName").getValue(); - lastPlayed = data.get("LastPlayed").getValue(); - seed = data.get("RandomSeed").getValue(); - CompoundTag version = data.get("Version"); - gameVersion = version.get("Name").getValue(); - worldName = name; + if (data.get("LevelName") instanceof StringTag) + worldName = data.get("LevelName").getValue(); + else + throw new IOException("level.dat missing LevelName"); + + if (data.get("LastPlayed") instanceof LongTag) + lastPlayed = data.get("LastPlayed").getValue(); + else + throw new IOException("level.dat missing LastPlayed"); + + gameVersion = null; + if (data.get("Version") instanceof CompoundTag) { + CompoundTag version = data.get("Version"); + + if (version.get("Name") instanceof StringTag) + gameVersion = version.get("Name").getValue(); + } } public void rename(String newName) throws IOException { @@ -106,7 +113,9 @@ public class World { CompoundTag data = nbt.get("Data"); data.put(new StringTag("LevelName", newName)); - NBTIO.writeTag(new GZIPOutputStream(Files.newOutputStream(levelDat)), nbt); + try (OutputStream os = new GZIPOutputStream(Files.newOutputStream(levelDat))) { + NBTIO.writeTag(os, nbt); + } // then change the folder's name Files.move(file, file.resolveSibling(newName)); @@ -127,9 +136,10 @@ public class World { } subDirectoryName = FileUtils.getName(subDirs.get(0)); } - new Unzipper(file, savesDir) + new Unzipper(file, worldDir) .setSubDirectory("/" + subDirectoryName + "/") .unzip(); + new World(worldDir).rename(name); } else if (Files.isDirectory(file)) { FileUtils.copyDirectory(file, worldDir); } @@ -145,17 +155,28 @@ public class World { } private static CompoundTag parseLevelDat(Path path) throws IOException { - Tag nbt = NBTIO.readTag(new GZIPInputStream(Files.newInputStream(path))); - if (nbt instanceof CompoundTag) - return (CompoundTag) nbt; - else - throw new IOException("level.dat malformed"); + try (InputStream is = new GZIPInputStream(Files.newInputStream(path))) { + Tag nbt = NBTIO.readTag(is); + if (nbt instanceof CompoundTag) + return (CompoundTag) nbt; + else + throw new IOException("level.dat malformed"); + } } - public static List getWorlds(Path worldDir) throws IOException { + public static List getWorlds(Path savesDir) { List worlds = new ArrayList<>(); - for (Path world : Files.newDirectoryStream(worldDir)) { - worlds.add(new World(world)); + try { + if (Files.exists(savesDir)) + for (Path world : Files.newDirectoryStream(savesDir)) { + try { + worlds.add(new World(world)); + } catch (IOException | IllegalArgumentException e) { + Logging.LOG.log(Level.WARNING, "Failed to read world " + world, e); + } + } + } catch (IOException e) { + Logging.LOG.log(Level.WARNING, "Failed to read saves", e); } return worlds; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Task.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Task.java index cb8598ae4..f827da418 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Task.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Task.java @@ -301,7 +301,13 @@ public abstract class Task { return finalized(scheduler, (variables, isDependentsSucceeded) -> { if (isDependentsSucceeded) { if (success != null) - success.accept(variables); + try { + success.accept(variables); + } catch (Exception e) { + Logging.LOG.log(Level.WARNING, "Failed to execute " + success, e); + if (failure != null) + failure.accept(e); + } } else { if (failure != null) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/CompressingUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/CompressingUtils.java index de4167590..4997a73d3 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/CompressingUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/CompressingUtils.java @@ -73,6 +73,8 @@ public final class CompressingUtils { } catch (ZipError error) { // Since Java 8 throws ZipError stupidly throw new ZipException(error.getMessage()); + } catch (UnsupportedOperationException ex) { + throw new IOException("Not a zip file"); } }