diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteAddonRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteAddonRepository.java index 2f5159ae25c..b7b95c3bad4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteAddonRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteAddonRepository.java @@ -59,7 +59,7 @@ public SearchResult search(DownloadProvider downloadProvider, String gameVersion int count = 0; for (ModTranslations.Mod mod : ModTranslations.getTranslationsByAddonType(getType()).searchMod(searchFilter)) { - String englishSearchFilter = String.join(" ", StringUtils.tokenize(StringUtils.isNotBlank(mod.getSubname()) ? mod.getSubname() : mod.getName())); + String englishSearchFilter = String.join(" ", StringUtils.tokenize(StringUtils.isNotBlank(mod.subname()) ? mod.subname() : mod.name())); if (StringUtils.isNotBlank(englishSearchFilter)) { englishSearchFiltersSet.add(englishSearchFilter); } @@ -92,7 +92,7 @@ public SearchResult search(DownloadProvider downloadProvider, String gameVersion } ModTranslations.Mod chineseTranslation = ModTranslations.getTranslationsByAddonType(getType()).getModByCurseForgeId(remoteAddon.slug()); - if (chineseTranslation != null && !StringUtils.isBlank(chineseTranslation.getName()) && StringUtils.containsChinese(chineseTranslation.getName())) { + if (chineseTranslation != null && !StringUtils.isBlank(chineseTranslation.name()) && StringUtils.containsChinese(chineseTranslation.name())) { searchResultArray[chineseIndex++] = remoteAddon; } else { searchResultArray[englishIndex--] = remoteAddon; @@ -104,10 +104,10 @@ public SearchResult search(DownloadProvider downloadProvider, String gameVersion StringUtils.LevCalculator levCalculator = new StringUtils.LevCalculator(); return new SearchResult(Stream.concat(Arrays.stream(searchResultArray, 0, chineseIndex).map(remoteMod -> { ModTranslations.Mod chineseRemoteMod = ModTranslations.getTranslationsByAddonType(getType()).getModByCurseForgeId(remoteMod.slug()); - if (chineseRemoteMod == null || StringUtils.isBlank(chineseRemoteMod.getName()) || !StringUtils.containsChinese(chineseRemoteMod.getName())) { + if (chineseRemoteMod == null || StringUtils.isBlank(chineseRemoteMod.name()) || !StringUtils.containsChinese(chineseRemoteMod.name())) { return Pair.pair(remoteMod, Integer.MAX_VALUE); } - String chineseRemoteModName = chineseRemoteMod.getName(); + String chineseRemoteModName = chineseRemoteMod.name(); if (searchFilter.isEmpty() || chineseRemoteModName.isEmpty()) { return Pair.pair(remoteMod, Math.max(searchFilter.length(), chineseRemoteModName.length())); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java index e96cc90cc2f..7ef829957f1 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java @@ -69,12 +69,6 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG; public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage { - public static final org.jackhuang.hmcl.ui.instances.DownloadPage.DownloadCallback FOR_MOD = - (downloadProvider, repository, version, mod, file) -> download(downloadProvider, repository, version, file, "mods"); - public static final org.jackhuang.hmcl.ui.instances.DownloadPage.DownloadCallback FOR_RESOURCE_PACK = - (downloadProvider, repository, version, pack, file) -> download(downloadProvider, repository, version, file, "resourcepacks"); - public static final org.jackhuang.hmcl.ui.instances.DownloadPage.DownloadCallback FOR_SHADER = - (downloadProvider, repository, version, shader, file) -> download(downloadProvider, repository, version, file, "shaderpacks"); private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(DecoratorPage.State.fromTitle(i18n("download"), -1)); private final TabHeader tab; @@ -93,12 +87,12 @@ public DownloadPage() { this(null); } - public DownloadPage(GameInstanceID uploadInstance) { + public DownloadPage(GameInstanceID updateInstance) { newGameTab.setNodeSupplier(loadVersionFor(() -> new VersionsPage(versionPageNavigator, i18n("install.installer.choose", i18n("install.installer.game")), "", DownloadProviders.getDownloadProvider(), GameComponentType.GAME, versionPageNavigator::onGameSelected))); modpackTab.setNodeSupplier(loadVersionFor(() -> { DownloadListPage page = HMCLLocalizedDownloadListPage.ofModPack((downloadProvider, repository, __, modpack, file) -> { - Instances.downloadModpackImpl(downloadProvider, repository, uploadInstance, modpack, file); + Instances.downloadModpackImpl(downloadProvider, repository, updateInstance, modpack, file); }, false); JFXButton installLocalModpackButton = FXUtils.newRaisedButton(i18n("install.modpack")); @@ -107,9 +101,9 @@ public DownloadPage(GameInstanceID uploadInstance) { page.getActions().add(installLocalModpackButton); return page; })); - modTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofMod(FOR_MOD, true))); - resourcePackTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofResourcePack(FOR_RESOURCE_PACK, true))); - shaderTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofShaderPack(FOR_SHADER, true))); + modTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofMod(true))); + resourcePackTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofResourcePack(true))); + shaderTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofShaderPack(true))); worldTab.setNodeSupplier(loadVersionFor(() -> new DownloadListPage(CurseForgeRemoteAddonRepository.WORLDS))); tab = new TabHeader(transitionPane, newGameTab, modpackTab, modTab, resourcePackTab, shaderTab, worldTab); @@ -124,7 +118,7 @@ public DownloadPage(GameInstanceID uploadInstance) { .startCategory(i18n("download.content").toUpperCase(Locale.ROOT)) .addNavigationDrawerTab(tab, modTab, i18n("mods"), SVG.EXTENSION, SVG.EXTENSION_FILL) .addNavigationDrawerTab(tab, resourcePackTab, i18n("resourcepack"), SVG.TEXTURE) - .addNavigationDrawerTab(tab, shaderTab, i18n("download.shader"), SVG.WB_SUNNY, SVG.WB_SUNNY_FILL) + .addNavigationDrawerTab(tab, shaderTab, i18n("shaderpack"), SVG.WB_SUNNY, SVG.WB_SUNNY_FILL) .addNavigationDrawerTab(tab, worldTab, i18n("world"), SVG.PUBLIC); FXUtils.setLimitWidth(sideBar, 200); setLeft(sideBar); @@ -217,14 +211,19 @@ public void showModpackDownloads() { tab.select(modpackTab, false); } + public DownloadListPage showModDownloads() { + tab.select(modTab, false); + return modTab.getNode(); + } + public DownloadListPage showResourcePackDownloads() { tab.select(resourcePackTab, false); return resourcePackTab.getNode(); } - public DownloadListPage showModDownloads() { - tab.select(modTab, false); - return modTab.getNode(); + public DownloadListPage showShaderDownloads() { + tab.select(shaderTab, false); + return shaderTab.getNode(); } public void showWorldDownloads() { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/AddonCheckUpdatesTask.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/AddonCheckUpdatesTask.java index 531bbaa94fb..9dff62c4635 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/AddonCheckUpdatesTask.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/AddonCheckUpdatesTask.java @@ -30,11 +30,11 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG; -public class AddonCheckUpdatesTask extends Task> { +public class AddonCheckUpdatesTask extends Task> { private final DownloadProvider downloadProvider; private final List> dependents; - public AddonCheckUpdatesTask(DownloadProvider downloadProvider, String gameVersion, Collection addons) { + public AddonCheckUpdatesTask(DownloadProvider downloadProvider, String gameVersion, Collection addons) { this.downloadProvider = downloadProvider; dependents = addons.stream().map(addon -> Task.supplyAsync(Schedulers.io(), () -> { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/AddonUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/AddonUpdatesPage.java index b48622eb98c..173a5abe994 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/AddonUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/AddonUpdatesPage.java @@ -62,15 +62,15 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; -public class AddonUpdatesPage extends BorderPane implements DecoratorPage { +public class AddonUpdatesPage extends BorderPane implements DecoratorPage { private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(DecoratorPage.State.fromTitle(i18n("addon.check_update"))); - private final LocalAddonManager localAddonManager; + private final Path addonsDirectory; private final ObservableList objects; @SuppressWarnings("unchecked") - public AddonUpdatesPage(LocalAddonManager localAddonManager, List updates) { - this.localAddonManager = localAddonManager; + public AddonUpdatesPage(Path addonsDirectory, List updates) { + this.addonsDirectory = addonsDirectory; getStyleClass().add("gray-background"); @@ -150,7 +150,7 @@ private void setupCellValueFactory(TableColumn column, private void updateFiles() { AddonUpdateTask task = new AddonUpdateTask( - localAddonManager.getDirectory(), + addonsDirectory, objects.stream() .filter(AddonUpdateObject::isEnabled) .map(AddonUpdateObject::getData) @@ -395,19 +395,18 @@ public static class AddonUpdateTask extends Task { RemoteAddon.Version remote = addon.targetVersion(); boolean isDisabled = local.isDisabled(); String originalFileName = local.getFile().getFileName().toString(); + String fileName = remote.file().filename(); + if (isDisabled) + fileName = StringUtils.addSuffix(fileName, LocalAddonManager.DISABLED_EXTENSION); + String newFileName = fileName; dependents.add(Task .runAsync(Schedulers.javafx(), () -> local.setOld(true)) .thenComposeAsync(() -> { - String fileName = addon.useRemoteFileName() ? remote.file().filename() : originalFileName; - if (isDisabled) - fileName = StringUtils.addSuffix(fileName, LocalAddonManager.DISABLED_EXTENSION); - var task = new FileDownloadTask( remote.file().url(), - addonDirectory.resolve(fileName) + addonDirectory.resolve(newFileName) ); - task.setName(remote.name()); return task; }) @@ -418,11 +417,14 @@ public static class AddonUpdateTask extends Task { if (isDisabled) local.markDisabled(); failedAddons.add(local); - } else if (!local.keepOldFiles()) { - try { - local.delete(); - } catch (IOException e) { - LOG.warning("Failed to delete outdated addon: " + local.getFile(), e); + } else { + local.onUpdated(newFileName); + if (!local.keepOldFiles()) { + try { + local.delete(); + } catch (IOException e) { + LOG.warning("Failed to delete outdated addon: " + local.getFile(), e); + } } } }) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DataPackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DataPackListPage.java index b533b4326bf..3f49b7e4ea3 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DataPackListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DataPackListPage.java @@ -22,7 +22,7 @@ import javafx.scene.control.Skin; import javafx.stage.FileChooser; import org.jackhuang.hmcl.game.World; -import org.jackhuang.hmcl.addon.datapack.DataPack; +import org.jackhuang.hmcl.addon.pack.datapack.DataPack; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.Controllers; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DataPackListPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DataPackListPageSkin.java index ae339417032..a420791e548 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DataPackListPageSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DataPackListPageSkin.java @@ -44,7 +44,7 @@ import javafx.scene.layout.Priority; import javafx.scene.layout.StackPane; import javafx.util.Duration; -import org.jackhuang.hmcl.addon.datapack.DataPack; +import org.jackhuang.hmcl.addon.pack.datapack.DataPack; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DownloadListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DownloadListPage.java index 11b12ff9f22..9e943d4f595 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DownloadListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DownloadListPage.java @@ -77,7 +77,7 @@ public class DownloadListPage extends Control implements DecoratorPage { private final ListProperty items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList()); private final ObservableList instances = FXCollections.observableArrayList(); private final ObjectProperty selectedInstance = new SimpleObjectProperty<>(); - private final DownloadPage.DownloadCallback callback; + private final DownloadPage.DownloadCallback callback; // ONLY FOR MODPACKS private boolean searchInitialized = false; protected final BooleanProperty supportChinese = new SimpleBooleanProperty(); private final ObservableList actions = FXCollections.observableArrayList(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DownloadPage.java index 386988de724..3bb7579f99c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DownloadPage.java @@ -60,6 +60,26 @@ public class DownloadPage extends Control implements DecoratorPage { + public static final DownloadCallback FOR_MOD = + (downloadProvider, repository, version, mod, file) -> + org.jackhuang.hmcl.ui.download.DownloadPage.download(downloadProvider, repository, version, file, "mods"); + public static final DownloadCallback FOR_RESOURCE_PACK = + (downloadProvider, repository, version, pack, file) -> + org.jackhuang.hmcl.ui.download.DownloadPage.download(downloadProvider, repository, version, file, "resourcepacks"); + public static final DownloadCallback FOR_SHADER = + (downloadProvider, repository, version, shader, file) -> + org.jackhuang.hmcl.ui.download.DownloadPage.download(downloadProvider, repository, version, file, "shaderpacks"); + + public static @Nullable DownloadCallback getDownloadCallbackFor(@Nullable RemoteAddon.Type addonType) { + if (addonType == null) return null; + return switch (addonType) { + case MOD -> FOR_MOD; + case RESOURCE_PACK -> FOR_RESOURCE_PACK; + case SHADER_PACK -> FOR_SHADER; + default -> null; + }; + } + private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(); private final BooleanProperty loaded = new SimpleBooleanProperty(false); private final BooleanProperty loading = new SimpleBooleanProperty(false); @@ -69,7 +89,7 @@ public class DownloadPage extends Control implements DecoratorPage { private final RemoteAddon addon; private final ModTranslations.Mod mod; private final HMCLGameInstance.Optional instanceReference; - private final DownloadCallback callback; + private final DownloadCallback callback; // ONLY FOR MODPACKS private final DownloadListPage page; private final RemoteAddon.Type type; @@ -157,11 +177,17 @@ public void setFailed(boolean failed) { this.failed.set(failed); } + public @Nullable DownloadCallback getCallbackFor(RemoteAddon addon) { + if (addon.type() == RemoteAddon.Type.MODPACK) return this.callback; + return getDownloadCallbackFor(addon.type()); + } + public void download(RemoteAddon addon, RemoteAddon.Version file) { - if (this.callback == null) { + var callback = getCallbackFor(addon); + if (callback == null) { saveAs(file); } else { - this.callback.download(page.getDownloadProvider(), instanceReference.repository(), instanceReference.instanceId(), addon, file); + callback.download(page.getDownloadProvider(), instanceReference.repository(), instanceReference.instanceId(), addon, file); } } @@ -386,20 +412,13 @@ private static final class DependencyAddonItem extends LineButton { pane.getChildren().setAll(imageView, content); FXUtils.setLimitHeight(this, 60); - RemoteAddon.Type type = addon.type(); - DownloadCallback callback = type == null ? null : switch (type) { - case MOD -> org.jackhuang.hmcl.ui.download.DownloadPage.FOR_MOD; - case RESOURCE_PACK -> org.jackhuang.hmcl.ui.download.DownloadPage.FOR_RESOURCE_PACK; - case SHADER_PACK -> org.jackhuang.hmcl.ui.download.DownloadPage.FOR_SHADER; - default -> null; // Dependencies should not be modpacks, worlds or customized stuff - }; setOnAction((e) -> { fireEvent(new DialogCloseEvent()); - Controllers.navigate(new DownloadPage(page, addon, instanceReference, callback)); + Controllers.navigate(new DownloadPage(page, addon, instanceReference, null)); }); setNode(IDX_LEADING, pane); - ModTranslations.Mod mod = ModTranslations.getTranslationsByAddonType(type).getModByCurseForgeId(addon.slug()); + ModTranslations.Mod mod = ModTranslations.getTranslationsByAddonType(addon.type()).getModByCurseForgeId(addon.slug()); content.setTitle(mod != null && I18n.isUseChinese() ? mod.getDisplayName() : addon.title()); content.setSubtitle(addon.description()); for (String category : addon.categories()) { @@ -483,7 +502,7 @@ private static final class AddonItem extends StackPane { } private static final class AddonVersion extends JFXDialogLayout { - public AddonVersion(RemoteAddon mod, RemoteAddon.Version version, DownloadPage selfPage) { + public AddonVersion(RemoteAddon addon, RemoteAddon.Version version, DownloadPage selfPage) { RemoteAddon.Type type = selfPage.type; String title = switch (type) { @@ -497,7 +516,7 @@ public AddonVersion(RemoteAddon mod, RemoteAddon.Version version, DownloadPage s VBox box = new VBox(8); box.setPadding(new Insets(8)); - var addonItem = new AddonItem(mod, version, selfPage); + var addonItem = new AddonItem(addon, version, selfPage); addonItem.setMouseTransparent(true); // Item is displayed for info, clicking shouldn't open the dialog again box.getChildren().setAll(addonItem); @@ -531,14 +550,14 @@ public AddonVersion(RemoteAddon mod, RemoteAddon.Version version, DownloadPage s this.setBody(box); JFXButton downloadButton = null; - if (selfPage.callback != null) { - downloadButton = new JFXButton(type == RemoteAddon.Type.MODPACK ? i18n("install.modpack") : i18n("mods.install")); + if (selfPage.getCallbackFor(addon) != null) { + downloadButton = new JFXButton(type == RemoteAddon.Type.MODPACK ? i18n("install.modpack") : i18n("addon.install")); downloadButton.getStyleClass().add("dialog-accept"); downloadButton.setOnAction(e -> { if (type == RemoteAddon.Type.MODPACK || !spinnerPane.isLoading() && spinnerPane.getFailedReason() == null) { fireEvent(new DialogCloseEvent()); } - selfPage.download(mod, version); + selfPage.download(addon, version); }); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameInstancePage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameInstancePage.java index aee7e753df2..9807234009b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameInstancePage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameInstancePage.java @@ -60,6 +60,7 @@ public class GameInstancePage extends DecoratorAnimatedPage implements Decorator private final TabHeader.Tab worldListTab = new TabHeader.Tab<>("worldList"); private final TabHeader.Tab schematicsTab = new TabHeader.Tab<>("schematicsTab"); private final TabHeader.Tab resourcePackTab = new TabHeader.Tab<>("resourcePackTab"); + private final TabHeader.Tab shaderListTab = new TabHeader.Tab<>("shaderListTab"); private final TransitionPane transitionPane = new TransitionPane(); private final BooleanProperty currentInstanceUpgradable = new SimpleBooleanProperty(); private final ObjectProperty instance = @@ -90,10 +91,11 @@ public GameInstancePage() { installerListTab.setNodeSupplier(() -> new InstallerListPage(instance)); modListTab.setNodeSupplier(() -> new ModListPage(instance)); resourcePackTab.setNodeSupplier(() -> new ResourcePackListPage(instance)); + shaderListTab.setNodeSupplier(() -> new ShaderPackListPage(instance)); worldListTab.setNodeSupplier(() -> new WorldListPage(instance)); schematicsTab.setNodeSupplier(() -> new SchematicsPage(instance)); - tab = new TabHeader(transitionPane, gameSettingsTab, installerListTab, modListTab, resourcePackTab, worldListTab, schematicsTab); + tab = new TabHeader(transitionPane, gameSettingsTab, installerListTab, modListTab, resourcePackTab, shaderListTab, worldListTab, schematicsTab); tab.select(gameSettingsTab); addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated); @@ -347,6 +349,7 @@ protected Skin(GameInstancePage control) { .addNavigationDrawerTab(control.tab, control.installerListTab, i18n("settings.tabs.installers"), SVG.DEPLOYED_CODE, SVG.DEPLOYED_CODE_FILL) .addNavigationDrawerTab(control.tab, control.modListTab, i18n("mods.manage"), SVG.EXTENSION, SVG.EXTENSION_FILL) .addNavigationDrawerTab(control.tab, control.resourcePackTab, i18n("resourcepack.manage"), SVG.TEXTURE) + .addNavigationDrawerTab(control.tab, control.shaderListTab, i18n("shaderpack.manage"), SVG.WB_SUNNY, SVG.WB_SUNNY_FILL) .addNavigationDrawerTab(control.tab, control.worldListTab, i18n("world.manage"), SVG.PUBLIC) .addNavigationDrawerTab(control.tab, control.schematicsTab, i18n("schematics.manage"), SVG.SCHEMA, SVG.SCHEMA_FILL); VBox.setVgrow(sideBar, Priority.ALWAYS); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/HMCLLocalizedDownloadListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/HMCLLocalizedDownloadListPage.java index 502deb93e9d..ff0578f3d6c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/HMCLLocalizedDownloadListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/HMCLLocalizedDownloadListPage.java @@ -31,20 +31,20 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG; public final class HMCLLocalizedDownloadListPage extends DownloadListPage { - public static DownloadListPage ofMod(DownloadPage.DownloadCallback callback, boolean instanceSelection) { - return new HMCLLocalizedDownloadListPage(callback, instanceSelection, RemoteAddon.Type.MOD, CurseForgeRemoteAddonRepository.MODS, ModrinthRemoteAddonRepository.MODS); + public static DownloadListPage ofMod(boolean instanceSelection) { + return new HMCLLocalizedDownloadListPage(null, instanceSelection, RemoteAddon.Type.MOD, CurseForgeRemoteAddonRepository.MODS, ModrinthRemoteAddonRepository.MODS); } public static DownloadListPage ofModPack(DownloadPage.DownloadCallback callback, boolean instanceSelection) { return new HMCLLocalizedDownloadListPage(callback, instanceSelection, RemoteAddon.Type.MODPACK, CurseForgeRemoteAddonRepository.MODPACKS, ModrinthRemoteAddonRepository.MODPACKS); } - public static DownloadListPage ofResourcePack(DownloadPage.DownloadCallback callback, boolean instanceSelection) { - return new HMCLLocalizedDownloadListPage(callback, instanceSelection, RemoteAddon.Type.RESOURCE_PACK, CurseForgeRemoteAddonRepository.RESOURCE_PACKS, ModrinthRemoteAddonRepository.RESOURCE_PACKS); + public static DownloadListPage ofResourcePack(boolean instanceSelection) { + return new HMCLLocalizedDownloadListPage(null, instanceSelection, RemoteAddon.Type.RESOURCE_PACK, CurseForgeRemoteAddonRepository.RESOURCE_PACKS, ModrinthRemoteAddonRepository.RESOURCE_PACKS); } - public static DownloadListPage ofShaderPack(DownloadPage.DownloadCallback callback, boolean instanceSelection) { - var page = new HMCLLocalizedDownloadListPage(callback, instanceSelection, RemoteAddon.Type.SHADER_PACK, CurseForgeRemoteAddonRepository.SHADERS, ModrinthRemoteAddonRepository.SHADER_PACKS); + public static DownloadListPage ofShaderPack(boolean instanceSelection) { + var page = new HMCLLocalizedDownloadListPage(null, instanceSelection, RemoteAddon.Type.SHADER_PACK, CurseForgeRemoteAddonRepository.SHADERS, ModrinthRemoteAddonRepository.SHADER_PACKS); page.supportChinese.set(false); return page; } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ModListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ModListPage.java index 8de0d08066b..0a26a766250 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ModListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ModListPage.java @@ -299,7 +299,7 @@ public void checkUpdates(Collection mods) { .composeAsync(() -> { GameVersionNumber version = gameInstance.getVersion(); return version != GameVersionNumber.unknown() - ? new AddonCheckUpdatesTask<>( + ? new AddonCheckUpdatesTask( DownloadProviders.getDownloadProvider(), version.toString(), mods) : null; }) @@ -310,7 +310,7 @@ public void checkUpdates(Collection mods) { } else if (result.isEmpty()) { Controllers.dialog(i18n("addon.check_update.empty")); } else { - Controllers.navigateForward(new AddonUpdatesPage<>(modManager, result)); + Controllers.navigateForward(new AddonUpdatesPage(modManager.getDirectory(), result)); } }) .withStagesHints("update.checking"), @@ -363,7 +363,7 @@ private static final class ModListPageSkin extends SkinBase { private final HBox toolbarNormal; private final HBox toolbarSelecting; - private final JFXListView listView; + private final JFXListView listView; /// Whether the search mechanism is currently active. private final BooleanProperty isSearching = new SimpleBooleanProperty(false); @@ -430,7 +430,7 @@ public ModListPageSkin(ModListPage skinnable) { createToolbarButton2(i18n("addon.check_update.button"), SVG.UPDATE, () -> skinnable.checkUpdates( listView.getItems().stream() - .map(ModListPage.ModInfoObject::getModInfo) + .map(ModInfoObject::getModInfo) .toList() ) ), @@ -464,7 +464,7 @@ public ModListPageSkin(ModListPage skinnable) { createToolbarButton2(i18n("addon.check_update.button"), SVG.UPDATE, () -> skinnable.checkUpdates( listView.getSelectionModel().getSelectedItems().stream() - .map(ModListPage.ModInfoObject::getModInfo) + .map(ModInfoObject::getModInfo) .toList() ) ), @@ -501,7 +501,7 @@ public ModListPageSkin(ModListPage skinnable) { ComponentList.setVgrow(center, Priority.ALWAYS); center.loadingProperty().bind(skinnable.loadingProperty()); - listView.setCellFactory(x -> new ModListPage.ModInfoListCell(listView, skinnable)); + listView.setCellFactory(x -> new ModInfoListCell(listView, skinnable)); listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); StackPane placeholderContainer = new StackPane(); @@ -521,17 +521,17 @@ public ModListPageSkin(ModListPage skinnable) { listView.setPlaceholder(placeholderContainer); Bindings.bindContent(listView.getItems(), skinnable.getItems()); - skinnable.getItems().addListener((ListChangeListener) c -> { + skinnable.getItems().addListener((ListChangeListener) c -> { if (isSearching.get()) { search(); } }); listView.setOnContextMenuRequested(event -> { - ModListPage.ModInfoObject selectedItem = listView.getSelectionModel().getSelectedItem(); + ModInfoObject selectedItem = listView.getSelectionModel().getSelectedItem(); if (selectedItem != null && listView.getSelectionModel().getSelectedItems().size() == 1) { listView.getSelectionModel().clearSelection(); - Controllers.dialog(new ModListPage.ModInfoDialog(selectedItem)); + Controllers.dialog(new ModInfoDialog(selectedItem)); } }); @@ -576,7 +576,7 @@ private void search() { } // Do we need to search in the background thread? - for (ModListPage.ModInfoObject item : getSkinnable().getItems()) { + for (ModInfoObject item : getSkinnable().getItems()) { LocalModFile modInfo = item.getModInfo(); if (predicate.test(modInfo.getFileName()) || predicate.test(modInfo.getName()) @@ -737,7 +737,7 @@ private static final class ModInfoDialog extends JFXDialogLayout { getActions().add(officialPageButton); } - if (modInfo.getModTranslations() == null || StringUtils.isBlank(modInfo.getModTranslations().getMcmod())) { + if (modInfo.getModTranslations() == null || StringUtils.isBlank(modInfo.getModTranslations().mcmod())) { JFXHyperlink searchButton = new JFXHyperlink(i18n("mods.mcmod.search")); searchButton.setExternalLink(NetworkUtils.withQuery("https://search.mcmod.cn/s", mapOf( pair("key", modInfo.getModInfo().getName()), @@ -826,7 +826,7 @@ protected void updateControl(ModInfoObject dataItem, boolean empty) { String displayName = modInfo.getName(); if (modTranslations != null && I18n.isUseChinese()) { - String chineseName = modTranslations.getName(); + String chineseName = modTranslations.name(); if (StringUtils.containsChinese(chineseName)) { if (StringUtils.containsEmoji(chineseName)) { StringBuilder builder = new StringBuilder(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ModTranslations.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ModTranslations.java index 790942d7cce..40dd4310324 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ModTranslations.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ModTranslations.java @@ -40,13 +40,13 @@ public enum ModTranslations { MOD("/assets/mod_data.txt") { @Override public String getMcmodUrl(Mod mod) { - return String.format("https://www.mcmod.cn/class/%s.html", mod.getMcmod()); + return String.format("https://www.mcmod.cn/class/%s.html", mod.mcmod()); } }, MODPACK("/assets/modpack_data.txt") { @Override public String getMcmodUrl(Mod mod) { - return String.format("https://www.mcmod.cn/modpack/%s.html", mod.getMcmod()); + return String.format("https://www.mcmod.cn/modpack/%s.html", mod.mcmod()); } }, EMPTY("") { @@ -119,7 +119,7 @@ private static String cleanSubname(String subname) { try (BufferedReader reader = new BufferedReader( new InputStreamReader( ModTranslations.class.getResourceAsStream(resourceName), StandardCharsets.UTF_8))) { - return this.mods = reader.lines().filter(line -> !line.startsWith("#")).map(Mod::new).toList(); + return this.mods = reader.lines().filter(line -> !line.startsWith("#")).map(Mod::fromLine).toList(); } catch (Exception e) { LOG.warning("Failed to load " + resourceName, e); return this.mods = List.of(); @@ -139,7 +139,7 @@ private static String cleanSubname(String subname) { List mods = getMods(); modIdMap = new HashMap<>(mods.size()); for (Mod mod : mods) { - for (String id : mod.getModIds()) { + for (String id : mod.modIds()) { if (StringUtils.isNotBlank(id)) { modIdMap.putIfAbsent(id, mod); } @@ -163,7 +163,7 @@ private static String cleanSubname(String subname) { List mods = getMods(); for (Mod mod : mods) { - String subname = cleanSubname(mod.getSubname()); + String subname = cleanSubname(mod.subname()); if (StringUtils.isNotBlank(subname)) { subnameMap.putIfAbsent(subname, mod); } @@ -186,8 +186,8 @@ private static String cleanSubname(String subname) { List mods = getMods(); curseForgeMap = new HashMap<>(mods.size()); for (Mod mod : mods) { - if (StringUtils.isNotBlank(mod.getCurseforge())) { - curseForgeMap.putIfAbsent(mod.getCurseforge(), mod); + if (StringUtils.isNotBlank(mod.curseforge())) { + curseForgeMap.putIfAbsent(mod.curseforge(), mod); } } @@ -210,17 +210,17 @@ private static String cleanSubname(String subname) { keywords = new ArrayList<>(); int maxKeywordLength = -1; for (Mod mod : mods) { - if (StringUtils.isNotBlank(mod.getName())) { - keywords.add(pair(mod.getName(), mod)); - maxKeywordLength = Math.max(maxKeywordLength, mod.getName().length()); + if (StringUtils.isNotBlank(mod.name())) { + keywords.add(pair(mod.name(), mod)); + maxKeywordLength = Math.max(maxKeywordLength, mod.name().length()); } - if (StringUtils.isNotBlank(mod.getSubname())) { - keywords.add(pair(mod.getSubname(), mod)); - maxKeywordLength = Math.max(maxKeywordLength, mod.getSubname().length()); + if (StringUtils.isNotBlank(mod.subname())) { + keywords.add(pair(mod.subname(), mod)); + maxKeywordLength = Math.max(maxKeywordLength, mod.subname().length()); } - if (StringUtils.isNotBlank(mod.getAbbr())) { - keywords.add(pair(mod.getAbbr(), mod)); - maxKeywordLength = Math.max(maxKeywordLength, mod.getAbbr().length()); + if (StringUtils.isNotBlank(mod.abbr())) { + keywords.add(pair(mod.abbr(), mod)); + maxKeywordLength = Math.max(maxKeywordLength, mod.abbr().length()); } } @@ -251,7 +251,7 @@ public Mod getMod(String id, String subname) { subname = cleanSubname(subname); if (StringUtils.isNotBlank(subname)) { Mod mod = getSubnameMap().get(subname); - if (mod != null && (StringUtils.isBlank(id) || mod.getModIds().contains(id))) + if (mod != null && (StringUtils.isBlank(id) || mod.modIds().contains(id))) return mod; } @@ -283,35 +283,14 @@ public List searchMod(String query) { .toList(); } - public static final class Mod { - private final String curseforge; - private final String mcmod; - private final List modIds; - private final String name; - private final String subname; - private final String abbr; - - public Mod(String line) { + public record Mod(String curseforge, String mcmod, List modIds, String name, String subname, String abbr) { + public static Mod fromLine(String line) { String[] items = line.split(";", -1); if (items.length != 6) { throw new IllegalArgumentException("Illegal mod data line, 6 items expected " + line); } - curseforge = items[0]; - mcmod = items[1]; - modIds = List.of(items[2].split(",")); - name = items[3]; - subname = items[4]; - abbr = items[5]; - } - - public Mod(String curseforge, String mcmod, List modIds, String name, String subname, String abbr) { - this.curseforge = curseforge; - this.mcmod = mcmod; - this.modIds = modIds; - this.name = name; - this.subname = subname; - this.abbr = abbr; + return new Mod(items[0], items[1], List.of(items[2].split(",")), items[3], items[4], items[5]); } public String getDisplayName() { @@ -325,29 +304,5 @@ public String getDisplayName() { } return builder.toString(); } - - public String getCurseforge() { - return curseforge; - } - - public String getMcmod() { - return mcmod; - } - - public List getModIds() { - return modIds; - } - - public String getName() { - return name; - } - - public String getSubname() { - return subname; - } - - public String getAbbr() { - return abbr; - } } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ResourcePackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ResourcePackListPage.java index 8440abc7e07..41b216cb0ce 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ResourcePackListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ResourcePackListPage.java @@ -42,8 +42,8 @@ import org.jackhuang.hmcl.addon.RemoteAddonRepository; import org.jackhuang.hmcl.addon.repository.CurseForgeRemoteAddonRepository; import org.jackhuang.hmcl.addon.repository.ModrinthRemoteAddonRepository; -import org.jackhuang.hmcl.addon.resourcepack.ResourcePackFile; -import org.jackhuang.hmcl.addon.resourcepack.ResourcePackManager; +import org.jackhuang.hmcl.addon.pack.resourcepack.ResourcePackFile; +import org.jackhuang.hmcl.addon.pack.resourcepack.ResourcePackManager; import org.jackhuang.hmcl.game.HMCLGameInstance; import org.jackhuang.hmcl.setting.DownloadProviders; import org.jackhuang.hmcl.setting.SettingsManager; @@ -175,7 +175,7 @@ public void addFiles(List files) { try { resourcePackManager.importResourcePack(file); } catch (Exception e) { - LOG.warning("Failed to add resource pack", e); + LOG.warning("Failed to add resource pack " + file, e); failures.add(file); } } @@ -192,10 +192,10 @@ public void addFiles(List files) { } - public void onAddFiles() { + private void onAddFiles() { FileChooser fileChooser = new FileChooser(); fileChooser.setTitle(i18n("resourcepack.add")); - fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("resourcepack"), "*.zip")); + fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.resourcepack"), "*.zip")); List files = Controllers.showOpenMultipleDialog(fileChooser); if (files != null && !files.isEmpty()) { addFiles(files); @@ -262,7 +262,7 @@ public void checkUpdates(Collection resourcePacks) { .composeAsync(() -> { GameVersionNumber version = gameInstance.getVersion(); return version != GameVersionNumber.unknown() - ? new AddonCheckUpdatesTask<>(DownloadProviders.getDownloadProvider(), + ? new AddonCheckUpdatesTask(DownloadProviders.getDownloadProvider(), version.toString(), resourcePacks) : null; }) @@ -272,7 +272,7 @@ public void checkUpdates(Collection resourcePacks) { } else if (result.isEmpty()) { Controllers.dialog(i18n("addon.check_update.empty")); } else { - Controllers.navigateForward(new AddonUpdatesPage<>(resourcePackManager, result)); + Controllers.navigateForward(new AddonUpdatesPage(resourcePackDirectory, result)); } }) .withStagesHints("update.checking"), @@ -408,7 +408,7 @@ private ResourcePackListPageSkin(ResourcePackListPage control) { ComponentList.setVgrow(center, Priority.ALWAYS); center.loadingProperty().bind(control.loadingProperty()); - listView.setCellFactory(x -> new ResourcePackListCell(listView, control)); + listView.setCellFactory(x -> new ResourcePackListCell(listView)); listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); StackPane placeholderContainer = new StackPane(); @@ -483,7 +483,7 @@ private void search() { LocalAddonFile.Description description = resourcePack.getDescription(); Stream descriptionParts = description == null ? Stream.empty() - : description.getParts().stream().map(LocalAddonFile.Description.Part::getText); + : description.parts().stream().map(LocalAddonFile.Description.Part::text); if (predicate.test(resourcePack.getFileNameWithExtension()) || predicate.test(resourcePack.getFileName()) || descriptionParts.anyMatch(predicate)) { @@ -522,9 +522,8 @@ private Image getDefaultIcon() { private Image loadIcon() { Image icon = file.loadIcon(); - if (icon != null && !icon.isError() && icon.getWidth() > 0 && icon.getHeight() > 0 && Math.abs(icon.getWidth() - icon.getHeight()) < 1) { + if (icon != null && !icon.isError() && icon.getWidth() > 0 && icon.getHeight() > 0 && Math.abs(icon.getWidth() - icon.getHeight()) < 1) return icon; - } return getDefaultIcon(); } } @@ -532,8 +531,6 @@ private Image loadIcon() { private static final class ResourcePackListCell extends MDListCell { private static final PseudoClass WARNING = PseudoClass.getPseudoClass("warning"); - private final ResourcePackListPage page; - private final JFXCheckBox checkBox; private final ImageContainer imageContainer = new ImageContainer(32); private final TwoLineListItem content = new TwoLineListItem(); @@ -542,9 +539,8 @@ private static final class ResourcePackListCell extends MDListCell listView, ResourcePackListPage page) { + public ResourcePackListCell(JFXListView listView) { super(listView); - this.page = page; getStyleClass().add("resource-pack-list-cell"); @@ -591,6 +587,7 @@ protected void updateControl(ResourcePackInfoObject item, boolean empty) { if (empty || item == null) return; ResourcePackFile file = item.getFile(); + item.iconCache.attachValue(imageContainer.imageProperty(), new WeakReference<>(itemProperty())); content.getTags().clear(); @@ -634,6 +631,7 @@ private static final class ResourcePackInfoDialog extends JFXDialogLayout { packInfoObject.iconCache.attachValue(imageContainer.imageProperty(), null); TwoLineListItem title = new TwoLineListItem(); + title.getTitleLabel().setWrapText(true); title.setTitle(pack.getFileName()); title.setSubtitle(pack.getFileNameWithExtension()); var compatibility = pack.getCompatibility(); @@ -668,7 +666,7 @@ private static final class ResourcePackInfoDialog extends JFXDialogLayout { RemoteAddonRepository repository = item.getValue(); JFXHyperlink button = new JFXHyperlink(i18n(item.getKey())); Task.runAsync(() -> { - Optional versionOptional = repository.getRemoteVersionByLocalFile(packInfoObject.getFile().getFile()); + Optional versionOptional = repository.getRemoteVersionByLocalFile(pack.getFile()); if (versionOptional.isPresent()) { RemoteAddon remoteAddon = repository.getAddonById(DownloadProviders.getDownloadProvider(), versionOptional.get().projectId()); FXUtils.runInFX(() -> { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ShaderPackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ShaderPackListPage.java new file mode 100644 index 00000000000..7049ecd7899 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ShaderPackListPage.java @@ -0,0 +1,555 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 org.jackhuang.hmcl.ui.instances; + +import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXDialogLayout; +import com.jfoenix.controls.JFXListView; +import com.jfoenix.controls.JFXTextField; +import javafx.animation.PauseTransition; +import javafx.application.Platform; +import javafx.beans.binding.Bindings; +import javafx.beans.value.ObservableValue; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.*; +import javafx.scene.image.Image; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.StackPane; +import javafx.stage.FileChooser; +import javafx.util.Duration; +import org.jackhuang.hmcl.addon.AddonLoader; +import org.jackhuang.hmcl.addon.RemoteAddon; +import org.jackhuang.hmcl.addon.RemoteAddonRepository; +import org.jackhuang.hmcl.addon.repository.CurseForgeRemoteAddonRepository; +import org.jackhuang.hmcl.addon.repository.ModrinthRemoteAddonRepository; +import org.jackhuang.hmcl.addon.shader.ShaderPackFile; +import org.jackhuang.hmcl.addon.shader.ShaderPackManager; +import org.jackhuang.hmcl.game.HMCLGameInstance; +import org.jackhuang.hmcl.setting.DownloadProviders; +import org.jackhuang.hmcl.task.Schedulers; +import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.ui.*; +import org.jackhuang.hmcl.ui.animation.ContainerAnimations; +import org.jackhuang.hmcl.ui.animation.TransitionPane; +import org.jackhuang.hmcl.ui.construct.*; +import org.jackhuang.hmcl.util.Pair; +import org.jackhuang.hmcl.util.StringUtils; +import org.jackhuang.hmcl.util.TaskCancellationAction; +import org.jackhuang.hmcl.util.i18n.I18n; +import org.jackhuang.hmcl.util.io.FileUtils; +import org.jackhuang.hmcl.util.javafx.ItemPropertyAsyncCache; +import org.jackhuang.hmcl.util.versioning.GameVersionNumber; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.nio.file.Path; +import java.util.*; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Predicate; + +import static org.jackhuang.hmcl.ui.FXUtils.ignoreEvent; +import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; +import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2; +import static org.jackhuang.hmcl.util.Pair.pair; +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +public class ShaderPackListPage extends ListPageBase { + + private final ReentrantLock lock = new ReentrantLock(); + + private final WeakListenerHolder listenerHolder = new WeakListenerHolder(); + private @Nullable HMCLGameInstance gameInstance; + + private Path shaderPackDir; + private ShaderPackManager shaderPackManager; + + public ShaderPackListPage(ObservableValue instanceContext) { + Objects.requireNonNull(instanceContext, "instanceContext"); + FXUtils.applyDragListener(this, ShaderPackFile::isFileShaderPack, this::addFiles); + + listenerHolder.add(FXUtils.onWeakChangeAndOperate(instanceContext, current -> { + if (current != null) { + loadInstance(current); + } + })); + } + + @Override + protected Skin createDefaultSkin() { + return new ShaderPackListPageSkin(this); + } + + public void loadInstance(HMCLGameInstance.Optional instance) { + this.gameInstance = instance.instance(); + if (gameInstance == null) { + this.shaderPackDir = null; + this.shaderPackManager = null; + getItems().clear(); + return; + } + + this.shaderPackDir = gameInstance.getShadersDirectory(); + this.shaderPackManager = gameInstance.getShaderPackManager(); + + refresh(); + } + + public void refresh() { + if (shaderPackManager == null) return; + setLoading(true); + Task.supplyAsync(Schedulers.io(), () -> { + lock.lock(); + try { + shaderPackManager.refresh(); + return shaderPackManager.getLocalFiles().stream().map(ShaderPackInfoObject::new).toList(); + } finally { + lock.unlock(); + } + }).whenComplete(Schedulers.javafx(), (result, exception) -> { + if (exception == null) { + getItems().setAll(result); + } else { + LOG.warning("Failed to load shader packs", exception); + getItems().clear(); + } + setLoading(false); + }).start(); + } + + public void addFiles(List files) { + if (shaderPackManager == null) return; + + List failures = new ArrayList<>(); + Task.runAsync(() -> { + for (Path file : files) { + try { + shaderPackManager.importShaderPack(file); + } catch (Exception e) { + LOG.warning("Failed to add shader pack " + file, e); + failures.add(file); + } + } + }).withRunAsync(Schedulers.javafx(), () -> { + if (!failures.isEmpty()) { + StringBuilder failure = new StringBuilder(i18n("shaderpack.add.failed")); + for (Path file: failures) { + failure.append("\n").append(file.toString()); + } + Controllers.dialog(failure.toString(), i18n("message.error"), MessageDialogPane.MessageType.ERROR); + } + refresh(); + }).start(); + } + + private void removeSelected(List selectedItems) { + try { + shaderPackManager.removeShaderPacks(selectedItems.stream().map(ShaderPackInfoObject::getFile).toList()); + } catch (IOException e) { + Controllers.dialog(i18n("shaderpack.delete.failed", e.getMessage()), i18n("message.error"), MessageDialogPane.MessageType.ERROR); + LOG.warning("Failed to delete shader packs", e); + } + refresh(); + } + + public void checkUpdates(Collection shaderPacks) { + HMCLGameInstance gameInstance = this.gameInstance; + if (gameInstance == null) return; + + Controllers.taskDialog( + Task.composeAsync(() -> { + GameVersionNumber version = gameInstance.getVersion(); + return version != GameVersionNumber.unknown() + ? new AddonCheckUpdatesTask(DownloadProviders.getDownloadProvider(), + version.toString(), shaderPacks) + : null; + }).whenComplete(Schedulers.javafx(), (result, exception) -> { + if (exception != null || result == null) { + Controllers.dialog(I18n.i18n("addon.check_update.failed_check"), I18n.i18n("message.failed"), MessageDialogPane.MessageType.ERROR); + } else if (result.isEmpty()) { + Controllers.dialog(I18n.i18n("addon.check_update.empty")); + } else { + Controllers.navigateForward(new AddonUpdatesPage(shaderPackDir, result)); + } + }).withStagesHints("update.checking"), + I18n.i18n("addon.check_update"), TaskCancellationAction.NORMAL); + } + + private void onAddFiles() { + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle(i18n("shaderpack.add")); + fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.shaderpack"), "*.zip")); + List files = FileUtils.toPaths(fileChooser.showOpenMultipleDialog(Controllers.getStage())); + if (files != null && !files.isEmpty()) { + addFiles(files); + } + } + + private void onDownload() { + if (gameInstance == null) return; + Controllers.getDownloadPage().showShaderDownloads().selectInstance(gameInstance.getId()); + Controllers.navigate(Controllers.getDownloadPage()); + } + + private void onOpenFolder() { + if (shaderPackDir != null) { + FXUtils.openFolder(shaderPackDir); + } + } + + public static final class ShaderPackInfoObject { + private final ShaderPackFile file; + + private final ItemPropertyAsyncCache iconCache; + + public ShaderPackInfoObject(ShaderPackFile file) { + this.file = file; + + this.iconCache = new ItemPropertyAsyncCache.Soft<>(this, this::loadIcon, this::getDefaultIcon); + } + + public ShaderPackFile getFile() { + return file; + } + + private Image getDefaultIcon() { + return switch (file.getShaderLoaderType()) { + case OPTIFINE_IRIS -> FXUtils.newBuiltinImage("/assets/img/opti-iris.png"); + default -> FXUtils.newBuiltinImage("/assets/img/unknown_pack.png"); + }; + } + + private Image loadIcon() { + Image icon = file.loadIcon(); + if (icon != null && !icon.isError() && icon.getWidth() > 0 && icon.getHeight() > 0 && Math.abs(icon.getWidth() - icon.getHeight()) < 1) + return icon; + return getDefaultIcon(); + } + } + + private static final class ShaderPackListPageSkin extends SkinBase { + private final JFXListView listView; + private final JFXTextField searchField = new JFXTextField(); + + private final TransitionPane toolbarPane = new TransitionPane(); + private final HBox searchBar = new HBox(); + private final HBox toolbarNormal = new HBox(); + private final HBox toolbarSelecting = new HBox(); + + private boolean isSearching; + + public ShaderPackListPageSkin(ShaderPackListPage control) { + super(control); + + StackPane pane = new StackPane(); + pane.setPadding(new Insets(10)); + pane.getStyleClass().addAll("notice-pane"); + + ComponentList root = new ComponentList(); + root.getStyleClass().add("no-padding"); + + listView = new JFXListView<>(); + + { + + // Toolbar Selecting + toolbarSelecting.getChildren().setAll( + createToolbarButton2(i18n("button.remove"), SVG.DELETE_FOREVER, () -> { + Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> + control.removeSelected(listView.getSelectionModel().getSelectedItems()), + null); + }), + createToolbarButton2(i18n("addon.check_update.button"), SVG.UPDATE, () -> + control.checkUpdates(listView.getSelectionModel().getSelectedItems().stream().map(ShaderPackInfoObject::getFile).toList()) + ), + createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () -> + listView.getSelectionModel().selectAll()), + createToolbarButton2(i18n("button.cancel"), SVG.CANCEL, () -> + listView.getSelectionModel().clearSelection()) + ); + + // Search Bar + searchBar.setAlignment(Pos.CENTER); + searchBar.setPadding(new Insets(0, 5, 0, 5)); + searchField.setPromptText(i18n("search")); + HBox.setHgrow(searchField, Priority.ALWAYS); + PauseTransition pause = new PauseTransition(Duration.millis(100)); + pause.setOnFinished(e -> search()); + FXUtils.onChange(searchField.textProperty(), newValue -> { + pause.setRate(1); + pause.playFromStart(); + }); + + JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE, + () -> { + changeToolbar(toolbarNormal); + + isSearching = false; + searchField.clear(); + Bindings.bindContent(listView.getItems(), getSkinnable().getItems()); + }); + + onEscPressed(searchField, closeSearchBar::fire); + + searchBar.getChildren().setAll(searchField, closeSearchBar); + + // Toolbar Normal + toolbarNormal.setAlignment(Pos.CENTER_LEFT); + toolbarNormal.setPickOnBounds(false); + toolbarNormal.getChildren().setAll( + createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, control::refresh), + createToolbarButton2(i18n("shaderpack.add"), SVG.ADD, control::onAddFiles), + createToolbarButton2(i18n("button.reveal_dir"), SVG.FOLDER_OPEN, control::onOpenFolder), + createToolbarButton2(i18n("addon.check_update.button"), SVG.UPDATE, () -> + control.checkUpdates(listView.getItems().stream().map(ShaderPackInfoObject::getFile).toList()) + ), + createToolbarButton2(i18n("download"), SVG.DOWNLOAD, control::onDownload), + createToolbarButton2(i18n("search"), SVG.SEARCH, () -> changeToolbar(searchBar)) + ); + + FXUtils.onChangeAndOperate(listView.getSelectionModel().selectedItemProperty(), + selectedItem -> { + if (selectedItem == null) + changeToolbar(isSearching ? searchBar : toolbarNormal); + else + changeToolbar(toolbarSelecting); + }); + FXUtils.setOverflowHidden(toolbarPane, 8); + root.getContent().add(toolbarPane); + + // Clear selection when pressing ESC + root.addEventHandler(KeyEvent.KEY_PRESSED, e -> { + if (e.getCode() == KeyCode.ESCAPE) { + if (listView.getSelectionModel().getSelectedItem() != null) { + listView.getSelectionModel().clearSelection(); + e.consume(); + } + } + }); + } + + { + SpinnerPane center = new SpinnerPane(); + ComponentList.setVgrow(center, Priority.ALWAYS); + center.loadingProperty().bind(control.loadingProperty()); + + listView.setCellFactory(x -> new ShaderPackListCell(listView)); + listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); + Bindings.bindContent(listView.getItems(), control.getItems()); + + listView.setOnContextMenuRequested(event -> { + ShaderPackInfoObject selectedItem = listView.getSelectionModel().getSelectedItem(); + if (selectedItem != null && listView.getSelectionModel().getSelectedItems().size() == 1) { + listView.getSelectionModel().clearSelection(); + Controllers.dialog(new ShaderPackInfoDialog(selectedItem)); + } + }); + + ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE); + listView.getStyleClass().add("no-horizontal-scrollbar"); + + center.setContent(listView); + root.getContent().add(center); + } + + pane.getChildren().setAll(root); + getChildren().setAll(pane); + } + + private void changeToolbar(HBox newToolbar) { + Node oldToolbar = toolbarPane.getCurrentNode(); + if (newToolbar != oldToolbar) { + toolbarPane.setContent(newToolbar, ContainerAnimations.FADE); + if (newToolbar == searchBar) { + Platform.runLater(searchField::requestFocus); + } + } + } + + private void search() { + isSearching = true; + + Bindings.unbindContent(listView.getItems(), getSkinnable().getItems()); + + String queryString = searchField.getText(); + if (StringUtils.isBlank(queryString)) { + listView.getItems().setAll(getSkinnable().getItems()); + } else { + listView.getItems().clear(); + + Predicate<@Nullable String> predicate; + try { + predicate = StringUtils.compileQuery(queryString); + } catch (Throwable e) { + LOG.warning("Illegal regular expression", e); + return; + } + + // Do we need to search in the background thread? + for (ShaderPackInfoObject item : getSkinnable().getItems()) { + var file = item.getFile(); + var meta = file.getMeta(); + if (predicate.test(file.getFile().getFileName().toString()) + || predicate.test(file.getName()) + || (meta != null && (predicate.test(meta.version()) || predicate.test(meta.description())))) { + listView.getItems().add(item); + } + } + } + } + + } + + private static final class ShaderPackListCell extends MDListCell { + + private final ImageContainer imageContainer = new ImageContainer(24); + private final TwoLineListItem content = new TwoLineListItem(); + private final JFXButton btnReveal = FXUtils.newToggleButton4(SVG.FOLDER); + private final JFXButton btnInfo = FXUtils.newToggleButton4(SVG.INFO); + + public ShaderPackListCell(JFXListView listView) { + super(listView); + + HBox root = new HBox(8); + root.setPickOnBounds(false); + root.setAlignment(Pos.CENTER_LEFT); + + HBox.setHgrow(content, Priority.ALWAYS); + content.setMouseTransparent(true); + + root.getChildren().setAll(imageContainer, content, btnReveal, btnInfo); + + setSelectable(); + + StackPane.setMargin(root, new Insets(8)); + getContainer().getChildren().add(root); + } + + @Override + protected void updateControl(ShaderPackInfoObject item, boolean empty) { + if (empty || item == null) return; + + ShaderPackFile file = item.file; + + item.iconCache.attachValue(imageContainer.imageProperty(), new WeakReference<>(itemProperty())); + + content.getTags().clear(); + content.setTitle(file.getFileName()); + content.setSubtitle(file.getMeta().description() == null ? "" : file.getMeta().description()); + content.addTag(switch (file.getShaderLoaderType()) { + case OPTIFINE_IRIS -> i18n("shaderpack.loader.optifine_iris"); + case APERTURE -> i18n("shaderpack.loader.aperture"); + }); + if (file.getMeta().version() != null) + content.addTag(file.getMeta().version()); + + FXUtils.installFastTooltip(btnReveal, i18n("reveal.in_file_manager")); + btnReveal.setOnAction(event -> FXUtils.showFileInExplorer(file.getFile())); + + btnInfo.setOnAction(e -> Controllers.dialog(new ShaderPackInfoDialog(item))); + } + } + + private static final class ShaderPackInfoDialog extends JFXDialogLayout { + + public ShaderPackInfoDialog(ShaderPackInfoObject infoObject) { + ShaderPackFile shaderPackFile = infoObject.file; + + HBox titleContainer = new HBox(); + titleContainer.setSpacing(8); + + maxWidthProperty().bind(Controllers.getDecorator().contentWidthProperty().multiply(0.7)); + + ImageContainer imageContainer = new ImageContainer(40); + infoObject.iconCache.attachValue(imageContainer.imageProperty(), null); + + TwoLineListItem title = new TwoLineListItem(); + title.getTitleLabel().setWrapText(true); + title.setTitle(shaderPackFile.getFileName()); + title.setSubtitle(shaderPackFile.getFile().getFileName().toString()); + title.addTag(switch (shaderPackFile.getShaderLoaderType()) { + case OPTIFINE_IRIS -> i18n("shaderpack.loader.optifine_iris"); + case APERTURE -> i18n("shaderpack.loader.aperture"); + }); + + titleContainer.getChildren().setAll(imageContainer, title); + setHeading(titleContainer); + + Label description = new Label(); + if (shaderPackFile.getMeta() != null && shaderPackFile.getMeta().description() != null) + description.setText(shaderPackFile.getMeta().description()); + description.setWrapText(true); + FXUtils.copyOnDoubleClick(description); + + ScrollPane descriptionPane = new ScrollPane(description); + FXUtils.smoothScrolling(descriptionPane); + descriptionPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + descriptionPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED); + descriptionPane.setFitToWidth(true); + FXUtils.onChange(description.heightProperty(), newVal -> { + double maxHeight = Controllers.getDecorator().contentHeightProperty().get() * 0.5; + double targetHeight = Math.min(newVal.doubleValue(), maxHeight); + descriptionPane.setPrefViewportHeight(targetHeight); + }); + + setBody(descriptionPane); + + for (Pair item : Arrays.asList( + pair("addon.curseforge", CurseForgeRemoteAddonRepository.SHADERS), + pair("addon.modrinth", ModrinthRemoteAddonRepository.SHADER_PACKS) + )) { + RemoteAddonRepository repository = item.getValue(); + JFXHyperlink button = new JFXHyperlink(i18n(item.getKey())); + Task.runAsync(() -> { + Optional versionOptional = repository.getRemoteVersionByLocalFile(shaderPackFile.getFile()); + if (versionOptional.isPresent()) { + RemoteAddon remoteAddon = repository.getAddonById(DownloadProviders.getDownloadProvider(), versionOptional.get().projectId()); + FXUtils.runInFX(() -> { + Set tags = new LinkedHashSet<>(); + for (AddonLoader loader : versionOptional.get().loaders()) { + String tag = I18n.translateLoaderName(loader); + if (tag != null) tags.add(tag); + } + title.addTagsIfNotExist(tags); + + button.setExternalLink(remoteAddon.pageUrl()); + button.setDisable(false); + }); + } + }).start(); + button.setDisable(true); + getActions().add(button); + } + + JFXButton okButton = new JFXButton(); + okButton.getStyleClass().add("dialog-accept"); + okButton.setText(i18n("button.ok")); + okButton.setOnAction(e -> fireEvent(new DialogCloseEvent())); + getActions().add(okButton); + + onEscPressed(this, okButton::fire); + } + + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/I18n.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/I18n.java index bcb3c192085..8bb84614bb8 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/I18n.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/I18n.java @@ -20,6 +20,7 @@ import org.jackhuang.hmcl.addon.AddonLoader; import org.jackhuang.hmcl.addon.AddonLoaderType; import org.jackhuang.hmcl.addon.mod.ModLoaderType; +import org.jackhuang.hmcl.addon.shader.ShaderLoaderType; import org.jackhuang.hmcl.download.ComponentRemoteVersion; import org.jackhuang.hmcl.download.game.GameRemoteVersion; import org.jackhuang.hmcl.util.StringUtils; @@ -140,6 +141,12 @@ public static String translateLoaderType(AddonLoaderType loaderType) { case LEGACY_FABRIC -> i18n("install.installer.legacyfabric"); default -> modLoaderType.displayName(); }; + } else if (loaderType instanceof ShaderLoaderType shaderLoaderType) { + return switch (shaderLoaderType) { + case OPTIFINE_IRIS -> i18n("shaderpack.loader.optifine_iris"); + case APERTURE -> i18n("shaderpack.loader.aperture"); + default -> shaderLoaderType.displayName(); + }; } return loaderType.displayName(); } diff --git a/HMCL/src/main/resources/assets/img/opti-iris.png b/HMCL/src/main/resources/assets/img/opti-iris.png new file mode 100644 index 00000000000..9df5d2349b3 Binary files /dev/null and b/HMCL/src/main/resources/assets/img/opti-iris.png differ diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 4e4a2493597..96621540558 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -189,6 +189,7 @@ addon.dependencies.has_broken=This version has broken dependencies. Please try a addon.download.recommend=Recommended Version for Minecraft %1s addon.download.title.release=Minecraft %s addon.download.title.snapshot=Minecraft %s (Snapshots) +addon.install=Install addon.modrinth=Modrinth addon.sort.date_created=Date Created addon.sort.last_updated=Last Updated @@ -381,7 +382,6 @@ datetime.format=MMM d, yyyy, h\:mm\:ss a download=Download download.code.404=File "%s" not found on the remote server. download.content=Addons -download.shader=Shaders download.failed=Failed to download "%1$s", response code: %2$d. download.failed.empty=No versions are available. Please click here to go back. download.failed.no_code=Failed to download @@ -429,6 +429,7 @@ extension.mod=Mod File extension.modloader.installer=Mod Loader Installer extension.resourcepack=Resource Pack Archive extension.schematic=Schematic File +extension.shaderpack=Shader Pack Archive extension.world=World Archive fatal.create_hmcl_current_directory_failure=HMCL cannot create the HMCL directory (%s). Please move HMCL to another location and reopen it. @@ -1181,7 +1182,6 @@ mods.restore=Restore mods.url=Official Page mods.update_modpack_mod.warning=Updating mods in a modpack can lead to irreparable results, possibly corrupting the modpack so that it cannot launch. Are you sure you want to update? mods.warning.loader_mismatch=Mod loader mismatch -mods.install=Install mods.save_as=Save As mods.unknown=Unknown Mod @@ -1640,7 +1640,14 @@ settings.type.global.preset.remove.confirm=Are you sure you want to delete prese settings.type.global.preset.subtitle=The current instance will inherit game settings from the specified preset settings.type.global.preset.auto_name=Preset %d +shaderpack=Shaders +shaderpack.add=Add +shaderpack.add.failed=Failed to add shader packs: +shaderpack.delete.failed=Failed to delete shader pack shaderpack.download.title=Download Shader - %1s +shaderpack.loader.aperture=Aperture +shaderpack.loader.optifine_iris=Optifine/Iris +shaderpack.manage=Shaders system.architecture=Architecture diff --git a/HMCL/src/main/resources/assets/lang/I18N_ar.properties b/HMCL/src/main/resources/assets/lang/I18N_ar.properties index 5111f7490b4..e9122e1a917 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ar.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ar.properties @@ -190,6 +190,7 @@ addon.curseforge=CurseForge # addon.download.recommend= # addon.download.title.release= # addon.download.title.snapshot= +addon.install=تثبيت addon.modrinth=Modrinth addon.sort.date_created=تاريخ الإنشاء addon.sort.last_updated=آخر تحديث @@ -383,7 +384,6 @@ datetime.format=MMM d, yyyy, h\:mm\:ss a download=تنزيل download.code.404=الملف "%s" غير موجود على الخادم البعيد. download.content=الإضافات -download.shader=الظلال download.failed=فشل تنزيل "%1$s"، رمز الاستجابة: %2$d. download.failed.empty=لا توجد إصدارات متاحة. انقر هنا للرجوع. download.failed.no_code=فشل التنزيل @@ -431,6 +431,7 @@ extension.mod=ملف مود extension.modloader.installer=مثبّت محمّل المودات extension.resourcepack=أرشيف حزمة الموارد extension.schematic=ملف مخطط +# extension.shaderpack= extension.world=أرشيف العالم fatal.create_hmcl_current_directory_failure=لا يستطيع HMCL إنشاء مجلد HMCL (%s). يُرجى نقل HMCL إلى موقع آخر وإعادة تشغيله. @@ -1181,7 +1182,6 @@ mods.restore=استعادة mods.url=الصفحة الرسمية mods.update_modpack_mod.warning=تحديث المودات في حزمة مودات قد يؤدي إلى نتائج لا يمكن إصلاحها، وقد يتسبب في تلف الحزمة بحيث لا تعمل. هل أنت متأكد من التحديث؟ mods.warning.loader_mismatch=عدم تطابق محمّل المودات -mods.install=تثبيت mods.save_as=حفظ باسم mods.unknown=مود غير معروف @@ -1637,7 +1637,14 @@ settings.type.global.manage=الإعدادات العامة # settings.type.global.preset.subtitle= # settings.type.global.preset.auto_name= +shaderpack=الظلال +# shaderpack.add= +# shaderpack.add.failed= +# shaderpack.delete.failed= shaderpack.download.title=تنزيل ظلال - %1s +# shaderpack.loader.aperture= +# shaderpack.loader.optifine_iris= +# shaderpack.manage= system.architecture=المعمارية diff --git a/HMCL/src/main/resources/assets/lang/I18N_de.properties b/HMCL/src/main/resources/assets/lang/I18N_de.properties index 6d474be5f7b..e2fdedc4f6a 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_de.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_de.properties @@ -188,6 +188,7 @@ addon.dependency.broken=Fehlerhafte Abhängigkeiten (Dieses Addon existierte vor addon.download.recommend=Empfohlene Version für Minecraft %1s addon.download.title.release=Minecraft %s addon.download.title.snapshot=Minecraft %s (Snapshots) +addon.install=Installieren addon.modrinth=Modrinth addon.sort.date_created=Erstellungsdatum addon.sort.last_updated=Zuletzt aktualisiert @@ -380,7 +381,6 @@ datetime.format=dd.MM.yyyy, HH\:mm\:ss download=Herunterladen download.code.404=Datei "%s" wurde nicht auf dem Remote-Server gefunden. download.content=Addons -download.shader=Shaderpakete download.failed=Fehler beim Herunterladen von "%1$s", Antwortcode: %2$d. download.failed.empty=Es sind keine Versionen verfügbar. Bitte klicken Sie hier, um zurückzukehren. download.failed.no_code=Herunterladen fehlgeschlagen @@ -428,6 +428,7 @@ extension.mod=Mod-Datei extension.modloader.installer=Modloader-Installationsprogramm extension.resourcepack=Ressourcenpaket-Archiv extension.schematic=Schematik-Datei +# extension.shaderpack= extension.world=Welt-Archiv fatal.create_hmcl_current_directory_failure=HMCL kann das HMCL-Verzeichnis (%s) nicht erstellen. Bitte verschieben Sie HMCL an einen anderen Speicherort und öffnen Sie es erneut. @@ -1180,7 +1181,6 @@ mods.restore=Wiederherstellen mods.url=Offizielle Seite mods.update_modpack_mod.warning=Das Aktualisieren von Mods in einem Modpack kann zu irreparablen Ergebnissen führen, wodurch das Modpack möglicherweise beschädigt wird, sodass es nicht mehr gestartet werden kann. Sind Sie sicher, dass Sie aktualisieren möchten? mods.warning.loader_mismatch=Modloader stimmt nicht überein -mods.install=Installieren mods.save_as=Speichern als mods.unknown=Unbekannte Mod @@ -1639,7 +1639,14 @@ settings.type.global.preset.remove.confirm=Möchten Sie die Voreinstellung "%s" settings.type.global.preset.subtitle=Die aktuelle Instanz erbt die Spieleinstellungen von der angegebenen Voreinstellung settings.type.global.preset.auto_name=Voreinstellung %d +# shaderpack= +# shaderpack.add= +# shaderpack.add.failed= +# shaderpack.delete.failed= shaderpack.download.title=Shaderpaket herunterladen - %1s +# shaderpack.loader.aperture= +# shaderpack.loader.optifine_iris= +# shaderpack.manage= system.architecture=Architektur diff --git a/HMCL/src/main/resources/assets/lang/I18N_es.properties b/HMCL/src/main/resources/assets/lang/I18N_es.properties index d61b585a426..8c6698ac646 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_es.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_es.properties @@ -188,6 +188,7 @@ addon.curseforge=CurseForge addon.download.recommend=Versión recomendada - Minecraft %1s # addon.download.title.release= # addon.download.title.snapshot= +# addon.install= addon.modrinth=Modrinth addon.sort.date_created=Fecha de creación addon.sort.last_updated=Última actualización @@ -381,7 +382,6 @@ datetime.format=d MMM yyyy, H\:mm\:ss download=Descargar download.code.404=Archivo no encontrado en el servidor remoto: %s download.content=Complementos -download.shader=Sombreadores download.failed=Falló la descarga de %1$s, código de respuesta: %2$d. download.failed.empty=No hay versiones disponibles, por favor haga clic aquí para volver. download.failed.no_code=No se ha podido descargar @@ -429,6 +429,7 @@ extension.mod=Archivo mod # extension.modloader.installer= # extension.resourcepack= # extension.schematic= +# extension.shaderpack= extension.world=Archivo del mundo fatal.create_hmcl_current_directory_failure=El lanzador no puede crear el directorio HMCL (%s). Por favor, mueva el lanzador a otra ubicación y vuelva a abrirlo. @@ -1173,7 +1174,6 @@ mods.restore=Restaurar mods.url=Página oficial mods.update_modpack_mod.warning=Actualizar mods en un modpack puede generar resultados irreparables, posiblemente corrompiendo el modpack para que no pueda iniciarse. ¿Seguro que quieres actualizar? # mods.warning.loader_mismatch= -mods.install=Instalar mods.save_as=Guardar como # mods.unknown= @@ -1629,7 +1629,14 @@ settings.type.global.manage=Config. Global # settings.type.global.preset.subtitle= # settings.type.global.preset.auto_name= +shaderpack=Sombreadores +# shaderpack.add= +# shaderpack.add.failed= +# shaderpack.delete.failed= # shaderpack.download.title= +# shaderpack.loader.aperture= +# shaderpack.loader.optifine_iris= +# shaderpack.manage= system.architecture=Arquitectura diff --git a/HMCL/src/main/resources/assets/lang/I18N_ja.properties b/HMCL/src/main/resources/assets/lang/I18N_ja.properties index 323189a2bd4..7ff302072ec 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ja.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ja.properties @@ -177,6 +177,7 @@ addon.curseforge=CurseForge # addon.download.recommend= # addon.download.title.release= # addon.download.title.snapshot= +# addon.install= addon.modrinth=Modrinth addon.sort.date_created=作成日 addon.sort.last_updated=最終更新 @@ -369,7 +370,6 @@ datetime.format=yyyy/MM/dd H:mm:ss download=ダウンロード download.code.404=リモートサーバーにファイルが見つかりません:%s download.content=ゲームコンテンツ -download.shader=シェーダー download.failed=%1$s のダウンロードに失敗しました、応答コード:%2$d download.failed.empty=候補者なし。戻るにはここをクリックしてください。 # download.failed.no_code= @@ -413,6 +413,7 @@ extension.mod=Modファイル # extension.modloader.installer= # extension.resourcepack= # extension.schematic= +# extension.shaderpack= extension.world=World zip # fatal.create_hmcl_current_directory_failure= @@ -981,7 +982,6 @@ mods.restore=Restore mods.url=公式ページ mods.update_modpack_mod.warning=modpack 内の mod を更新すると、修復不可能な結果につながる可能性があり、modpack が破損して起動できなくなる可能性があります。更新してもよろしいですか? # mods.warning.loader_mismatch= -# mods.install= # mods.save_as= # mods.unknown= @@ -1433,7 +1433,14 @@ settings.type.global.manage=グローバルゲーム設定 # settings.type.global.preset.subtitle= # settings.type.global.preset.auto_name= +shaderpack=シェーダー +# shaderpack.add= +# shaderpack.add.failed= +# shaderpack.delete.failed= # shaderpack.download.title= +# shaderpack.loader.aperture= +# shaderpack.loader.optifine_iris= +# shaderpack.manage= system.architecture=Arch diff --git a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties index 5d23c25050f..a6b1957d89d 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties @@ -188,6 +188,7 @@ addon.curseforge=CurseForge addon.download.recommend=宜版 - 礦藝 %1s # addon.download.title.release= # addon.download.title.snapshot= +addon.install=裝至是例 addon.modrinth=Modrinth addon.sort.date_created=創立之期 addon.sort.last_updated=近日迭更 @@ -380,7 +381,6 @@ datetime.format=yyyy 年 MM 月 dd 日 HH:mm:ss download=引 download.code.404=伺服器無案曰:%s\n君可求助於右上之鈕。 download.content=戲案 -download.shader=光影 download.failed=引之未成:%1$s,\n謬碼:%2$d\n君可求助於右上之鈕 download.failed.empty=[無可裝之版,點此返]\n(君可求助於右上之鈕) download.failed.no_code=引之未成 @@ -425,6 +425,7 @@ extension.mod=改囊案 # extension.modloader.installer= # extension.resourcepack= # extension.schematic= +# extension.shaderpack= extension.world=生界緊囊 fatal.create_hmcl_current_directory_failure=HMCL 不能立其案夾 (%s),宜稍遷而再啟之。\n君可求助於 https://docs.hmcl.net/help.html。 @@ -993,7 +994,6 @@ mods.restore=復 mods.url=改囊官網 mods.update_modpack_mod.warning=更迭改囊集之改囊或損之,以不能啟。行而不復,誠更迭乎? # mods.warning.loader_mismatch= -mods.install=裝至是例 mods.save_as=引至私案夾 # mods.unknown= @@ -1445,7 +1445,14 @@ settings.type.global.manage=全例戲設 # settings.type.global.preset.subtitle= # settings.type.global.preset.auto_name= +shaderpack=光影 +# shaderpack.add= +# shaderpack.add.failed= +# shaderpack.delete.failed= shaderpack.download.title=引光影 - %1s +# shaderpack.loader.aperture= +# shaderpack.loader.optifine_iris= +# shaderpack.manage= system.architecture=大構 diff --git a/HMCL/src/main/resources/assets/lang/I18N_ru.properties b/HMCL/src/main/resources/assets/lang/I18N_ru.properties index df6eca6224d..cd06895319f 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ru.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ru.properties @@ -184,6 +184,7 @@ addon.dependencies.has_broken=У этой версии есть сломанны addon.download.recommend=Рекомендуемая версия для Minecraft %1s addon.download.title.release=Minecraft %s addon.download.title.snapshot=Minecraft %s (снапшоты) +addon.install=Установить addon.modrinth=Modrinth addon.sort.date_created=Создан addon.sort.last_updated=Обновлён @@ -377,7 +378,6 @@ datetime.format=d MMM yyyy г., HH\:mm\:ss download=Скачать download.code.404=Файл «%s» не найден на удаленном сервере. download.content=Аддоны -download.shader=Шейдеры download.failed=Не удалось скачать «%1$s», код ответа: %2$d. download.failed.empty=Версий нет. Нажмите здесь, чтобы вернуться назад. download.failed.no_code=Не удалось скачать @@ -422,6 +422,7 @@ extension.mod=Файл мода extension.modloader.installer=Установщик загрузчика модов extension.resourcepack=Архив пакета ресурсов extension.schematic=Файл схемы +# extension.shaderpack= extension.world=Архив мира fatal.create_hmcl_current_directory_failure=Лаунчер не может создать папку HMCL (%s). Пожалуйста, переместите лаунчер в другое место и откройте его снова. @@ -1113,7 +1114,6 @@ mods.restore=Восстановить mods.url=Официальная страница mods.update_modpack_mod.warning=Обновление модов в модпаке может привести к непоправимым последствиям: модпак может повредиться и перестать запускаться. Вы уверены, что хотите обновить? mods.warning.loader_mismatch=Не тот загрузчик модов -mods.install=Установить mods.save_as=Сохранить как mods.unknown=Неизвестный мод @@ -1569,7 +1569,14 @@ settings.type.global.preset.remove.confirm=Удалить пресет «%s»? settings.type.global.preset.subtitle=Текущая сборка будет брать настройки игры из выбранного пресета settings.type.global.preset.auto_name=Пресет %d +shaderpack=Шейдеры +# shaderpack.add= +# shaderpack.add.failed= +# shaderpack.delete.failed= shaderpack.download.title=Скачать шейдер - %1s +# shaderpack.loader.aperture= +# shaderpack.loader.optifine_iris= +# shaderpack.manage= system.architecture=Архитектура diff --git a/HMCL/src/main/resources/assets/lang/I18N_uk.properties b/HMCL/src/main/resources/assets/lang/I18N_uk.properties index 4afbf45756f..ed9c712d206 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_uk.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_uk.properties @@ -190,6 +190,7 @@ addon.dependency.broken=Зламані залежності (Це доповне addon.download.recommend=Рекомендована версія для Minecraft %1s addon.download.title.release=Minecraft %s addon.download.title.snapshot=Minecraft %s (Снапшоти) +addon.install=Встановити addon.modrinth=Modrinth addon.sort.date_created=Дата створення addon.sort.last_updated=Останнє оновлення @@ -377,7 +378,6 @@ datetime.format=dd.MM.yyyy, HH:mm:ss download=Завантажити download.code.404=Файл "%s" не знайдено на віддаленому сервері. download.content=Додатки -download.shader=Шейдери download.failed=Не вдалося завантажити "%1$s", код відповіді: %2$d. download.failed.empty=Немає доступних версій. Натисніть тут, щоб повернутися. download.failed.no_code=Не вдалося завантажити @@ -424,6 +424,7 @@ extension.mod=Файл мода extension.modloader.installer=Встановлювач завантажувача модів extension.resourcepack=Архів пакету ресурсів extension.schematic=Файл схеми +# extension.shaderpack= extension.world=Архів світу fatal.create_hmcl_current_directory_failure=Hello Minecraft! Лаунчер не може створити каталог HMCL (%s). Перемістіть HMCL в інше місце та відкрийте його знову. @@ -1136,7 +1137,6 @@ mods.restore=Відновити mods.url=Офіційна сторінка mods.update_modpack_mod.warning=Оновлення модів у модпаку може призвести до непоправних наслідків, можливо, пошкодивши модпак так, що він не зможе запуститися. Ви впевнені, що хочете оновити? mods.warning.loader_mismatch=Невідповідність завантажувача модів -mods.install=Встановити mods.save_as=Зберегти як mods.unknown=Невідомий мод @@ -1592,7 +1592,14 @@ settings.type.global.preset.remove.confirm=Ви впевнені, що хоче settings.type.global.preset.subtitle=Поточний екземпляр успадковуватиме налаштування гри з вказаного пресету settings.type.global.preset.auto_name=Пресет %d +shaderpack=Шейдери +# shaderpack.add= +# shaderpack.add.failed= +# shaderpack.delete.failed= shaderpack.download.title=Завантаження шейдера - %1s +# shaderpack.loader.aperture= +# shaderpack.loader.optifine_iris= +# shaderpack.manage= system.architecture=Архітектура diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties index 797ed0852b1..3e40bfe7be1 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties @@ -189,6 +189,7 @@ addon.dependencies.has_broken=此版本存在损坏的前置内容。请尝试 addon.download.recommend=推荐版本 - Minecraft %1s addon.download.title.release=Minecraft %s addon.download.title.snapshot=Minecraft %s (快照) +addon.install=安装到当前实例 addon.modrinth=Modrinth addon.sort.date_created=创建日期 addon.sort.last_updated=最近更新 @@ -381,7 +382,6 @@ datetime.format=yyyy 年 MM 月 dd 日 HH:mm:ss download=下载 download.code.404=远程服务器不包含需要下载的文件: %s\n你可以点击右上角帮助按钮进行求助。 download.content=游戏内容 -download.shader=光影 download.failed=下载失败: %1$s,\n错误码:%2$d\n你可以点击右上角帮助按钮进行求助。 download.failed.empty=[没有可供安装的版本,点击此处返回]\n(你可以点击右上角帮助按钮进行求助) download.failed.no_code=下载失败 @@ -426,6 +426,7 @@ extension.mod=模组文件 extension.modloader.installer=模组加载器安装包 extension.resourcepack=资源包压缩包 extension.schematic=原理图文件 +extension.shaderpack=光影包压缩包 extension.world=世界压缩包 fatal.create_hmcl_current_directory_failure=HMCL 无法创建 HMCL 文件夹 (%s),请将 HMCL 移动至其他位置再启动。\n如遇到问题,你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。 @@ -997,7 +998,6 @@ mods.restore=回退 mods.url=官方页面 mods.update_modpack_mod.warning=更新整合包中的模组可能导致整合包损坏,使整合包无法正常启动。该操作不可逆,确定要更新吗? mods.warning.loader_mismatch=模组加载器不匹配 -mods.install=安装到当前实例 mods.save_as=下载到本地文件夹 mods.unknown=未知模组 @@ -1452,7 +1452,14 @@ settings.type.global.preset.remove.confirm=确定要删除预设“%s”吗? settings.type.global.preset.subtitle=当前实例将继承指定预设中的游戏设置 settings.type.global.preset.auto_name=预设 %d +shaderpack=光影 +shaderpack.add=添加光影包 +shaderpack.add.failed=添加光影包失败: +shaderpack.delete.failed=删除光影包失败 shaderpack.download.title=光影下载 - %1s +shaderpack.loader.aperture=Aperture +shaderpack.loader.optifine_iris=Optifine/Iris +shaderpack.manage=光影包管理 system.architecture=架构 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties index 3c3f3527142..0d255c7fb74 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties @@ -187,6 +187,7 @@ addon.dependencies.has_broken=此版本存在損壞的前置內容。請嘗試 addon.download.recommend=推薦版本 - Minecraft %1s addon.download.title.release=Minecraft %s addon.download.title.snapshot=Minecraft %s (快照) +addon.install=安裝到目前實例 addon.modrinth=Modrinth addon.sort.date_created=建立日期 addon.sort.last_updated=最近更新 @@ -379,7 +380,6 @@ datetime.format=yyyy 年 MM 月 dd 日 HH:mm:ss download=下載 download.code.404=遠端伺服器沒有需要下載的檔案:%s download.content=遊戲內容 -download.shader=光影 download.failed=下載失敗:%1$s\n錯誤碼:%2$d download.failed.empty=沒有可供安裝的版本,按一下此處返回。 download.failed.no_code=下載失敗 @@ -423,6 +423,7 @@ extension.mod=模組檔案 extension.modloader.installer=模組載入器安裝檔 extension.resourcepack=資源包壓縮檔 extension.schematic=原理圖檔案 +extension.shaderpack=光影包壓縮包 extension.world=世界壓縮檔 fatal.create_hmcl_current_directory_failure=HMCL 無法建立 HMCL 資料夾 (%s),請將 HMCL 移動至其他位置再開啟。 @@ -991,7 +992,6 @@ mods.restore=回退 mods.url=官方頁面 mods.update_modpack_mod.warning=更新模組包中的模組可能導致模組包損壞,使模組包無法正常啟動。該操作不可逆,確定要更新嗎? mods.warning.loader_mismatch=模組載入器不匹配 -mods.install=安裝到目前實例 mods.save_as=下載到本機目錄 mods.unknown=未知模組 @@ -1446,7 +1446,14 @@ settings.type.global.preset.remove.confirm=確定要刪除預設「%s」嗎? settings.type.global.preset.subtitle=當前實例將繼承指定預設中的遊戲設定 settings.type.global.preset.auto_name=預設 %d +shaderpack=光影 +shaderpack.add=新增光影包 +shaderpack.add.failed=新增光影包失敗: +shaderpack.delete.failed=刪除光影包失敗 shaderpack.download.title=光影下載 - %1s +shaderpack.loader.aperture=Aperture +shaderpack.loader.optifine_iris=Optifine/Iris +shaderpack.manage=光影包管理 system.architecture=架構 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/AddonLoader.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/AddonLoader.java index 6da37553d56..b4925bada6d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/AddonLoader.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/AddonLoader.java @@ -17,13 +17,13 @@ */ package org.jackhuang.hmcl.addon; -import org.jackhuang.hmcl.addon.mod.ModLoaderType; import org.jackhuang.hmcl.util.StringUtils; import org.jetbrains.annotations.NotNullByDefault; import org.jetbrains.annotations.Nullable; @NotNullByDefault public record AddonLoader(String name, @Nullable AddonLoaderType type) { + public static boolean mightBeLoader(String str) { if (StringUtils.isBlank(str) || !StringUtils.isASCII(str) @@ -38,15 +38,7 @@ public static boolean mightBeLoader(String str) { } public static AddonLoader of(String name) { - for (var type : ModLoaderType.values()) { - for (String loaderName : type.names()) { - if (name.equalsIgnoreCase(loaderName)) { - return new AddonLoader(name, type); - } - } - - } - return new AddonLoader(name, null); + return new AddonLoader(name, AddonLoaderType.of(name)); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/AddonLoaderType.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/AddonLoaderType.java index c144b20630a..db003a8ccbd 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/AddonLoaderType.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/AddonLoaderType.java @@ -17,15 +17,35 @@ */ package org.jackhuang.hmcl.addon; +import org.jackhuang.hmcl.addon.mod.ModLoaderType; +import org.jackhuang.hmcl.addon.shader.ShaderLoaderType; import org.jetbrains.annotations.NotNullByDefault; +import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; import java.util.Set; +import java.util.stream.Stream; /// For mods and shaders @NotNullByDefault public interface AddonLoaderType { + List VALUES = Stream.concat( + Arrays.stream(ModLoaderType.values()), + Arrays.stream(ShaderLoaderType.values()) + ).toList(); + + static @Nullable AddonLoaderType of(String name) { + for (var type : VALUES) { + if (type.names().contains(name.toLowerCase(Locale.ROOT))) + return type; + } + return null; + } + String displayName(); @Unmodifiable Set names(); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/LocalAddonFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/LocalAddonFile.java index 0537e4a4501..58b702cda09 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/LocalAddonFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/LocalAddonFile.java @@ -20,6 +20,7 @@ import org.jackhuang.hmcl.download.DownloadProvider; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.io.FileUtils; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNullByDefault; import org.jetbrains.annotations.Nullable; @@ -51,44 +52,38 @@ public boolean isDisabled() { public abstract boolean keepOldFiles(); - public abstract void delete() throws IOException; + public void delete() throws IOException { + FileUtils.forceDeleteIfExists(getFile()); + } @Nullable public AddonUpdate checkUpdates(DownloadProvider downloadProvider, String gameVersion, RemoteAddon.Source source) throws IOException { return null; } + public void onUpdated(String newFileNameWithExt) { + } + @NotNullByDefault public record AddonUpdate( RemoteAddon.Source source, RemoteAddon.Type repoType, LocalAddonFile localAddonFile, RemoteAddon.Version currentVersion, - RemoteAddon.Version targetVersion, - boolean useRemoteFileName + RemoteAddon.Version targetVersion ) { } - public static class Description { - private final List parts; - + public record Description(List parts) { public Description(String text) { - this.parts = new ArrayList<>(); - this.parts.add(new LocalAddonFile.Description.Part(text, "black")); - } - - public Description(List parts) { - this.parts = parts; - } - - public List getParts() { - return parts; + this(new ArrayList<>()); + this.parts.add(new Part(text, "black")); } @Override - public String toString() { + public @NotNull String toString() { StringBuilder builder = new StringBuilder(); - for (LocalAddonFile.Description.Part part : parts) { + for (Part part : parts) { builder.append(part.text); } return builder.toString(); @@ -98,25 +93,15 @@ public String toStringSingleLine() { return toString().lines().map(String::trim).filter(StringUtils::isNotBlank).collect(Collectors.joining(" | ")); } - public static class Part { - private final String text; - private final String color; + public record Part(String text, String color) { - public Part(String text) { - this(text, ""); - } - - public Part(String text, String color) { - this.text = Objects.requireNonNull(text); - this.color = Objects.requireNonNull(color); + public Part { + Objects.requireNonNull(text); + Objects.requireNonNull(color); } - public String getText() { - return text; - } - - public String getColor() { - return color; + public Part(String text) { + this(text, ""); } } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/LocalAddonManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/LocalAddonManager.java index 8a81e1fe356..3a23e36a817 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/LocalAddonManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/LocalAddonManager.java @@ -132,7 +132,7 @@ public Path setOld(T modFile, boolean old) throws IOException { } } - private Path backupFile(Path file) throws IOException { + public static Path backupFile(Path file) throws IOException { Path newPath = file.resolveSibling( StringUtils.addSuffix( StringUtils.removeSuffix(FileUtils.getName(file), DISABLED_EXTENSION), @@ -145,7 +145,7 @@ private Path backupFile(Path file) throws IOException { return newPath; } - private Path restoreFile(Path file) throws IOException { + public static Path restoreFile(Path file) throws IOException { Path newPath = file.resolveSibling( StringUtils.removeSuffix(FileUtils.getName(file), OLD_EXTENSION) ); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/LocalModFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/LocalModFile.java index ab364826864..d4daec3b172 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/LocalModFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/LocalModFile.java @@ -27,7 +27,6 @@ import org.jackhuang.hmcl.util.io.FileUtils; import java.io.IOException; -import java.nio.file.Files; import java.nio.file.Path; import java.util.*; @@ -189,11 +188,6 @@ public void markDisabled() throws IOException { file = modManager.disableMod(file); } - @Override - public void delete() throws IOException { - Files.deleteIfExists(file); - } - @Override public AddonUpdate checkUpdates(DownloadProvider downloadProvider, String gameVersion, RemoteAddon.Source source) throws IOException { RemoteAddonRepository repository = source.getRepoForType(RemoteAddon.Type.MOD); @@ -207,7 +201,7 @@ public AddonUpdate checkUpdates(DownloadProvider downloadProvider, String gameVe .sorted(Comparator.comparing(RemoteAddon.Version::datePublished).reversed()) .toList(); if (remoteVersions.isEmpty()) return null; - return new AddonUpdate(source, RemoteAddon.Type.MOD, this, currentVersion.get(), remoteVersions.get(0), true); + return new AddonUpdate(source, RemoteAddon.Type.MOD, this, currentVersion.get(), remoteVersions.get(0)); } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/ModManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/ModManager.java index e89abc3e38c..ab33353aa14 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/ModManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/ModManager.java @@ -20,7 +20,7 @@ import com.google.gson.JsonParseException; import org.jackhuang.hmcl.addon.LocalAddonFile; import org.jackhuang.hmcl.addon.LocalAddonManager; -import org.jackhuang.hmcl.addon.meta.*; +import org.jackhuang.hmcl.addon.mod.meta.*; import org.jackhuang.hmcl.game.DefaultGameInstance; import org.jackhuang.hmcl.game.GameComponentAnalyzer; import org.jackhuang.hmcl.game.GameComponentType; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/FabricModMetadata.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/meta/FabricModMetadata.java similarity index 98% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/FabricModMetadata.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/meta/FabricModMetadata.java index 4601ed1ca5d..e7098789f91 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/FabricModMetadata.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/meta/FabricModMetadata.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.addon.meta; +package org.jackhuang.hmcl.addon.mod.meta; import com.google.gson.*; import com.google.gson.annotations.JsonAdapter; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/ForgeNewModMetadata.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/meta/ForgeNewModMetadata.java similarity index 99% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/ForgeNewModMetadata.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/meta/ForgeNewModMetadata.java index 6b7ff0085b4..e86c1ca0df7 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/ForgeNewModMetadata.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/meta/ForgeNewModMetadata.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.addon.meta; +package org.jackhuang.hmcl.addon.mod.meta; import com.google.gson.JsonArray; import com.google.gson.JsonDeserializationContext; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/ForgeOldModMetadata.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/meta/ForgeOldModMetadata.java similarity index 99% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/ForgeOldModMetadata.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/meta/ForgeOldModMetadata.java index 81f358c94e6..a49e7926351 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/ForgeOldModMetadata.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/meta/ForgeOldModMetadata.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.addon.meta; +package org.jackhuang.hmcl.addon.mod.meta; import com.google.gson.JsonParseException; import com.google.gson.annotations.SerializedName; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/ForgeOldModMetadataLst.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/meta/ForgeOldModMetadataLst.java similarity index 95% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/ForgeOldModMetadataLst.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/meta/ForgeOldModMetadataLst.java index 25909ac0ccd..8d1a1308344 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/ForgeOldModMetadataLst.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/meta/ForgeOldModMetadataLst.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.addon.meta; +package org.jackhuang.hmcl.addon.mod.meta; import org.jackhuang.hmcl.util.gson.JsonSerializable; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/LiteModMetadata.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/meta/LiteModMetadata.java similarity index 98% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/LiteModMetadata.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/meta/LiteModMetadata.java index aea8c4fab2e..dea7f306a1a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/LiteModMetadata.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/meta/LiteModMetadata.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.addon.meta; +package org.jackhuang.hmcl.addon.mod.meta; import com.google.gson.JsonParseException; import kala.compress.archivers.zip.ZipArchiveEntry; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/QuiltModMetadata.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/meta/QuiltModMetadata.java similarity index 98% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/QuiltModMetadata.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/meta/QuiltModMetadata.java index 02e31ed7269..775acbf0f4b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/QuiltModMetadata.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/meta/QuiltModMetadata.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.addon.meta; +package org.jackhuang.hmcl.addon.mod.meta; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/PackMcMeta.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/pack/PackMcMeta.java similarity index 97% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/PackMcMeta.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/addon/pack/PackMcMeta.java index 3cc4d34e43c..76507488c9f 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/meta/PackMcMeta.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/pack/PackMcMeta.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.addon.meta; +package org.jackhuang.hmcl.addon.pack; import com.google.gson.*; import com.google.gson.annotations.JsonAdapter; @@ -40,10 +40,8 @@ @JsonSerializable public record PackMcMeta(@SerializedName("pack") PackInfo pack) implements Validation { - private static final Gson LENIENT_GSON = JsonUtils.defaultGsonBuilder().setStrictness(Strictness.LENIENT).create(); - public static PackMcMeta fromNonNullJson(String jsonString) throws JsonParseException { - PackMcMeta parsed = LENIENT_GSON.fromJson(jsonString, PackMcMeta.class); + PackMcMeta parsed = JsonUtils.LENIENT_GSON.fromJson(jsonString, PackMcMeta.class); if (parsed == null) throw new JsonParseException("Json object cannot be null."); return parsed; @@ -51,7 +49,7 @@ public static PackMcMeta fromNonNullJson(String jsonString) throws JsonParseExce public static PackMcMeta fromNonNullJsonFile(Path jsonFile) throws JsonParseException, IOException { try (var reader = Files.newBufferedReader(jsonFile)) { - PackMcMeta parsed = LENIENT_GSON.fromJson(reader, PackMcMeta.class); + PackMcMeta parsed = JsonUtils.LENIENT_GSON.fromJson(reader, PackMcMeta.class); if (parsed == null) throw new JsonParseException("Json object cannot be null."); return parsed; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/datapack/DataPack.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/pack/datapack/DataPack.java similarity index 99% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/addon/datapack/DataPack.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/addon/pack/datapack/DataPack.java index 6c15efbe74d..593cd0d4f16 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/datapack/DataPack.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/pack/datapack/DataPack.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.addon.datapack; +package org.jackhuang.hmcl.addon.pack.datapack; import com.google.gson.JsonParseException; import javafx.application.Platform; @@ -24,7 +24,7 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableList; import org.jackhuang.hmcl.addon.LocalAddonFile; -import org.jackhuang.hmcl.addon.meta.PackMcMeta; +import org.jackhuang.hmcl.addon.pack.PackMcMeta; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.io.CompressingUtils; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/resourcepack/ResourcePackFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/pack/resourcepack/ResourcePackFile.java similarity index 77% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/addon/resourcepack/ResourcePackFile.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/addon/pack/resourcepack/ResourcePackFile.java index 9f2ec2657a8..9af1033102c 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/resourcepack/ResourcePackFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/pack/resourcepack/ResourcePackFile.java @@ -15,14 +15,14 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.addon.resourcepack; +package org.jackhuang.hmcl.addon.pack.resourcepack; import javafx.scene.image.Image; import org.jackhuang.hmcl.addon.LocalAddonFile; -import org.jackhuang.hmcl.addon.meta.PackMcMeta; +import org.jackhuang.hmcl.addon.pack.PackMcMeta; import org.jackhuang.hmcl.util.StringUtils; +import org.jackhuang.hmcl.util.io.CompressingUtils; import org.jackhuang.hmcl.util.io.FileUtils; -import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -32,31 +32,42 @@ import java.util.List; import java.util.Locale; +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + public sealed abstract class ResourcePackFile extends LocalAddonFile implements Comparable permits ResourcePackFolder, ResourcePackZipFile { - static ResourcePackFile fromFile(ResourcePackManager manager, Path path) throws IOException { - if (isFileResourcePack(path)) { - return Files.isRegularFile(path) ? new ResourcePackZipFile(manager, path) : new ResourcePackFolder(manager, path); - } - return null; + + static @Nullable ResourcePackFile fromFile(ResourcePackManager manager, Path path) { + if (!isFileResourcePack(path)) return null; + return Files.isRegularFile(path) ? ResourcePackZipFile.load(manager, path) : ResourcePackFolder.load(manager, path); } public static boolean isFileResourcePack(Path file) { if (Files.isDirectory(file)) return Files.isRegularFile(file.resolve("pack.mcmeta")); - if (Files.isRegularFile(file)) return file.toString().toLowerCase(Locale.ROOT).endsWith(".zip"); + if (Files.isRegularFile(file) && file.toString().toLowerCase(Locale.ROOT).endsWith(".zip")) { + try (var zipFileTree = CompressingUtils.openZipFile(file)) { + return zipFileTree.getEntry("pack.mcmeta") != null; + } catch (IOException e) { + LOG.warning("Failed to check if file is resource pack", e); + } + } return false; } protected final ResourcePackManager manager; protected Path file; + protected final PackMcMeta.PackInfo packInfo; + protected final String fileName; protected final String fileNameWithExtension; private Compatibility compatibility = null; - protected ResourcePackFile(ResourcePackManager manager, Path file) { + protected ResourcePackFile(ResourcePackManager manager, Path file, @Nullable PackMcMeta.PackInfo packInfo) { super(); this.manager = manager; this.file = file; + this.packInfo = packInfo; + this.fileName = StringUtils.parseColorEscapes(FileUtils.getNameWithoutExtension(file)); this.fileNameWithExtension = file.getFileName().toString(); } @@ -112,9 +123,9 @@ public final boolean keepOldFiles() { public void markDisabled() { } - @Nullable - @Contract(pure = true) - public abstract PackMcMeta.PackInfo getPackInfo(); + public @Nullable PackMcMeta.PackInfo getPackInfo() { + return packInfo; + } @Nullable public LocalAddonFile.Description getDescription() { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/resourcepack/ResourcePackFolder.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/pack/resourcepack/ResourcePackFolder.java similarity index 77% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/addon/resourcepack/ResourcePackFolder.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/addon/pack/resourcepack/ResourcePackFolder.java index 5886f5cb57f..e2cdedbfbe8 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/resourcepack/ResourcePackFolder.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/pack/resourcepack/ResourcePackFolder.java @@ -15,38 +15,33 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.addon.resourcepack; +package org.jackhuang.hmcl.addon.pack.resourcepack; import javafx.scene.image.Image; -import org.jackhuang.hmcl.addon.meta.PackMcMeta; -import org.jackhuang.hmcl.util.io.FileUtils; +import org.jackhuang.hmcl.addon.pack.PackMcMeta; import org.jetbrains.annotations.Nullable; import java.io.ByteArrayInputStream; -import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import static org.jackhuang.hmcl.util.logging.Logger.LOG; final class ResourcePackFolder extends ResourcePackFile { - private final PackMcMeta.PackInfo info; - - public ResourcePackFolder(ResourcePackManager manager, Path path) { - super(manager, path); + public static ResourcePackFolder load(ResourcePackManager manager, Path path) { PackMcMeta meta = null; try { meta = PackMcMeta.fromNonNullJsonFile(path.resolve("pack.mcmeta")); } catch (Exception e) { LOG.warning("Failed to parse resource pack meta", e); } - this.info = meta != null ? meta.pack() : null; + + return new ResourcePackFolder(manager, path, meta != null ? meta.pack() : null); } - @Override - public PackMcMeta.PackInfo getPackInfo() { - return info; + private ResourcePackFolder(ResourcePackManager manager, Path path, PackMcMeta.PackInfo info) { + super(manager, path, info); } @Override @@ -67,10 +62,4 @@ public PackMcMeta.PackInfo getPackInfo() { } return null; } - - @Override - public void delete() throws IOException { - FileUtils.deleteDirectory(file); - } - } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/resourcepack/ResourcePackManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/pack/resourcepack/ResourcePackManager.java similarity index 94% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/addon/resourcepack/ResourcePackManager.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/addon/pack/resourcepack/ResourcePackManager.java index 69d1477f451..3bfa47854c4 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/resourcepack/ResourcePackManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/pack/resourcepack/ResourcePackManager.java @@ -15,13 +15,13 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.addon.resourcepack; +package org.jackhuang.hmcl.addon.pack.resourcepack; import com.google.gson.annotations.SerializedName; import kala.encdet.EncodingDetector; import org.jackhuang.hmcl.addon.LocalAddonManager; +import org.jackhuang.hmcl.addon.pack.PackMcMeta; import org.jackhuang.hmcl.game.DefaultGameInstance; -import org.jackhuang.hmcl.addon.meta.PackMcMeta; import org.jackhuang.hmcl.util.Pair; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.gson.JsonSerializable; @@ -364,11 +364,7 @@ public void importResourcePack(Path file) throws IOException, IllegalArgumentExc Files.createDirectories(resourcePackDirectory); Path newFile = resourcePackDirectory.resolve(file.getFileName()); - if (Files.isDirectory(file)) { - FileUtils.copyDirectory(file, newFile); - } else { - FileUtils.copyFile(file, newFile); - } + FileUtils.copyTo(file, resourcePackDirectory); addResourcePackInfo(newFile); } else { @@ -521,6 +517,37 @@ public Stream> arePacksEnabled(Stream options = loadOptions(); + List optPacks = new ArrayList<>(deserializePackList(options.get("resourcePacks"))); + List optIncompatiblePacks = new ArrayList<>(deserializePackList(options.get("incompatibleResourcePacks"))); + for (var l : List.of(optPacks, optIncompatiblePacks)) { + var it = l.listIterator(); + while (it.hasNext()) { + var next = it.next(); + if (oldName.equals(next) || oldNameNewFormat.equals(next)) { + it.set(supportsNewOptionsFormat ? newNameNewFormat : newName); + modified = true; + } + } + } + if (modified) { + options.put("resourcePacks", serializePackList(optPacks)); + options.put("incompatibleResourcePacks", serializePackList(optIncompatiblePacks)); + saveOptions(options); + } + return modified; + } finally { + lock.unlock(); + } + } + public ResourcePackFile.Compatibility getCompatibility(@NotNull ResourcePackFile resourcePack) { if (resourcePack.getPackInfo() == null) return ResourcePackFile.Compatibility.MISSING_PACK_META; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/resourcepack/ResourcePackZipFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/pack/resourcepack/ResourcePackZipFile.java similarity index 79% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/addon/resourcepack/ResourcePackZipFile.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/addon/pack/resourcepack/ResourcePackZipFile.java index 4b900350e1f..9cddc679508 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/resourcepack/ResourcePackZipFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/pack/resourcepack/ResourcePackZipFile.java @@ -15,19 +15,18 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.addon.resourcepack; +package org.jackhuang.hmcl.addon.pack.resourcepack; import javafx.scene.image.Image; import org.jackhuang.hmcl.download.DownloadProvider; import org.jackhuang.hmcl.addon.RemoteAddon; import org.jackhuang.hmcl.addon.RemoteAddonRepository; -import org.jackhuang.hmcl.addon.meta.PackMcMeta; +import org.jackhuang.hmcl.addon.pack.PackMcMeta; import org.jackhuang.hmcl.util.io.CompressingUtils; import org.jetbrains.annotations.Nullable; import java.io.ByteArrayInputStream; import java.io.IOException; -import java.nio.file.Files; import java.nio.file.Path; import java.util.Comparator; import java.util.List; @@ -36,25 +35,20 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG; final class ResourcePackZipFile extends ResourcePackFile { - private final PackMcMeta.PackInfo info; - - public ResourcePackZipFile(ResourcePackManager manager, Path path) throws IOException { - super(manager, path); + public static ResourcePackZipFile load(ResourcePackManager manager, Path path) { PackMcMeta meta = null; try (var zipFileTree = CompressingUtils.openZipTree(path)) { - try { - meta = PackMcMeta.fromNonNullJson(zipFileTree.readTextEntry("/pack.mcmeta")); - } catch (Exception e) { - LOG.warning("Failed to parse resource pack meta", e); - } + meta = PackMcMeta.fromNonNullJson(zipFileTree.readTextEntry("/pack.mcmeta")); + } catch (Exception e) { + LOG.warning("Failed to parse resource pack meta", e); } - this.info = meta != null ? meta.pack() : null; + + return new ResourcePackZipFile(manager, path, meta != null ? meta.pack() : null); } - @Override - public PackMcMeta.PackInfo getPackInfo() { - return info; + private ResourcePackZipFile(ResourcePackManager manager, Path path, PackMcMeta.PackInfo info) { + super(manager, path, info); } @Override @@ -80,8 +74,9 @@ public PackMcMeta.PackInfo getPackInfo() { } @Override - public void delete() throws IOException { - Files.deleteIfExists(file); + public void onUpdated(String newFileNameWithExt) { + super.onUpdated(newFileNameWithExt); + manager.updateOptions(getFileNameWithExtension(), newFileNameWithExt); } @Override @@ -96,7 +91,7 @@ public AddonUpdate checkUpdates(DownloadProvider downloadProvider, String gameVe .sorted(Comparator.comparing(RemoteAddon.Version::datePublished).reversed()) .toList(); if (remoteVersions.isEmpty()) return null; - return new AddonUpdate(source, RemoteAddon.Type.RESOURCE_PACK, this, currentVersion.get(), remoteVersions.get(0), false); + return new AddonUpdate(source, RemoteAddon.Type.RESOURCE_PACK, this, currentVersion.get(), remoteVersions.get(0)); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/CurseForgeRemoteAddonRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/CurseForgeRemoteAddonRepository.java index e044ff332b4..732b83f1ace 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/CurseForgeRemoteAddonRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/repository/CurseForgeRemoteAddonRepository.java @@ -510,7 +510,7 @@ else if (StringUtils.isNotBlank(logo.url())) "", name, summary, - categories.stream().map(category -> Integer.toString(category.getId())).collect(Collectors.toList()), + categories.stream().map(category -> Integer.toString(category.getId())).toList(), links.websiteUrl, iconUrl, this, diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/shader/ShaderLoaderType.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/shader/ShaderLoaderType.java new file mode 100644 index 00000000000..191add311d1 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/shader/ShaderLoaderType.java @@ -0,0 +1,48 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 org.jackhuang.hmcl.addon.shader; + +import org.jackhuang.hmcl.addon.AddonLoaderType; +import org.jetbrains.annotations.NotNullByDefault; +import org.jetbrains.annotations.Unmodifiable; + +import java.util.Set; + +@NotNullByDefault +public enum ShaderLoaderType implements AddonLoaderType { + OPTIFINE_IRIS("Optifine/Iris", "optifine", "iris"), + APERTURE("Aperture", "aperture"); + + private final String displayName; + private final Set names; + + ShaderLoaderType(String displayName, String... names) { + this.displayName = displayName; + this.names = Set.of(names); + } + + @Override + public String displayName() { + return displayName; + } + + @Override + public @Unmodifiable Set names() { + return names; + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/shader/ShaderPackFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/shader/ShaderPackFile.java new file mode 100644 index 00000000000..651843ec288 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/shader/ShaderPackFile.java @@ -0,0 +1,135 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 org.jackhuang.hmcl.addon.shader; + +import javafx.scene.image.Image; +import org.jackhuang.hmcl.addon.LocalAddonFile; +import org.jackhuang.hmcl.addon.LocalAddonManager; +import org.jackhuang.hmcl.util.StringUtils; +import org.jackhuang.hmcl.util.io.CompressingUtils; +import org.jackhuang.hmcl.util.io.FileUtils; +import org.jetbrains.annotations.NotNullByDefault; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Objects; +import java.util.stream.Stream; + +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +@NotNullByDefault +public sealed abstract class ShaderPackFile extends LocalAddonFile implements Comparable permits ShaderPackZipFile, ShaderPackFolder { + public static @Nullable ShaderPackFile fromFile(ShaderPackManager manager, Path file) throws IOException { + return Files.isRegularFile(file) ? ShaderPackZipFile.load(manager, file) : ShaderPackFolder.load(manager, file); + } + + public static boolean isFileShaderPack(Path file) { + if (Files.isDirectory(file)) return Files.isDirectory(file.resolve("shaders")); + if (Files.isRegularFile(file)) { + try (var zipSystem = CompressingUtils.createReadOnlyZipFileSystem(file)) { + Path root = zipSystem.getRootDirectories().iterator().next(); + try (Stream stream = Files.walk(root)) { + return stream.filter(path -> path.endsWith("shaders")).anyMatch(Files::isDirectory); + } + } catch (IOException e) { + LOG.warning("Failed to check if file is shader pack", e); + } + } + return false; + } + + protected final ShaderPackManager manager; + protected Path file; + protected final ShaderLoaderType loaderType; + protected final ShaderPackMeta meta; + + protected final String fileName; + protected final String fileNameWithExtension; + + protected ShaderPackFile(ShaderPackManager manager, Path file, ShaderLoaderType loaderType, @Nullable ShaderPackMeta meta) { + this.manager = manager; + this.file = file; + this.loaderType = loaderType; + this.meta = Objects.requireNonNullElse(meta, ShaderPackMeta.EMPTY); + + this.fileName = FileUtils.getNameWithoutExtension(file); + this.fileNameWithExtension = file.getFileName().toString(); + } + + @Override + public Path getFile() { + return file; + } + + @Override + public String getFileName() { + return fileName; + } + + public String getFileNameWithExtension() { + return fileNameWithExtension; + } + + public ShaderLoaderType getShaderLoaderType() { + return loaderType; + } + + public ShaderPackMeta getMeta() { + return meta; + } + + public abstract @Nullable Image loadIcon(); + + public String getName() { + return StringUtils.isBlank(getMeta().name()) ? getFile().getFileName().toString() : getMeta().name(); + } + + @Override + public boolean isDisabled() { + return false; + } + + @Override + public void markDisabled() { + // NO-OP + } + + @Override + public boolean keepOldFiles() { + return false; + } + + @Override + public void setOld(boolean old) throws IOException { + if (old) file = LocalAddonManager.backupFile(file); + else file = LocalAddonManager.restoreFile(file); + } + + @Override + public int compareTo(ShaderPackFile other) { + return fileName.compareTo(other.fileName); + } + + @Override + public void onUpdated(String newFileNameWithExt) { + super.onUpdated(newFileNameWithExt); + manager.updateConfigFileName(this, newFileNameWithExt); + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/shader/ShaderPackFolder.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/shader/ShaderPackFolder.java new file mode 100644 index 00000000000..307061e0ad0 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/shader/ShaderPackFolder.java @@ -0,0 +1,86 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 org.jackhuang.hmcl.addon.shader; + +import javafx.scene.image.Image; +import org.jackhuang.hmcl.util.StringUtils; +import org.jackhuang.hmcl.util.gson.JsonUtils; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +final class ShaderPackFolder extends ShaderPackFile { + + public static @Nullable ShaderPackFolder load(ShaderPackManager manager, Path file) { + aperture: + { + Path metaPath = file.resolve("pack.json"); + if (!Files.isRegularFile(metaPath) + || !Files.isDirectory(file.resolve("slang")) + || !Files.isDirectory(file.resolve("src"))) { + break aperture; + } + + ShaderPackMeta meta = null; + try { + meta = JsonUtils.fromJsonFile(JsonUtils.LENIENT_GSON, metaPath, ShaderPackMeta.class); + } catch (IOException e) { + LOG.warning("Failed to load shader metadata", e); + } + if (meta == null || StringUtils.isBlank(meta.apertureApiVersion())) break aperture; + + return new ShaderPackFolder(manager, file, ShaderLoaderType.APERTURE, meta); + } + { + // Optifine/Iris + Path shadersPath = file.resolve("shaders"); + if (!Files.isDirectory(shadersPath)) return null; + + ShaderPackMeta meta = null; + try { + meta = JsonUtils.fromJsonFile(JsonUtils.LENIENT_GSON, shadersPath.resolve("pack.json"), ShaderPackMeta.class); + } catch (IOException e) { + LOG.warning("Failed to load shader metadata", e); + } + + return new ShaderPackFolder(manager, file, ShaderLoaderType.OPTIFINE_IRIS, meta); + } + } + + private ShaderPackFolder(ShaderPackManager manager, Path file, ShaderLoaderType loaderType, @Nullable ShaderPackMeta shaderPackMeta) { + super(manager, file, loaderType, shaderPackMeta); + } + + public @Nullable Image loadIcon() { + Path iconPath = switch (getShaderLoaderType()) { + case APERTURE -> getFile().resolve("pack.png"); + case OPTIFINE_IRIS -> getFile().resolve("shaders").resolve("pack.png"); + }; + if (!Files.isRegularFile(iconPath)) return null; + try (var inputStream = Files.newInputStream(iconPath)) { + return new Image(inputStream, 64, 64, true, true); + } catch (Exception e) { + LOG.warning("Failed to load shader pack icon at " + iconPath, e); + } + return null; + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/shader/ShaderPackManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/shader/ShaderPackManager.java new file mode 100644 index 00000000000..bd0c1e96949 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/shader/ShaderPackManager.java @@ -0,0 +1,123 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 org.jackhuang.hmcl.addon.shader; + +import org.jackhuang.hmcl.addon.LocalAddonManager; +import org.jackhuang.hmcl.game.DefaultGameInstance; +import org.jackhuang.hmcl.util.io.FileUtils; +import org.jetbrains.annotations.Unmodifiable; + +import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.List; + +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +public class ShaderPackManager extends LocalAddonManager { + + public ShaderPackManager(DefaultGameInstance instance) { + super(instance); + } + + private boolean loaded = false; + + @Override + public Path getDirectory() { + return instance.getShadersDirectory(); + } + + @Override + public @Unmodifiable List getLocalFiles() throws IOException { + lock.lock(); + try { + if (!loaded) + refresh(); + return super.getLocalFiles(); + } finally { + lock.unlock(); + } + } + + @Override + public Comparator getComparator() { + return ShaderPackFile::compareTo; + } + + private void addShaderPackInfo(Path file) throws IOException { + ShaderPackFile shaderPackFile = ShaderPackFile.fromFile(this, file); + if (shaderPackFile != null) localFiles.add(shaderPackFile); + } + + @Override + public void refresh() throws IOException { + lock.lock(); + try { + localFiles.clear(); + + if (Files.isDirectory(getDirectory())) { + try (DirectoryStream directoryStream = Files.newDirectoryStream(getDirectory())) { + for (Path item : directoryStream) { + try { + addShaderPackInfo(item); + } catch (IOException e) { + LOG.warning("Failed to load resource pack " + item, e); + } + } + } + } + + loaded = true; + } finally { + lock.unlock(); + } + } + + public void importShaderPack(Path file) throws IOException, IllegalArgumentException { + if (ShaderPackFile.isFileShaderPack(file)) { + Files.createDirectories(getDirectory()); + FileUtils.copyTo(file, getDirectory()); + + loaded = false; + } else { + throw new IllegalArgumentException("File is not shader pack"); + } + } + + public void removeShaderPacks(List files) throws IOException { + for (var file : files) { + file.delete(); + } + + loaded = false; + } + + public void updateConfigFileName(ShaderPackFile pack, String newFileNameWithExt) { + var configPath = pack.getFile().resolveSibling(pack.getFileNameWithExtension() + ".txt"); + if (Files.isRegularFile(configPath)) { + var newConfigPath = pack.getFile().resolveSibling(newFileNameWithExt + ".txt"); + try { + Files.move(configPath, newConfigPath); + } catch (IOException e) { + LOG.warning("Failed to rename shader config file " + configPath, e); + } + } + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/shader/ShaderPackMeta.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/shader/ShaderPackMeta.java new file mode 100644 index 00000000000..7c9f93f7ec2 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/shader/ShaderPackMeta.java @@ -0,0 +1,31 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 org.jackhuang.hmcl.addon.shader; + +import com.google.gson.annotations.SerializedName; +import org.jackhuang.hmcl.util.gson.JsonSerializable; +import org.jetbrains.annotations.Nullable; + +@JsonSerializable +public record ShaderPackMeta(@Nullable String name, @Nullable String version, + @SerializedName(value = "shaderDescription") @Nullable String description, + @SerializedName(value = "api") @Nullable String apertureApiVersion +) { + public static final ShaderPackMeta EMPTY = new ShaderPackMeta(null, null, null, null); + +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/shader/ShaderPackZipFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/shader/ShaderPackZipFile.java new file mode 100644 index 00000000000..0942ee87fbd --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/shader/ShaderPackZipFile.java @@ -0,0 +1,127 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 org.jackhuang.hmcl.addon.shader; + +import javafx.scene.image.Image; +import org.jackhuang.hmcl.addon.RemoteAddon; +import org.jackhuang.hmcl.addon.RemoteAddonRepository; +import org.jackhuang.hmcl.download.DownloadProvider; +import org.jackhuang.hmcl.util.StringUtils; +import org.jackhuang.hmcl.util.gson.JsonUtils; +import org.jackhuang.hmcl.util.io.CompressingUtils; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.stream.Stream; + +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +final class ShaderPackZipFile extends ShaderPackFile { + + public static @Nullable ShaderPackZipFile load(ShaderPackManager manager, Path file) throws IOException { + if (!Files.isRegularFile(file) || !file.toString().toLowerCase(Locale.ROOT).endsWith(".zip")) return null; + + try (var zipFileSystem = CompressingUtils.createReadOnlyZipFileSystem(file)) { + Path root = zipFileSystem.getRootDirectories().iterator().next(); + + aperture: + { + Path metaPath = root.resolve("pack.json"); + if (!Files.isRegularFile(metaPath) + || !Files.isDirectory(root.resolve("src")) + || !Files.isDirectory(root.resolve("slang"))) { + break aperture; + } + ShaderPackMeta meta = null; + try { + meta = JsonUtils.fromJsonFile(JsonUtils.LENIENT_GSON, metaPath, ShaderPackMeta.class); + } catch (IOException e) { + LOG.warning("Failed to load shader metadata", e); + } + if (meta == null || StringUtils.isBlank(meta.apertureApiVersion())) break aperture; + + String iconPath = root.resolve("pack.png").toAbsolutePath().normalize().toString(); + return new ShaderPackZipFile(manager, file, iconPath, ShaderLoaderType.APERTURE, meta); + } + + { + // Optifine & Iris + Path shadersPath; + try (Stream stream = Files.walk(root)) { + shadersPath = stream + .filter(path -> path.endsWith("shaders")) + .filter(Files::isDirectory) + .findFirst().orElse(null); + if (shadersPath == null) return null; + } + ShaderPackMeta meta = null; + try { + meta = JsonUtils.fromJsonFile(JsonUtils.LENIENT_GSON, shadersPath.resolve("pack.json"), ShaderPackMeta.class); + } catch (IOException e) { + LOG.warning("Failed to load shader metadata", e); + } + + String iconPath = shadersPath.resolve("pack.png").toAbsolutePath().normalize().toString(); + return new ShaderPackZipFile(manager, file, iconPath, ShaderLoaderType.OPTIFINE_IRIS, meta); + } + } + } + + private final String iconPath; + + private ShaderPackZipFile(ShaderPackManager manager, Path file, String iconPath, ShaderLoaderType loaderType, @Nullable ShaderPackMeta shaderPackMeta) { + super(manager, file, loaderType, shaderPackMeta); + this.iconPath = iconPath; + } + + @Override + public @Nullable Image loadIcon() { + try (var zipFileSystem = CompressingUtils.createReadOnlyZipFileSystem(getFile())) { + var path = zipFileSystem.getPath(iconPath); + if (!Files.isRegularFile(path)) return null; + try (var inputStream = Files.newInputStream(zipFileSystem.getPath(iconPath))) { + return new Image(inputStream, 64, 64, true, true); + } + } catch (Exception e) { + LOG.warning("Failed to load shader pack icon at %s!/%s".formatted(getFile(), iconPath), e); + } + return null; + } + + @Override + public @Nullable AddonUpdate checkUpdates(DownloadProvider downloadProvider, String gameVersion, RemoteAddon.Source source) throws IOException { + RemoteAddonRepository repository = source.getRepoForType(RemoteAddon.Type.SHADER_PACK); + if (repository == null) return null; + Optional currentVersion = repository.getRemoteVersionByLocalFile(file); + if (currentVersion.isEmpty()) return null; + List remoteVersions = repository.getRemoteVersionsById(downloadProvider, currentVersion.get().projectId()) + .filter(version -> version.gameVersions().contains(gameVersion)) //TODO loader + .filter(version -> version.loaders().stream().anyMatch(it -> it.type() == getShaderLoaderType())) + .filter(version -> version.datePublished().compareTo(currentVersion.get().datePublished()) > 0) + .sorted(Comparator.comparing(RemoteAddon.Version::datePublished).reversed()) + .toList(); + if (remoteVersions.isEmpty()) return null; + return new AddonUpdate(source, RemoteAddon.Type.SHADER_PACK, this, currentVersion.get(), remoteVersions.get(0)); + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameInstance.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameInstance.java index 041467d669b..b53747aae5d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameInstance.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameInstance.java @@ -19,7 +19,8 @@ import com.google.gson.JsonParseException; import org.jackhuang.hmcl.addon.mod.ModManager; -import org.jackhuang.hmcl.addon.resourcepack.ResourcePackManager; +import org.jackhuang.hmcl.addon.pack.resourcepack.ResourcePackManager; +import org.jackhuang.hmcl.addon.shader.ShaderPackManager; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.versioning.GameVersionNumber; @@ -73,6 +74,9 @@ public abstract class DefaultGameInstance implements GameInstance { /// Lazily created resource-pack manager for this snapshot member only. private @Nullable ResourcePackManager resourcePackManager; + /// Lazily created shader-pack manager for this snapshot member only. + private @Nullable ShaderPackManager shaderPackManager; + protected DefaultGameInstance( DefaultGameRepositorySnapshot snapshot, GameInstanceID id, @@ -214,6 +218,20 @@ public ResourcePackManager getResourcePackManager() { return resourcePackManager; } + /// Returns the shader-pack manager for this snapshot member. + /// + /// The manager is created on first use and is not shared with other snapshot wrappers. After a + /// repository refresh or COW publish, callers should obtain the manager from the current + /// instance again. + /// + /// @return the shader-pack manager + public ShaderPackManager getShaderPackManager() { + if (shaderPackManager == null) { + shaderPackManager = new ShaderPackManager(this); + } + return shaderPackManager; + } + /// Detects the Minecraft game version from this instance's primary client jar. /// /// @return the detected version, or [GameVersionNumber#unknown()] when detection fails diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstance.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstance.java index a558a8792b0..d322eeefebe 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstance.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstance.java @@ -142,6 +142,13 @@ default Path getResourcePackDirectory() { return getRunDirectory().resolve("resourcepacks"); } + /// Returns the directory containing shader packs used by this instance. + /// + /// @return the shader pack directory below the run directory + default Path getShadersDirectory() { + return getRunDirectory().resolve("shaderpacks"); + } + /// Returns the directory containing saved worlds used by this instance. /// /// @return the saves directory below the run directory diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java index 3e6d66b625e..09bae88d4f4 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java @@ -79,6 +79,16 @@ public final class JsonUtils { .registerTypeAdapterFactory(LowerCaseEnumTypeAdapter.FACTORY) .create(); + /// The lenient [Gson] instance. + /// + /// Configured with: + /// - Pretty printing enabled. + /// - Complex map key serialization enabled. + /// - Type adapters for [java.time.Instant], [java.util.UUID], and [java.nio.file.Path]. + /// - [ValidationTypeAdapterFactory], [LowerCaseEnumTypeAdapterFactory], and + /// [JsonTypeAdapterFactory]. + public static final Gson LENIENT_GSON = defaultGsonBuilder().setStrictness(Strictness.LENIENT).create(); + /// Not instantiable. private JsonUtils() { } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/FileUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/FileUtils.java index c6cf0ee08ea..6692bea0cd4 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/FileUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/FileUtils.java @@ -468,6 +468,14 @@ public static void forceDelete(Path file) Files.delete(file); } + public static void forceDeleteIfExists(Path file) + throws IOException { + if (Files.isDirectory(file)) + deleteDirectory(file); + else + Files.deleteIfExists(file); + } + public static void copyFile(Path srcFile, Path destFile) throws IOException { Objects.requireNonNull(srcFile, "Source must not be null"); @@ -483,6 +491,16 @@ public static void copyFile(Path srcFile, Path destFile) Files.copy(srcFile, destFile, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING); } + public static void copyTo(Path src, Path destDir) throws IOException { + Objects.requireNonNull(src, "Source must not be null"); + Objects.requireNonNull(destDir, "Destination directory must not be null"); + Path dest = destDir.resolve(src.getFileName()); + if (Files.isDirectory(src)) + copyDirectory(src, dest); + else + copyFile(src, dest); + } + public static List listFilesByExtension(Path file, String extension) { try (Stream list = Files.list(file)) { return list.filter(it -> Files.isRegularFile(it) && extension.equals(getExtension(it))) diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/resourcepack/ResourcePackManagerTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/addon/pack/resourcepack/ResourcePackManagerTest.java similarity index 88% rename from HMCLCore/src/test/java/org/jackhuang/hmcl/resourcepack/ResourcePackManagerTest.java rename to HMCLCore/src/test/java/org/jackhuang/hmcl/addon/pack/resourcepack/ResourcePackManagerTest.java index 26ab05a748b..eb990917c78 100644 --- a/HMCLCore/src/test/java/org/jackhuang/hmcl/resourcepack/ResourcePackManagerTest.java +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/addon/pack/resourcepack/ResourcePackManagerTest.java @@ -1,6 +1,6 @@ /* * Hello Minecraft! Launcher - * Copyright (C) 2025 huangyuhui and contributors + * Copyright (C) 2026 huangyuhui 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 @@ -15,13 +15,13 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.resourcepack; +package org.jackhuang.hmcl.addon.pack.resourcepack; import org.jackhuang.hmcl.util.versioning.GameVersionNumber; import org.junit.jupiter.api.Test; -import static org.jackhuang.hmcl.addon.resourcepack.ResourcePackManager.isMcVersionSupported; -import static org.jackhuang.hmcl.addon.resourcepack.ResourcePackManager.isMcVersionSupportsNewOptionsFormat; +import static org.jackhuang.hmcl.addon.pack.resourcepack.ResourcePackManager.isMcVersionSupported; +import static org.jackhuang.hmcl.addon.pack.resourcepack.ResourcePackManager.isMcVersionSupportsNewOptionsFormat; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue;