From 23750e12229d1d51272aa165d8528282fd8c4043 Mon Sep 17 00:00:00 2001 From: huanghongxun Date: Tue, 5 Oct 2021 16:50:05 +0800 Subject: [PATCH] feat(mod): mod upgrade detection. --- .../hmcl/ui/construct/Navigator.java | 2 + .../hmcl/ui/construct/TabHeader.java | 22 +- .../hmcl/ui/construct/TaskListPane.java | 2 +- .../hmcl/ui/main/LauncherSettingsPage.java | 13 +- .../hmcl/ui/versions/ModListPage.java | 2 +- .../hmcl/ui/versions/ModUpdateTask.java | 2 +- .../hmcl/ui/versions/ModUpdatesDialog.java | 67 ---- .../hmcl/ui/versions/ModUpdatesPane.java | 155 +++++++++ .../assets/lang/I18N_zh_CN.properties | 3 + .../java/org/jackhuang/hmcl/mod/LocalMod.java | 1 + .../jackhuang/hmcl/mod/curse/CurseAddon.java | 3 +- .../curse/CurseForgeRemoteModRepository.java | 41 +-- .../hmcl/task/AsyncTaskExecutor.java | 5 + .../java/org/jackhuang/hmcl/task/Task.java | 10 + .../org/jackhuang/hmcl/util/MurmurHash.java | 214 ------------ .../org/jackhuang/hmcl/util/MurmurHash2.java | 319 ++++++++++++++++++ .../CurseForgeRemoteModRepositoryTest.java | 53 +++ 17 files changed, 607 insertions(+), 307 deletions(-) delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesDialog.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPane.java delete mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/util/MurmurHash.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/util/MurmurHash2.java create mode 100644 HMCLCore/src/test/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepositoryTest.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/Navigator.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/Navigator.java index f1de4f579..068664ea1 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/Navigator.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/Navigator.java @@ -111,6 +111,8 @@ public class Navigator extends TransitionPane { Logging.LOG.info("Closed page " + from); Node poppedNode = stack.pop(); + NavigationEvent exited = new NavigationEvent(this, poppedNode, Navigation.NavigationDirection.PREVIOUS, NavigationEvent.EXITED); + poppedNode.fireEvent(exited); if (poppedNode instanceof PageAware) ((PageAware) poppedNode).onPageHidden(); backable.set(canGoBack()); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TabHeader.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TabHeader.java index 777bbe050..84cd620ea 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TabHeader.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TabHeader.java @@ -39,7 +39,7 @@ import javafx.util.Duration; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.util.javafx.MappedObservableList; -public class TabHeader extends Control implements TabControl { +public class TabHeader extends Control implements TabControl, PageAware { public TabHeader(Tab... tabs) { getStyleClass().setAll("tab-header"); @@ -86,6 +86,26 @@ public class TabHeader extends Control implements TabControl { getSelectionModel().select(tab); } + @Override + public void onPageShown() { + Tab tab = getSelectionModel().getSelectedItem(); + if (tab != null) { + if (tab.getNode() instanceof PageAware) { + ((PageAware) tab.getNode()).onPageShown(); + } + } + } + + @Override + public void onPageHidden() { + Tab tab = getSelectionModel().getSelectedItem(); + if (tab != null) { + if (tab.getNode() instanceof PageAware) { + ((PageAware) tab.getNode()).onPageHidden(); + } + } + } + /** * The position to place the tabs. */ diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java index a050f1b9e..ab87bb1bb 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java @@ -144,7 +144,7 @@ public final class TaskListPane extends StackPane { Platform.runLater(() -> { ProgressListNode node = new ProgressListNode(task); nodes.put(task, node); - StageNode stageNode = stageNodes.stream().filter(x -> x.stage.equals(task.getStage())).findAny().orElse(null); + StageNode stageNode = stageNodes.stream().filter(x -> x.stage.equals(task.getInheritedStage())).findAny().orElse(null); listBox.add(listBox.indexOf(stageNode) + 1, node); }); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/LauncherSettingsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/LauncherSettingsPage.java index 2901af308..a11876033 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/LauncherSettingsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/LauncherSettingsPage.java @@ -26,6 +26,7 @@ import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.animation.ContainerAnimations; import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.ui.construct.AdvancedListBox; +import org.jackhuang.hmcl.ui.construct.PageAware; import org.jackhuang.hmcl.ui.construct.TabHeader; import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; @@ -34,7 +35,7 @@ import org.jackhuang.hmcl.ui.versions.VersionSettingsPage; import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -public class LauncherSettingsPage extends DecoratorAnimatedPage implements DecoratorPage { +public class LauncherSettingsPage extends DecoratorAnimatedPage implements DecoratorPage, PageAware { private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("settings"), -1)); private final TabHeader tab; private final TabHeader.Tab gameTab = new TabHeader.Tab<>("versionSettingsPage"); @@ -124,6 +125,16 @@ public class LauncherSettingsPage extends DecoratorAnimatedPage implements Decor setCenter(transitionPane); } + @Override + public void onPageShown() { + tab.onPageShown(); + } + + @Override + public void onPageHidden() { + tab.onPageHidden(); + } + public void showGameSettings(Profile profile) { gameTab.getNode().loadVersion(profile, null); tab.select(gameTab); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java index 42b7dd8d2..ceecebe5f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java @@ -187,7 +187,7 @@ public final class ModListPage extends ListPageBase> { dependents = mods.stream() .map(mod -> Task.supplyAsync(() -> { return mod.checkUpdates(gameVersion, CurseForgeRemoteModRepository.MODS); - }).setSignificance(TaskSignificance.MAJOR).withCounter("mods.check_updates")) + }).setSignificance(TaskSignificance.MAJOR).setName(mod.getFileName()).withCounter("mods.check_updates")) .collect(Collectors.toList()); setStage("mods.check_updates"); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesDialog.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesDialog.java deleted file mode 100644 index 28f262a90..000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesDialog.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2021 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.versions; - -import com.jfoenix.controls.JFXListView; -import org.jackhuang.hmcl.mod.LocalMod; -import org.jackhuang.hmcl.mod.curse.CurseAddon; -import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository; -import org.jackhuang.hmcl.ui.construct.DialogPane; -import org.jackhuang.hmcl.ui.construct.MDListCell; -import org.jackhuang.hmcl.ui.construct.TwoLineListItem; - -import java.util.List; - -import static org.jackhuang.hmcl.util.i18n.I18n.i18n; - -public class ModUpdatesDialog extends DialogPane { - - public ModUpdatesDialog(List updates) { - setTitle(i18n("mods.check_updates")); - - JFXListView listView = new JFXListView<>(); - listView.getItems().setAll(updates); - listView.setCellFactory(l -> new ModUpdateCell(listView)); - setBody(listView); - } - - public static class ModUpdateCell extends MDListCell { - TwoLineListItem content = new TwoLineListItem(); - - public ModUpdateCell(JFXListView listView) { - super(listView); - - getContainer().getChildren().setAll(content); - } - - @Override - protected void updateControl(LocalMod.ModUpdate item, boolean empty) { - if (empty) return; - ModTranslations.Mod mod = ModTranslations.getModById(item.getLocalMod().getId()); - content.setTitle(mod != null ? mod.getDisplayName() : item.getCurrentVersion().getName()); - content.setSubtitle(item.getLocalMod().getFileName()); - content.getTags().setAll(); - - if (item.getCurrentVersion().getSelf() instanceof CurseAddon.LatestFile) { - content.getTags().add("Curseforge"); - } else if (item.getCurrentVersion().getSelf() instanceof ModrinthRemoteModRepository.ModVersion) { - content.getTags().add("Modrinth"); - } - } - } -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPane.java new file mode 100644 index 000000000..a1c246057 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPane.java @@ -0,0 +1,155 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2021 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.versions; + +import com.jfoenix.controls.*; +import com.jfoenix.controls.datamodels.treetable.RecursiveTreeObject; +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.geometry.Pos; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import org.jackhuang.hmcl.mod.LocalMod; +import org.jackhuang.hmcl.mod.curse.CurseAddon; +import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository; +import org.jackhuang.hmcl.ui.construct.MDListCell; +import org.jackhuang.hmcl.ui.construct.PageCloseEvent; +import org.jackhuang.hmcl.ui.construct.TwoLineListItem; +import org.jackhuang.hmcl.ui.decorator.DecoratorPage; + +import java.util.List; +import java.util.stream.Collectors; + +import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; + +public class ModUpdatesPane extends BorderPane implements DecoratorPage { + private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(DecoratorPage.State.fromTitle(i18n("download"), -1)); + + public ModUpdatesPane(List updates) { + + JFXTreeTableColumn fileNameColumn = new JFXTreeTableColumn<>(i18n("mods.check_updates.file")); + fileNameColumn.setCellValueFactory(data -> { + if (fileNameColumn.validateValue(data)) { + return data.getValue().getValue().fileName; + } else { + return fileNameColumn.getComputedValue(data); + } + }); + + JFXTreeTableColumn currentVersionColumn = new JFXTreeTableColumn<>(i18n("mods.check_updates.current_version")); + currentVersionColumn.setCellValueFactory(data -> { + if (currentVersionColumn.validateValue(data)) { + return data.getValue().getValue().currentVersion; + } else { + return currentVersionColumn.getComputedValue(data); + } + }); + + JFXTreeTableColumn targetVersionColumn = new JFXTreeTableColumn<>(i18n("mods.check_updates.target_version")); + targetVersionColumn.setCellValueFactory(data -> { + if (targetVersionColumn.validateValue(data)) { + return data.getValue().getValue().targetVersion; + } else { + return targetVersionColumn.getComputedValue(data); + } + }); + + ObservableList objects = FXCollections.observableList(updates.stream().map(ModUpdateObject::new).collect(Collectors.toList())); + + RecursiveTreeItem root = new RecursiveTreeItem<>( + objects, + RecursiveTreeObject::getChildren); + + JFXTreeTableView table = new JFXTreeTableView<>(root); + table.setShowRoot(false); + table.setEditable(true); + table.getColumns().setAll(fileNameColumn, currentVersionColumn, targetVersionColumn); + + setCenter(table); + + HBox actions = new HBox(8); + actions.setAlignment(Pos.CENTER_RIGHT); + + JFXButton nextButton = new JFXButton(); + nextButton.getStyleClass().add("jfx-button-raised"); + nextButton.setButtonType(JFXButton.ButtonType.RAISED); + nextButton.setOnAction(e -> updateMods()); + + JFXButton cancelButton = new JFXButton(); + cancelButton.getStyleClass().add("jfx-button-raised"); + cancelButton.setButtonType(JFXButton.ButtonType.RAISED); + cancelButton.setOnAction(e -> fireEvent(new PageCloseEvent())); + onEscPressed(this, cancelButton::fire); + + actions.getChildren().setAll(nextButton, cancelButton); + setBottom(actions); + } + + private void updateMods() { + + } + + @Override + public ReadOnlyObjectWrapper stateProperty() { + return state; + } + + public static class ModUpdateCell extends MDListCell { + TwoLineListItem content = new TwoLineListItem(); + + public ModUpdateCell(JFXListView listView) { + super(listView); + + getContainer().getChildren().setAll(content); + } + + @Override + protected void updateControl(LocalMod.ModUpdate item, boolean empty) { + if (empty) return; + ModTranslations.Mod mod = ModTranslations.getModById(item.getLocalMod().getId()); + content.setTitle(mod != null ? mod.getDisplayName() : item.getCurrentVersion().getName()); + content.setSubtitle(item.getLocalMod().getFileName()); + content.getTags().setAll(); + + if (item.getCurrentVersion().getSelf() instanceof CurseAddon.LatestFile) { + content.getTags().add("Curseforge"); + } else if (item.getCurrentVersion().getSelf() instanceof ModrinthRemoteModRepository.ModVersion) { + content.getTags().add("Modrinth"); + } + } + } + + private static class ModUpdateObject extends RecursiveTreeObject { + final LocalMod.ModUpdate data; + final StringProperty fileName = new SimpleStringProperty(); + final StringProperty currentVersion = new SimpleStringProperty(); + final StringProperty targetVersion = new SimpleStringProperty(); + + public ModUpdateObject(LocalMod.ModUpdate data) { + this.data = data; + + fileName.set(data.getLocalMod().getFileName()); + currentVersion.set(data.getCurrentVersion().getName()); + targetVersion.set(data.getCandidates().get(0).getName()); + } + } +} diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 62e6701d2..c541801b4 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -581,6 +581,9 @@ mods.add.failed=添加模组 %s 失败。 mods.add.success=成功添加模组 %s。 mods.category=类别 mods.check_updates=检查模组更新 +mods.check_updates.current_version=当前版本 +mods.check_updates.file=文件 +mods.check_updates.target_version=目标版本 mods.choose_mod=选择模组 mods.curseforge=CurseForge mods.dependencies=前置 Mod diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalMod.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalMod.java index 2a1e518fe..1d269a9ff 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalMod.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalMod.java @@ -148,6 +148,7 @@ public final class LocalMod implements Comparable { .filter(version -> version.getLoaders().contains(modLoaderType)) .sorted(Comparator.comparing(RemoteMod.Version::getDatePublished).reversed()) .collect(Collectors.toList()); + if (remoteVersions.isEmpty()) return null; return new ModUpdate(this, currentVersion.get(), remoteVersions); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java index f946249d6..c13e9d615 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java @@ -17,6 +17,7 @@ */ package org.jackhuang.hmcl.mod.curse; +import org.jackhuang.hmcl.mod.ModLoaderType; import org.jackhuang.hmcl.mod.RemoteMod; import org.jackhuang.hmcl.util.Immutable; @@ -508,7 +509,7 @@ public class CurseAddon implements RemoteMod.IMod { new RemoteMod.File(Collections.emptyMap(), getDownloadUrl(), getFileName()), Collections.emptyList(), gameVersion.stream().filter(ver -> ver.startsWith("1.") || ver.contains("w")).collect(Collectors.toList()), - Collections.emptyList() + Collections.singletonList(ModLoaderType.FORGE) ); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java index 7ddf7630b..0a90861fb 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java @@ -21,15 +21,12 @@ import com.google.gson.reflect.TypeToken; import org.jackhuang.hmcl.mod.LocalMod; import org.jackhuang.hmcl.mod.RemoteMod; import org.jackhuang.hmcl.mod.RemoteModRepository; -import org.jackhuang.hmcl.util.MurmurHash; +import org.jackhuang.hmcl.util.MurmurHash2; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.io.HttpRequest; import org.jackhuang.hmcl.util.io.NetworkUtils; -import java.io.BufferedReader; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStreamReader; +import java.io.*; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; @@ -42,7 +39,7 @@ import static org.jackhuang.hmcl.util.Pair.pair; public final class CurseForgeRemoteModRepository implements RemoteModRepository { - private static final String PREFIX = "https://addons-ecs.forgesvc.net/api/v2"; + private static final String PREFIX = "https://addons-ecs.forgesvc.net"; private final int section; @@ -51,7 +48,7 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository } public List searchPaginated(String gameVersion, int category, int pageOffset, int pageSize, String searchFilter, int sort) throws IOException { - String response = NetworkUtils.doGet(new URL(NetworkUtils.withQuery(PREFIX + "/addon/search", mapOf( + String response = NetworkUtils.doGet(new URL(NetworkUtils.withQuery(PREFIX + "/api/v2/addon/search", mapOf( pair("categoryId", Integer.toString(category)), pair("gameId", "432"), pair("gameVersion", gameVersion), @@ -76,18 +73,22 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository @Override public Optional getRemoteVersionByLocalFile(LocalMod localMod, Path file) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); - try (BufferedReader reader = new BufferedReader(new InputStreamReader(Files.newInputStream(file)))) { - int b; - while ((b = reader.read()) != -1) { - if (b != 0x9 && b != 0xa && b != 0xd && b != 0x20) { - baos.write(b); + try (InputStream stream = Files.newInputStream(file)) { + byte[] buf = new byte[1024]; + int len; + while ((len = stream.read(buf, 0, buf.length)) != -1) { + for (int i = 0; i < len; i++) { + byte b = buf[i]; + if (b != 0x9 && b != 0xa && b != 0xd && b != 0x20) { + baos.write(b); + } } } } - int hash = MurmurHash.hash32(baos.toByteArray(), baos.size(), 1); + long hash = Integer.toUnsignedLong(MurmurHash2.hash32(baos.toByteArray(), baos.size(), 1)); - FingerprintResponse response = HttpRequest.POST(PREFIX + "/fingerprint") + FingerprintResponse response = HttpRequest.POST(PREFIX + "/api/v2/fingerprint") .json(Collections.singletonList(hash)) .getJson(FingerprintResponse.class); @@ -100,20 +101,20 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository @Override public RemoteMod getModById(String id) throws IOException { - String response = NetworkUtils.doGet(NetworkUtils.toURL(PREFIX + "/addon/" + id)); + String response = NetworkUtils.doGet(NetworkUtils.toURL(PREFIX + "/api/v2/addon/" + id)); return JsonUtils.fromNonNullJson(response, CurseAddon.class).toMod(); } @Override public Stream getRemoteVersionsById(String id) throws IOException { - String response = NetworkUtils.doGet(NetworkUtils.toURL(PREFIX + "/addon/" + id + "/files")); + String response = NetworkUtils.doGet(NetworkUtils.toURL(PREFIX + "/api/v2/addon/" + id + "/files")); List files = JsonUtils.fromNonNullJson(response, new TypeToken>() { }.getType()); return files.stream().map(CurseAddon.LatestFile::toVersion); } public List getCategoriesImpl() throws IOException { - String response = NetworkUtils.doGet(NetworkUtils.toURL(PREFIX + "/category/section/" + section)); + String response = NetworkUtils.doGet(NetworkUtils.toURL(PREFIX + "/api/v2/category/section/" + section)); List categories = JsonUtils.fromNonNullJson(response, new TypeToken>() { }.getType()); return reorganizeCategories(categories, section); @@ -231,9 +232,9 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository private static class FingerprintResponse { private final boolean isCacheBuilt; private final List exactMatches; - private final List exactFingerprints; + private final List exactFingerprints; - public FingerprintResponse(boolean isCacheBuilt, List exactMatches, List exactFingerprints) { + public FingerprintResponse(boolean isCacheBuilt, List exactMatches, List exactFingerprints) { this.isCacheBuilt = isCacheBuilt; this.exactMatches = exactMatches; this.exactFingerprints = exactFingerprints; @@ -247,7 +248,7 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository return exactMatches; } - public List getExactFingerprints() { + public List getExactFingerprints() { return exactFingerprints; } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/AsyncTaskExecutor.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/AsyncTaskExecutor.java index 11f0bb626..e01bc26c0 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/AsyncTaskExecutor.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/AsyncTaskExecutor.java @@ -208,6 +208,11 @@ public final class AsyncTaskExecutor extends TaskExecutor { task.setCancelled(this::isCancelled); task.setState(Task.TaskState.READY); + if (task.getStage() != null) { + task.setInheritedStage(task.getStage()); + } else if (parentTask != null) { + task.setInheritedStage(parentTask.getInheritedStage()); + } task.setNotifyPropertiesChanged(() -> taskListeners.forEach(it -> it.onPropertiesUpdate(task))); if (task.getSignificance().shouldLog()) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Task.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Task.java index c81ed4eb1..e753330d3 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Task.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Task.java @@ -99,6 +99,16 @@ public abstract class Task { this.stage = stage; } + private String inheritedStage = null; + + public String getInheritedStage() { + return inheritedStage; + } + + void setInheritedStage(String inheritedStage) { + this.inheritedStage = inheritedStage; + } + // properties Map properties; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/MurmurHash.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/MurmurHash.java deleted file mode 100644 index 9f9d2e397..000000000 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/MurmurHash.java +++ /dev/null @@ -1,214 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2021 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.util; - -/** - * murmur hash 2.0. - * - * The murmur hash is a relatively fast hash function from - * http://murmurhash.googlepages.com/ for platforms with efficient - * multiplication. - * - * This is a re-implementation of the original C code plus some - * additional features. - * - * Public domain. - * - * @author Viliam Holub - * @version 1.0.2 - * - */ -public class MurmurHash { - - // all methods static; private constructor. - private MurmurHash() { - } - - /** - * Generates 32 bit hash from byte array of the given length and - * seed. - * - * @param data byte array to hash - * @param length length of the array to hash - * @param seed initial seed value - * @return 32 bit hash of the given array - */ - public static int hash32(final byte[] data, int length, int seed) { - // 'm' and 'r' are mixing constants generated offline. - // They're not really 'magic', they just happen to work well. - final int m = 0x5bd1e995; - final int r = 24; - - // Initialize the hash to a random value - int h = seed ^ length; - int length4 = length / 4; - - for (int i = 0; i < length4; i++) { - final int i4 = i * 4; - int k = (data[i4 + 0] & 0xff) + ((data[i4 + 1] & 0xff) << 8) - + ((data[i4 + 2] & 0xff) << 16) + ((data[i4 + 3] & 0xff) << 24); - k *= m; - k ^= k >>> r; - k *= m; - h *= m; - h ^= k; - } - - // Handle the last few bytes of the input array - switch (length % 4) { - case 3: - h ^= (data[(length & ~3) + 2] & 0xff) << 16; - case 2: - h ^= (data[(length & ~3) + 1] & 0xff) << 8; - case 1: - h ^= (data[length & ~3] & 0xff); - h *= m; - } - - h ^= h >>> 13; - h *= m; - h ^= h >>> 15; - - return h; - } - - /** - * Generates 32 bit hash from byte array with default seed value. - * - * @param data byte array to hash - * @param length length of the array to hash - * @return 32 bit hash of the given array - */ - public static int hash32(final byte[] data, int length) { - return hash32(data, length, 0x9747b28c); - } - - /** - * Generates 32 bit hash from a string. - * - * @param text string to hash - * @return 32 bit hash of the given string - */ - public static int hash32(final String text) { - final byte[] bytes = text.getBytes(); - return hash32(bytes, bytes.length); - } - - /** - * Generates 32 bit hash from a substring. - * - * @param text string to hash - * @param from starting index - * @param length length of the substring to hash - * @return 32 bit hash of the given string - */ - public static int hash32(final String text, int from, int length) { - return hash32(text.substring(from, from + length)); - } - - /** - * Generates 64 bit hash from byte array of the given length and seed. - * - * @param data byte array to hash - * @param length length of the array to hash - * @param seed initial seed value - * @return 64 bit hash of the given array - */ - public static long hash64(final byte[] data, int length, int seed) { - final long m = 0xc6a4a7935bd1e995L; - final int r = 47; - - long h = (seed & 0xffffffffl) ^ (length * m); - - int length8 = length / 8; - - for (int i = 0; i < length8; i++) { - final int i8 = i * 8; - long k = ((long) data[i8 + 0] & 0xff) + (((long) data[i8 + 1] & 0xff) << 8) - + (((long) data[i8 + 2] & 0xff) << 16) + (((long) data[i8 + 3] & 0xff) << 24) - + (((long) data[i8 + 4] & 0xff) << 32) + (((long) data[i8 + 5] & 0xff) << 40) - + (((long) data[i8 + 6] & 0xff) << 48) + (((long) data[i8 + 7] & 0xff) << 56); - - k *= m; - k ^= k >>> r; - k *= m; - - h ^= k; - h *= m; - } - - switch (length % 8) { - case 7: - h ^= (long) (data[(length & ~7) + 6] & 0xff) << 48; - case 6: - h ^= (long) (data[(length & ~7) + 5] & 0xff) << 40; - case 5: - h ^= (long) (data[(length & ~7) + 4] & 0xff) << 32; - case 4: - h ^= (long) (data[(length & ~7) + 3] & 0xff) << 24; - case 3: - h ^= (long) (data[(length & ~7) + 2] & 0xff) << 16; - case 2: - h ^= (long) (data[(length & ~7) + 1] & 0xff) << 8; - case 1: - h ^= (long) (data[length & ~7] & 0xff); - h *= m; - } - ; - - h ^= h >>> r; - h *= m; - h ^= h >>> r; - - return h; - } - - /** - * Generates 64 bit hash from byte array with default seed value. - * - * @param data byte array to hash - * @param length length of the array to hash - * @return 64 bit hash of the given string - */ - public static long hash64(final byte[] data, int length) { - return hash64(data, length, 0xe17a1465); - } - - /** - * Generates 64 bit hash from a string. - * - * @param text string to hash - * @return 64 bit hash of the given string - */ - public static long hash64(final String text) { - final byte[] bytes = text.getBytes(); - return hash64(bytes, bytes.length); - } - - /** - * Generates 64 bit hash from a substring. - * - * @param text string to hash - * @param from starting index - * @param length length of the substring to hash - * @return 64 bit hash of the given array - */ - public static long hash64(final String text, int from, int length) { - return hash64(text.substring(from, from + length)); - } -} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/MurmurHash2.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/MurmurHash2.java new file mode 100644 index 000000000..19be699f6 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/MurmurHash2.java @@ -0,0 +1,319 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2021 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.util; + +import java.nio.charset.StandardCharsets; + +/** + * Implementation of the MurmurHash2 32-bit and 64-bit hash functions. + * + *

MurmurHash is a non-cryptographic hash function suitable for general + * hash-based lookup. The name comes from two basic operations, multiply (MU) + * and rotate (R), used in its inner loop. Unlike cryptographic hash functions, + * it is not specifically designed to be difficult to reverse by an adversary, + * making it unsuitable for cryptographic purposes.

+ * + *

This contains a Java port of the 32-bit hash function {@code MurmurHash2} + * and the 64-bit hash function {@code MurmurHash64A} from Austin Applyby's + * original {@code c++} code in SMHasher.

+ * + *

This is a re-implementation of the original C code plus some additional + * features.

+ * + *

This is public domain code with no copyrights. From home page of + * SMHasher:

+ * + *
+ * "All MurmurHash versions are public domain software, and the author + * disclaims all copyright to their code." + *
+ * + * @see MurmurHash + * @see + * Original MurmurHash2 c++ code + * @since 1.13 + */ +public class MurmurHash2 { + + // Constants for 32-bit variant + private static final int M32 = 0x5bd1e995; + private static final int R32 = 24; + + // Constants for 64-bit variant + private static final long M64 = 0xc6a4a7935bd1e995L; + private static final int R64 = 47; + + /** No instance methods. */ + private MurmurHash2() { + } + + /** + * Generates a 32-bit hash from byte array with the given length and seed. + * + * @param data The input byte array + * @param length The length of the array + * @param seed The initial seed value + * @return The 32-bit hash + */ + public static int hash32(final byte[] data, final int length, final int seed) { + // Initialize the hash to a random value + int h = seed ^ length; + + // Mix 4 bytes at a time into the hash + final int nblocks = length >> 2; + + // body + for (int i = 0; i < nblocks; i++) { + final int index = (i << 2); + int k = getLittleEndianInt(data, index); + k *= M32; + k ^= k >>> R32; + k *= M32; + h *= M32; + h ^= k; + } + + // Handle the last few bytes of the input array + final int index = (nblocks << 2); + switch (length - index) { + case 3: + h ^= (data[index + 2] & 0xff) << 16; + case 2: + h ^= (data[index + 1] & 0xff) << 8; + case 1: + h ^= (data[index] & 0xff); + h *= M32; + } + + // Do a few final mixes of the hash to ensure the last few + // bytes are well-incorporated. + h ^= h >>> 13; + h *= M32; + h ^= h >>> 15; + + return h; + } + + /** + * Generates a 32-bit hash from byte array with the given length and a default seed value. + * This is a helper method that will produce the same result as: + * + *
+     * int seed = 0x9747b28c;
+     * int hash = MurmurHash2.hash32(data, length, seed);
+     * 
+ * + * @param data The input byte array + * @param length The length of the array + * @return The 32-bit hash + * @see #hash32(byte[], int, int) + */ + public static int hash32(final byte[] data, final int length) { + return hash32(data, length, 0x9747b28c); + } + + /** + * Generates a 32-bit hash from a string with a default seed. + *

+ * Before 1.14 the string was converted using default encoding. + * Since 1.14 the string is converted to bytes using UTF-8 encoding. + *

+ * This is a helper method that will produce the same result as: + * + *
+     * int seed = 0x9747b28c;
+     * byte[] bytes = data.getBytes(StandardCharsets.UTF_8);
+     * int hash = MurmurHash2.hash32(bytes, bytes.length, seed);
+     * 
+ * + * @param text The input string + * @return The 32-bit hash + * @see #hash32(byte[], int, int) + */ + public static int hash32(final String text) { + final byte[] bytes = text.getBytes(StandardCharsets.UTF_8); + return hash32(bytes, bytes.length); + } + + /** + * Generates a 32-bit hash from a substring with a default seed value. + * The string is converted to bytes using the default encoding. + * This is a helper method that will produce the same result as: + * + *
+     * int seed = 0x9747b28c;
+     * byte[] bytes = text.substring(from, from + length).getBytes(StandardCharsets.UTF_8);
+     * int hash = MurmurHash2.hash32(bytes, bytes.length, seed);
+     * 
+ * + * @param text The input string + * @param from The starting index + * @param length The length of the substring + * @return The 32-bit hash + * @see #hash32(byte[], int, int) + */ + public static int hash32(final String text, final int from, final int length) { + return hash32(text.substring(from, from + length)); + } + + /** + * Generates a 64-bit hash from byte array of the given length and seed. + * + * @param data The input byte array + * @param length The length of the array + * @param seed The initial seed value + * @return The 64-bit hash of the given array + */ + public static long hash64(final byte[] data, final int length, final int seed) { + long h = (seed & 0xffffffffL) ^ (length * M64); + + final int nblocks = length >> 3; + + // body + for (int i = 0; i < nblocks; i++) { + final int index = (i << 3); + long k = getLittleEndianLong(data, index); + + k *= M64; + k ^= k >>> R64; + k *= M64; + + h ^= k; + h *= M64; + } + + final int index = (nblocks << 3); + switch (length - index) { + case 7: + h ^= ((long) data[index + 6] & 0xff) << 48; + case 6: + h ^= ((long) data[index + 5] & 0xff) << 40; + case 5: + h ^= ((long) data[index + 4] & 0xff) << 32; + case 4: + h ^= ((long) data[index + 3] & 0xff) << 24; + case 3: + h ^= ((long) data[index + 2] & 0xff) << 16; + case 2: + h ^= ((long) data[index + 1] & 0xff) << 8; + case 1: + h ^= ((long) data[index] & 0xff); + h *= M64; + } + + h ^= h >>> R64; + h *= M64; + h ^= h >>> R64; + + return h; + } + + /** + * Generates a 64-bit hash from byte array with given length and a default seed value. + * This is a helper method that will produce the same result as: + * + *
+     * int seed = 0xe17a1465;
+     * int hash = MurmurHash2.hash64(data, length, seed);
+     * 
+ * + * @param data The input byte array + * @param length The length of the array + * @return The 64-bit hash + * @see #hash64(byte[], int, int) + */ + public static long hash64(final byte[] data, final int length) { + return hash64(data, length, 0xe17a1465); + } + + /** + * Generates a 64-bit hash from a string with a default seed. + *

+ * Before 1.14 the string was converted using default encoding. + * Since 1.14 the string is converted to bytes using UTF-8 encoding. + *

+ * This is a helper method that will produce the same result as: + * + *
+     * int seed = 0xe17a1465;
+     * byte[] bytes = data.getBytes(StandardCharsets.UTF_8);
+     * int hash = MurmurHash2.hash64(bytes, bytes.length, seed);
+     * 
+ * + * @param text The input string + * @return The 64-bit hash + * @see #hash64(byte[], int, int) + */ + public static long hash64(final String text) { + final byte[] bytes = text.getBytes(StandardCharsets.UTF_8); + return hash64(bytes, bytes.length); + } + + /** + * Generates a 64-bit hash from a substring with a default seed value. + * The string is converted to bytes using the default encoding. + * This is a helper method that will produce the same result as: + * + *
+     * int seed = 0xe17a1465;
+     * byte[] bytes = text.substring(from, from + length).getBytes(StandardCharsets.UTF_8);
+     * int hash = MurmurHash2.hash64(bytes, bytes.length, seed);
+     * 
+ * + * @param text The The input string + * @param from The starting index + * @param length The length of the substring + * @return The 64-bit hash + * @see #hash64(byte[], int, int) + */ + public static long hash64(final String text, final int from, final int length) { + return hash64(text.substring(from, from + length)); + } + + /** + * Gets the little-endian int from 4 bytes starting at the specified index. + * + * @param data The data + * @param index The index + * @return The little-endian int + */ + private static int getLittleEndianInt(final byte[] data, final int index) { + return ((data[index ] & 0xff) ) | + ((data[index + 1] & 0xff) << 8) | + ((data[index + 2] & 0xff) << 16) | + ((data[index + 3] & 0xff) << 24); + } + + /** + * Gets the little-endian long from 8 bytes starting at the specified index. + * + * @param data The data + * @param index The index + * @return The little-endian long + */ + private static long getLittleEndianLong(final byte[] data, final int index) { + return (((long) data[index ] & 0xff) ) | + (((long) data[index + 1] & 0xff) << 8) | + (((long) data[index + 2] & 0xff) << 16) | + (((long) data[index + 3] & 0xff) << 24) | + (((long) data[index + 4] & 0xff) << 32) | + (((long) data[index + 5] & 0xff) << 40) | + (((long) data[index + 6] & 0xff) << 48) | + (((long) data[index + 7] & 0xff) << 56); + } +} diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepositoryTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepositoryTest.java new file mode 100644 index 000000000..bac0b016f --- /dev/null +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepositoryTest.java @@ -0,0 +1,53 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2021 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.mod.curse; + +import org.jackhuang.hmcl.util.MurmurHash2; +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Paths; + +public class CurseForgeRemoteModRepositoryTest { + + @Test + @Ignore + public void testMurmurHash() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (InputStream is = Files.newInputStream(Paths.get("C:\\Users\\huang\\Downloads\\JustEnoughCalculation-1.16.5-3.8.5.jar"))) { + byte[] buf = new byte[1024]; + int len; + while ((len = is.read(buf, 0, buf.length)) > 0) { + for (int i = 0; i < len; i++) { + byte b = buf[i]; + if (b != 9 && b != 10 && b != 13 && b != 32) { + baos.write(b); + } + } + } + + } + long hash = Integer.toUnsignedLong(MurmurHash2.hash32(baos.toByteArray(), baos.size(), 1)); + + Assert.assertEquals(hash, 3333498611L); + } +}