diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java index 669b3ba3903..809513881d9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java @@ -39,6 +39,7 @@ import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.Lang; + import org.jackhuang.hmcl.util.PortablePath; import org.jackhuang.hmcl.util.function.ExceptionalConsumer; import org.jackhuang.hmcl.util.function.ExceptionalRunnable; @@ -46,9 +47,11 @@ import org.jackhuang.hmcl.util.i18n.LocalizedText; import org.jackhuang.hmcl.util.io.CompressingUtils; import org.jackhuang.hmcl.util.io.FileUtils; +import org.jackhuang.hmcl.util.io.IOUtils; import org.jetbrains.annotations.NotNullByDefault; import org.jetbrains.annotations.Nullable; +import java.io.Closeable; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.charset.Charset; @@ -124,6 +127,40 @@ public static Modpack readModpackManifest(Path file, Charset charset) throws Uns throw new UnsupportedModpackException(file.toString()); } + /// 存储解析启动器包装 ZIP 后的结果 + /// @param innerPath 包装文件系统内的整合包条目路径 + /// @param wrapperFs 包装文件系统;当不再需要 [innerPath] 时必须被关闭 + public record LauncherWrapper(Path innerPath, FileSystem wrapperFs) implements Closeable { + /// 关闭包装的 [FileSystem]。 + @Override + public void close() throws IOException { + wrapperFs.close(); + } + } + + /// 检测 [file] 是否为 HMCL 启动器包装 ZIP(其内部嵌入了实际的整合包 `modpack.zip` 或 `modpack.mrpack`) + /// 返回一个包含内部条目路径和包装文件系统的 [LauncherWrapper], + /// 如果 [file] 不是包装 ZIP,则返回 `null` + @Nullable + public static LauncherWrapper unwrapIfLauncherWrapper(Path file, Charset charset) { + FileSystem outerFs = null; + try { + outerFs = CompressingUtils.createReadOnlyZipFileSystem(file, charset); + for (String innerName : new String[]{"modpack.zip", "modpack.mrpack"}) { + Path entryPath = outerFs.getPath("/" + innerName); + if (Files.isRegularFile(entryPath)) { + LauncherWrapper result = new LauncherWrapper(entryPath, outerFs); + outerFs = null; + return result; + } + } + } catch (IOException ignored) { + } finally { + IOUtils.closeQuietly(outerFs); + } + return null; + } + public static Path findMinecraftDirectoryInManuallyCreatedModpack(String modpackName, FileSystem fs) throws IOException, UnsupportedModpackException { Path root = fs.getPath("/"); if (isMinecraftDirectory(root)) return root; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/LocalModpackPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/LocalModpackPage.java index c731a684d6d..ee78f3a81e6 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/LocalModpackPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/LocalModpackPage.java @@ -40,9 +40,14 @@ import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.io.CompressingUtils; import org.jackhuang.hmcl.util.io.FileUtils; +import org.jackhuang.hmcl.util.io.IOUtils; +import org.jetbrains.annotations.Nullable; import java.nio.charset.Charset; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; import java.nio.file.Path; +import java.util.concurrent.atomic.AtomicReference; import static org.jackhuang.hmcl.util.logging.Logger.LOG; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; @@ -53,6 +58,11 @@ public final class LocalModpackPage extends ModpackPage { private Modpack manifest = null; private Charset charset; + private final AtomicReference wrapperFsRef = new AtomicReference<>(); + @Nullable + private volatile Path resolvedModpackFile; + private volatile boolean cleanedUp; + public LocalModpackPage(WizardController controller) { super(controller); @@ -104,10 +114,32 @@ public LocalModpackPage(WizardController controller) { Task.supplyAsync(() -> CompressingUtils.findSuitableEncoding(selectedFile)) .thenApplyAsync(encoding -> { charset = encoding; - manifest = ModpackHelper.readModpackManifest(selectedFile, encoding); + Path actualFile = selectedFile; + if (selectedFile.getFileSystem() == FileSystems.getDefault()) { + var wrapper = ModpackHelper.unwrapIfLauncherWrapper(selectedFile, encoding); + if (wrapper != null) { + actualFile = wrapper.innerPath(); + resolvedModpackFile = actualFile; + wrapperFsRef.set(wrapper.wrapperFs()); + } + } + manifest = ModpackHelper.readModpackManifest(actualFile, encoding); return manifest; }) .whenComplete(Schedulers.javafx(), (manifest, exception) -> { + FileSystem fs = wrapperFsRef.getAndSet(null); + if (fs != null) { + if (exception != null || cleanedUp) { + IOUtils.closeQuietly(fs); + } else { + Path innerPath = resolvedModpackFile; + if (innerPath != null) { + controller.getSettings().put(MODPACK_FILE, innerPath); + } + controller.getSettings().put(MODPACK_WRAPPER_FS, fs); + } + } + if (exception instanceof ManuallyCreatedModpackException) { hideSpinner(); nameProperty.set(FileUtils.getName(selectedFile)); @@ -146,7 +178,12 @@ public LocalModpackPage(WizardController controller) { @Override public void cleanup(SettingsMap settings) { + cleanedUp = true; settings.remove(MODPACK_FILE); + // 同时从 AtomicReference(后台任务可能尚未转移) + // 和 settings(可能已被 whenComplete 转移)中关闭 FS。 + IOUtils.closeQuietly(wrapperFsRef.getAndSet(null)); + IOUtils.closeQuietly(settings.remove(MODPACK_WRAPPER_FS)); } protected void onInstall() { @@ -179,6 +216,7 @@ protected void onDescribe() { } public static final SettingsMap.Key MODPACK_FILE = new SettingsMap.Key<>("MODPACK_FILE"); + public static final SettingsMap.Key MODPACK_WRAPPER_FS = new SettingsMap.Key<>("MODPACK_WRAPPER_FS"); public static final SettingsMap.Key MODPACK_NAME = new SettingsMap.Key<>("MODPACK_NAME"); public static final SettingsMap.Key MODPACK_MANIFEST = new SettingsMap.Key<>("MODPACK_MANIFEST"); public static final SettingsMap.Key MODPACK_CHARSET = new SettingsMap.Key<>("MODPACK_CHARSET"); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackInstallWizardProvider.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackInstallWizardProvider.java index cc8fae2ba95..1b5b344ea69 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackInstallWizardProvider.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackInstallWizardProvider.java @@ -35,10 +35,13 @@ import org.jackhuang.hmcl.ui.wizard.WizardProvider; import org.jackhuang.hmcl.util.SettingsMap; import org.jackhuang.hmcl.util.StringUtils; +import org.jackhuang.hmcl.util.io.IOUtils; +import org.jetbrains.annotations.Nullable; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.charset.Charset; +import java.nio.file.FileSystem; import java.nio.file.Path; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; @@ -145,7 +148,19 @@ public Object finish(SettingsMap settings) { } }); - return finishModpackInstallingAsync(settings); + @Nullable FileSystem wrapperFs = settings.remove(LocalModpackPage.MODPACK_WRAPPER_FS); + try { + Task task = finishModpackInstallingAsync(settings); + if (task != null && wrapperFs != null) { + FileSystem fs = wrapperFs; + wrapperFs = null; + task = task.whenComplete(Schedulers.defaultScheduler(), + ignored -> IOUtils.closeQuietly(fs)); + } + return task; + } finally { + IOUtils.closeQuietly(wrapperFs); + } } private static Node createModpackInstallPage(WizardController controller) {