diff --git a/build.gradle.kts b/build.gradle.kts index c4288ad..ca880d4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -63,7 +63,12 @@ tasks.test { } tasks.processResources { - filesMatching("**") { + filesMatching("**/*.json") { + expand( + "version" to project.version, + ) + } + filesMatching("**/*.mod.json") { expand( "version" to project.version, ) diff --git a/src/main/java/i18nupdatemod/I18nUpdateMod.java b/src/main/java/i18nupdatemod/I18nUpdateMod.java index 1afd352..8ed0dbf 100644 --- a/src/main/java/i18nupdatemod/I18nUpdateMod.java +++ b/src/main/java/i18nupdatemod/I18nUpdateMod.java @@ -7,6 +7,8 @@ import i18nupdatemod.core.ResourcePack; import i18nupdatemod.core.ResourcePackConverter; import i18nupdatemod.entity.GameAssetDetail; +import i18nupdatemod.core.LoadDetailUI; +import i18nupdatemod.entity.LoadStage; import i18nupdatemod.util.FileUtil; import i18nupdatemod.util.Log; @@ -24,11 +26,17 @@ public class I18nUpdateMod { public static final String MOD_ID = "i18nupdatemod"; public static String MOD_VERSION; - + public static volatile boolean shouldShutdown = false; public static final Gson GSON = new Gson(); public static void init(Path minecraftPath, String minecraftVersion, String loader) { + LoadDetailUI.show(); + LoadDetailUI.setStage(LoadStage.INIT); + try (InputStream is = I18nUpdateMod.class.getResourceAsStream("/i18nMetaData.json")) { + if (is == null) { + throw new IllegalStateException("/i18nMetaData.json not found"); + } MOD_VERSION = GSON.fromJson(new InputStreamReader(is), JsonObject.class).get("version").getAsString(); } catch (Exception e) { Log.warning("Error getting version: " + e); @@ -47,6 +55,11 @@ public static void init(Path minecraftPath, String minecraftVersion, String load Log.warning("I18nUpdateMod会从互联网获取内容不可控的资源包。"); Log.warning("这一行为违背了网易我的世界「开发者内容审核制度」:禁止上传与提审内容不一致的游戏内容。"); Log.warning("为了遵循这一制度,I18nUpdateMod不会下载任何内容。"); + + LoadDetailUI.appendLog("I18nUpdateMod会从互联网获取内容不可控的资源包。"); + LoadDetailUI.appendLog("这一行为违背了网易我的世界「开发者内容审核制度」:禁止上传与提审内容不一致的游戏内容。"); + LoadDetailUI.appendLog("为了遵循这一制度,I18nUpdateMod不会下载任何内容。"); + LoadDetailUI.appendLog("请您手动关闭此窗口"); return; } catch (ClassNotFoundException ignored) { } @@ -57,13 +70,23 @@ public static void init(Path minecraftPath, String minecraftVersion, String load try { //Get asset + if (shouldShutdown) { + return; + } GameAssetDetail assets = I18nConfig.getAssetDetail(minecraftVersion, loader); //Update resource pack + LoadDetailUI.setStage(LoadStage.DOWNLOAD_ASSET); + if (shouldShutdown) { + return; + } List languagePacks = new ArrayList<>(); boolean convertNotNeed = assets.downloads.size() == 1 && assets.downloads.get(0).targetVersion.equals(minecraftVersion); String applyFileName = assets.downloads.get(0).fileName; for (GameAssetDetail.AssetDownloadDetail it : assets.downloads) { + if (shouldShutdown) { + return; + } FileUtil.setTemporaryDirPath(Paths.get(localStorage, "." + MOD_ID, it.targetVersion)); ResourcePack languagePack = new ResourcePack(it.fileName, convertNotNeed); languagePack.checkUpdate(it.fileUrl, it.md5Url); @@ -71,22 +94,37 @@ public static void init(Path minecraftPath, String minecraftVersion, String load } //Convert resourcepack + LoadDetailUI.setStage(LoadStage.CONVERT_RESOURCE_PACK); + if (shouldShutdown) { + return; + } if (!convertNotNeed) { FileUtil.setTemporaryDirPath(Paths.get(localStorage, "." + MOD_ID, minecraftVersion)); applyFileName = assets.covertFileName; ResourcePackConverter converter = new ResourcePackConverter(languagePacks, applyFileName); converter.convert(assets.covertPackFormat, getResourcePackDescription(assets.downloads)); } + LoadDetailUI.appendLog("资源包已转换完成。"); //Apply resource pack + LoadDetailUI.setStage(LoadStage.APPLY_RESOURCE_PACK); + if (shouldShutdown) { + return; + } GameConfig config = new GameConfig(minecraftPath.resolve("options.txt")); config.addResourcePack("Minecraft-Mod-Language-Modpack", (minecraftMajorVersion <= 12 ? "" : "file/") + applyFileName); config.writeToFile(); + LoadDetailUI.appendLog("资源包已应用。"); + LoadDetailUI.setStage(LoadStage.FINISH); } catch (Exception e) { Log.warning(String.format("Failed to update resource pack: %s", e)); + LoadDetailUI.appendLog(String.format("I18n Update Mod 运行失败: %s", e)); + LoadDetailUI.autoClose(6000); + return; // e.printStackTrace(); } + LoadDetailUI.hide(); } private static String getResourcePackDescription(List downloads) { diff --git a/src/main/java/i18nupdatemod/core/I18nConfig.java b/src/main/java/i18nupdatemod/core/I18nConfig.java index b0169c7..63b0815 100644 --- a/src/main/java/i18nupdatemod/core/I18nConfig.java +++ b/src/main/java/i18nupdatemod/core/I18nConfig.java @@ -1,10 +1,7 @@ package i18nupdatemod.core; import com.google.gson.Gson; -import i18nupdatemod.entity.AssetMetaData; -import i18nupdatemod.entity.GameAssetDetail; -import i18nupdatemod.entity.GameMetaData; -import i18nupdatemod.entity.I18nMetaData; +import i18nupdatemod.entity.*; import i18nupdatemod.util.Log; import i18nupdatemod.util.Version; import i18nupdatemod.util.VersionRange; @@ -62,8 +59,10 @@ public static GameAssetDetail getAssetDetail(String minecraftVersion, String loa GameMetaData convert = getGameMetaData(minecraftVersion); GameAssetDetail ret = new GameAssetDetail(); + LoadDetailUI.appendLog("正在获取最快的镜像源..."); String assetRoot = getFastestUrl(); Log.debug("Using asset root: " + assetRoot); + LoadDetailUI.appendLog("即将从 " + assetRoot + " 下载资源包"); if (assetRoot.equals("https://raw.githubusercontent.com/")) { ret.downloads = createDownloadDetailsFromGit(convert, loader); diff --git a/src/main/java/i18nupdatemod/core/LoadDetailUI.java b/src/main/java/i18nupdatemod/core/LoadDetailUI.java new file mode 100644 index 0000000..887ef87 --- /dev/null +++ b/src/main/java/i18nupdatemod/core/LoadDetailUI.java @@ -0,0 +1,168 @@ +package i18nupdatemod.core; + +import i18nupdatemod.I18nUpdateMod; +import i18nupdatemod.entity.LoadStage; +import i18nupdatemod.util.Log; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.net.URL; + +public class LoadDetailUI { + private static volatile LoadDetailUI instance; + private final JFrame frame; + private final JProgressBar statusBar; + private final JTextArea logArea; + private final boolean useGUI; + + private LoadDetailUI() { + useGUI = !Boolean.parseBoolean(System.getProperty("java.awt.headless", "false")); + + if (!useGUI) { + frame = null; + statusBar = null; + logArea = null; + return; + } + + frame = new JFrame(); + frame.setTitle("I18nUpdateMod-资源包下载进度"); + frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); + frame.setSize(854, 480); + frame.setLocationRelativeTo(null); + frame.setLayout(new BorderLayout()); + frame.addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + shutdown(); + } + }); + + + URL iconURL = getClass().getResource("/icons/CFPA.png"); + if (iconURL != null) { + Image icon = Toolkit.getDefaultToolkit().getImage(iconURL); + frame.setIconImage(icon); + } + + // 主面板 + JPanel panel = new JPanel(); + panel.setBackground(new Color(220, 220, 220)); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + panel.setBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15)); + + // 状态栏 + statusBar = new JProgressBar(); + statusBar.setString(LoadStage.getDescription(LoadStage.INIT)); + statusBar.setStringPainted(true); + statusBar.setMaximum(LoadStage.values().length - 1); + statusBar.setValue(0); + statusBar.setForeground(new Color(102, 255, 102)); + panel.add(statusBar); + panel.add(Box.createVerticalStrut(10)); + + // 日志输出区 + logArea = new JTextArea(6, 30); + logArea.setEditable(false); + logArea.setFont(new Font("Monospaced", Font.PLAIN, 13)); + JScrollPane scrollPane = new JScrollPane(logArea); + scrollPane.setBorder(BorderFactory.createLineBorder(Color.GRAY)); + panel.add(scrollPane); + panel.add(Box.createVerticalStrut(10)); + + // 提示文字 + JLabel tip = new JLabel("如遇到进度卡住,可以点击下方按钮/关闭此窗口停止此次下载。"); + tip.setAlignmentX(Component.CENTER_ALIGNMENT); + panel.add(tip); + panel.add(Box.createVerticalStrut(10)); + + // 停止按钮 + JButton stopButton = new JButton("停止此次下载"); + stopButton.setFocusPainted(false); + stopButton.setBackground(Color.WHITE); + stopButton.setAlignmentX(Component.CENTER_ALIGNMENT); + stopButton.addActionListener((ActionEvent e) -> shutdown()); + panel.add(stopButton); + + frame.add(panel, BorderLayout.CENTER); + } + + public static LoadDetailUI getInstance() { + if (instance == null) { + synchronized (LoadDetailUI.class) { + if (instance == null) { + instance = new LoadDetailUI(); + } + } + } + return instance; + } + + public static void show() { + LoadDetailUI gui = getInstance(); + if (!gui.useGUI || gui.frame == null) { + return; + } + SwingUtilities.invokeLater(() -> gui.frame.setVisible(true)); + } + + public static void hide() { + LoadDetailUI gui = getInstance(); + if (!gui.useGUI || gui.frame == null) { + return; + } + SwingUtilities.invokeLater(() -> gui.frame.setVisible(false)); + } + + private void shutdown() { + I18nUpdateMod.shouldShutdown = true; + Log.info("User shutdown task"); + if (!useGUI) { + return; + } + hide(); + } + + public static void setStage(LoadStage stage) { + LoadDetailUI gui = getInstance(); + if (!gui.useGUI || gui.statusBar == null || gui.logArea == null) { + return; + } + SwingUtilities.invokeLater(() -> { + gui.statusBar.setString(LoadStage.getDescription(stage)); + gui.statusBar.setValue(stage.getValue()); + gui.logArea.append("当前阶段: " + LoadStage.getDescription(stage) + "\n"); + gui.logArea.setCaretPosition(gui.logArea.getDocument().getLength()); + }); + } + + public static void appendLog(String log) { + LoadDetailUI gui = getInstance(); + if (!gui.useGUI || gui.logArea == null) { + return; + } + SwingUtilities.invokeLater(() -> { + gui.logArea.append(log + "\n"); + gui.logArea.setCaretPosition(gui.logArea.getDocument().getLength()); + }); + } + + public static void autoClose(int delayTime){ + SwingWorker worker = new SwingWorker() { + @Override + protected Void doInBackground() throws Exception { + Thread.sleep(delayTime); + return null; + } + + @Override + protected void done() { + hide(); + } + }; + worker.execute(); + } +} \ No newline at end of file diff --git a/src/main/java/i18nupdatemod/core/ResourcePack.java b/src/main/java/i18nupdatemod/core/ResourcePack.java index 6c890e5..58d9ddd 100644 --- a/src/main/java/i18nupdatemod/core/ResourcePack.java +++ b/src/main/java/i18nupdatemod/core/ResourcePack.java @@ -1,5 +1,6 @@ package i18nupdatemod.core; +import i18nupdatemod.I18nUpdateMod; import i18nupdatemod.util.AssetUtil; import i18nupdatemod.util.DigestUtil; import i18nupdatemod.util.FileUtil; @@ -7,6 +8,7 @@ import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InterruptedIOException; import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Path; @@ -41,11 +43,14 @@ public ResourcePack(String filename, boolean saveToGame) { public void checkUpdate(String fileUrl, String md5Url) throws IOException, URISyntaxException, NoSuchAlgorithmException { if (isUpToDate(md5Url)) { + LoadDetailUI.appendLog(filename + " 无需更新"); Log.debug("Already up to date."); return; } //In this time, we can only download full file + LoadDetailUI.appendLog("正在下载 " + filename); downloadFull(fileUrl, md5Url); + LoadDetailUI.appendLog(filename + " 下载完成"); //In the future, we will download patch file and merge local file } @@ -75,17 +80,29 @@ private boolean checkMd5(Path localFile, String md5Url) throws IOException, URIS } private void downloadFull(String fileUrl, String md5Url) throws IOException { + if (I18nUpdateMod.shouldShutdown) { + throw new InterruptedIOException("Download cancelled by user"); + } + + Path downloadTmp = FileUtil.getTemporaryPath(filename + ".tmp"); try { - Path downloadTmp = FileUtil.getTemporaryPath(filename + ".tmp"); AssetUtil.download(fileUrl, downloadTmp); if (!checkMd5(downloadTmp, md5Url)) { throw new IOException("Download MD5 not match"); } Files.move(downloadTmp, tmpFilePath, StandardCopyOption.REPLACE_EXISTING); Log.debug(String.format("Updates temp file: %s", tmpFilePath)); + } catch (InterruptedIOException e) { + Files.deleteIfExists(downloadTmp); + throw e; } catch (Exception e) { Log.warning("Error while downloading: %s", e); } + + if (I18nUpdateMod.shouldShutdown) { + throw new InterruptedIOException("Download cancelled by user"); + } + if (!Files.exists(tmpFilePath)) { throw new FileNotFoundException("Tmp file not found."); } diff --git a/src/main/java/i18nupdatemod/core/ResourcePackConverter.java b/src/main/java/i18nupdatemod/core/ResourcePackConverter.java index 0a97712..9d4928e 100644 --- a/src/main/java/i18nupdatemod/core/ResourcePackConverter.java +++ b/src/main/java/i18nupdatemod/core/ResourcePackConverter.java @@ -40,6 +40,7 @@ public void convert(int packFormat, String description) throws Exception { // zos.setMethod(ZipOutputStream.STORED); for (Path p : sourcePath) { Log.info("Converting: " + p); + LoadDetailUI.appendLog("正在转换 " + p); try (ZipFile zf = new ZipFile(p.toFile(), StandardCharsets.UTF_8)) { for (Enumeration e = zf.entries(); e.hasMoreElements(); ) { ZipEntry ze = e.nextElement(); diff --git a/src/main/java/i18nupdatemod/entity/LoadStage.java b/src/main/java/i18nupdatemod/entity/LoadStage.java new file mode 100644 index 0000000..31bd883 --- /dev/null +++ b/src/main/java/i18nupdatemod/entity/LoadStage.java @@ -0,0 +1,36 @@ +package i18nupdatemod.entity; + +public enum LoadStage { + INIT(0), + DOWNLOAD_ASSET(1), + CONVERT_RESOURCE_PACK(2), + APPLY_RESOURCE_PACK(3), + FINISH(4); + + private final int value; + + LoadStage(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public static String getDescription(LoadStage stage) { + switch (stage) { + case INIT: + return "初始化"; + case DOWNLOAD_ASSET: + return "更新资源包"; + case CONVERT_RESOURCE_PACK: + return "转换资源包"; + case APPLY_RESOURCE_PACK: + return "应用资源包"; + case FINISH: + return "完成"; + default: + return "未知"; + } + } +} diff --git a/src/main/java/i18nupdatemod/util/AssetUtil.java b/src/main/java/i18nupdatemod/util/AssetUtil.java index d75c840..1511ebd 100644 --- a/src/main/java/i18nupdatemod/util/AssetUtil.java +++ b/src/main/java/i18nupdatemod/util/AssetUtil.java @@ -2,18 +2,23 @@ import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; -import org.apache.commons.io.FileUtils; +import i18nupdatemod.I18nUpdateMod; import org.apache.commons.io.IOUtils; import org.jetbrains.annotations.NotNull; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; +import java.io.InterruptedIOException; +import java.io.OutputStream; import java.lang.reflect.Type; import java.net.HttpURLConnection; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.net.SocketTimeoutException; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.HashMap; @@ -35,8 +40,36 @@ public class AssetUtil { public static void download(String url, Path localFile) throws IOException, URISyntaxException { Log.info("Downloading: %s -> %s", url, localFile); - FileUtils.copyURLToFile(new URI(url).toURL(), localFile.toFile(), - (int) TimeUnit.SECONDS.toMillis(3), (int) TimeUnit.SECONDS.toMillis(33)); + + HttpURLConnection connection = (HttpURLConnection) new URI(url).toURL().openConnection(); + connection.setRequestMethod("GET"); + connection.setConnectTimeout((int) TimeUnit.SECONDS.toMillis(3)); + connection.setReadTimeout((int) TimeUnit.SECONDS.toMillis(1)); + + try (InputStream input = connection.getInputStream(); + OutputStream output = Files.newOutputStream(localFile)) { + byte[] buffer = new byte[8192]; + while (true) { + if (I18nUpdateMod.shouldShutdown) { + throw new InterruptedIOException("Download cancelled by user"); + } + + int read; + try { + read = input.read(buffer); + } catch (SocketTimeoutException e) { + continue; + } + + if (read < 0) { + break; + } + output.write(buffer, 0, read); + } + } finally { + connection.disconnect(); + } + Log.debug("Downloaded: %s -> %s", url, localFile); } diff --git a/src/main/resources/icons/CFPA.png b/src/main/resources/icons/CFPA.png new file mode 100644 index 0000000..88d4fdb Binary files /dev/null and b/src/main/resources/icons/CFPA.png differ diff --git a/src/main/resources/icons/CFPA_with_title.png b/src/main/resources/icons/CFPA_with_title.png new file mode 100644 index 0000000..b46d3cc Binary files /dev/null and b/src/main/resources/icons/CFPA_with_title.png differ