diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java index d015b9dc3..3880ca0bd 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java @@ -32,7 +32,6 @@ import javafx.stage.StageStyle; import org.jackhuang.hmcl.Launcher; import org.jackhuang.hmcl.Metadata; import org.jackhuang.hmcl.download.java.JavaRepository; -import org.jackhuang.hmcl.mod.curse.CurseModManager; import org.jackhuang.hmcl.setting.Accounts; import org.jackhuang.hmcl.setting.EnumCommonDirectory; import org.jackhuang.hmcl.setting.Profiles; @@ -49,9 +48,7 @@ import org.jackhuang.hmcl.ui.main.LauncherSettingsPage; import org.jackhuang.hmcl.ui.main.RootPage; import org.jackhuang.hmcl.ui.multiplayer.MultiplayerPage; import org.jackhuang.hmcl.ui.versions.GameListPage; -import org.jackhuang.hmcl.ui.versions.DownloadListPage; import org.jackhuang.hmcl.ui.versions.VersionPage; -import org.jackhuang.hmcl.ui.versions.Versions; import org.jackhuang.hmcl.util.FutureCallback; import org.jackhuang.hmcl.util.Lazy; import org.jackhuang.hmcl.util.Logging; @@ -87,13 +84,6 @@ public final class Controllers { }); private static Lazy rootPage = new Lazy<>(RootPage::new); private static DecoratorController decorator; - private static Lazy modDownloadListPage = new Lazy<>(() -> { - return new DownloadListPage(CurseModManager.SECTION_MODPACK, Versions::downloadModpackImpl) { - { - state.set(State.fromTitle(i18n("modpack.download"))); - } - }; - }); private static Lazy downloadPage = new Lazy<>(DownloadPage::new); private static Lazy accountListPage = new Lazy<>(() -> { AccountListPage accountListPage = new AccountListPage(); @@ -131,11 +121,6 @@ public final class Controllers { return rootPage.get(); } - // FXThread - public static DownloadListPage getModpackDownloadListPage() { - return modDownloadListPage.get(); - } - // FXThread public static MultiplayerPage getMultiplayerPage() { return multiplayerPage.get(); @@ -318,7 +303,6 @@ public final class Controllers { versionPage = null; gameListPage = null; settingsPage = null; - modDownloadListPage = null; decorator = null; stage = null; scene = null; 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 b1f3a9bda..ec84a86f4 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 @@ -24,8 +24,8 @@ import javafx.scene.Node; import javafx.scene.layout.BorderPane; import org.jackhuang.hmcl.download.*; import org.jackhuang.hmcl.download.game.GameRemoteVersion; -import org.jackhuang.hmcl.mod.DownloadManager; -import org.jackhuang.hmcl.mod.curse.CurseModManager; +import org.jackhuang.hmcl.mod.RemoteModRepository; +import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository; import org.jackhuang.hmcl.setting.DownloadProviders; import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profiles; @@ -81,7 +81,7 @@ public class DownloadPage extends BorderPane implements DecoratorPage { newGameTab.setNodeSupplier(() -> new VersionsPage(versionPageNavigator, i18n("install.installer.choose", i18n("install.installer.game")), "", DownloadProviders.getDownloadProvider(), "game", versionPageNavigator::onGameSelected)); modpackTab.setNodeSupplier(() -> { - DownloadListPage page = new DownloadListPage(CurseModManager.SECTION_MODPACK, Versions::downloadModpackImpl); + DownloadListPage page = new DownloadListPage(CurseForgeRemoteModRepository.MODPACKS, Versions::downloadModpackImpl); JFXButton installLocalModpackButton = new JFXButton(i18n("install.modpack")); installLocalModpackButton.setButtonType(JFXButton.ButtonType.RAISED); @@ -91,10 +91,10 @@ public class DownloadPage extends BorderPane implements DecoratorPage { page.getActions().add(installLocalModpackButton); return page; }); - modTab.setNodeSupplier(() -> new ModDownloadListPage(CurseModManager.SECTION_MOD, (profile, version, file) -> download(profile, version, file, "mods"), true)); - resourcePackTab.setNodeSupplier(() -> new DownloadListPage(CurseModManager.SECTION_RESOURCE_PACK, (profile, version, file) -> download(profile, version, file, "resourcepacks"))); -// customizationTab.setNodeSupplier(() -> new ModDownloadListPage(CurseModManager.SECTION_CUSTOMIZATION, this::download)); - worldTab.setNodeSupplier(() -> new DownloadListPage(CurseModManager.SECTION_WORLD)); + modTab.setNodeSupplier(() -> new ModDownloadListPage((profile, version, file) -> download(profile, version, file, "mods"), true)); + resourcePackTab.setNodeSupplier(() -> new DownloadListPage(CurseForgeRemoteModRepository.RESOURCE_PACKS, (profile, version, file) -> download(profile, version, file, "resourcepacks"))); +// customizationTab.setNodeSupplier(() -> new ModDownloadListPage(CurseModManager.CUSTOMIZATIONS, this::download)); + worldTab.setNodeSupplier(() -> new DownloadListPage(CurseForgeRemoteModRepository.WORLDS)); tab = new TabHeader(newGameTab, modpackTab, modTab, resourcePackTab, worldTab); Profiles.registerVersionsListener(this::loadVersions); @@ -154,7 +154,7 @@ public class DownloadPage extends BorderPane implements DecoratorPage { setCenter(transitionPane); } - private void download(Profile profile, @Nullable String version, DownloadManager.Version file, String subdirectoryName) { + private void download(Profile profile, @Nullable String version, RemoteModRepository.Version file, String subdirectoryName) { if (version == null) version = profile.getSelectedVersion(); Path runDirectory = profile.getRepository().hasVersion(version) ? profile.getRepository().getRunDirectory(version).toPath() : profile.getRepository().getBaseDirectory().toPath(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java index 2bd64e23f..f0c45d787 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java @@ -40,9 +40,7 @@ import javafx.scene.image.ImageView; import javafx.scene.layout.*; import org.jackhuang.hmcl.game.GameVersion; import org.jackhuang.hmcl.game.Version; -import org.jackhuang.hmcl.mod.DownloadManager; -import org.jackhuang.hmcl.mod.curse.CurseAddon; -import org.jackhuang.hmcl.mod.curse.CurseModManager; +import org.jackhuang.hmcl.mod.RemoteModRepository; import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; @@ -55,14 +53,16 @@ import org.jackhuang.hmcl.ui.construct.SpinnerPane; import org.jackhuang.hmcl.ui.construct.TwoLineListItem; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; import org.jackhuang.hmcl.util.AggregatedObservableList; +import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.i18n.I18n; import org.jackhuang.hmcl.util.javafx.BindingMapping; import java.io.File; -import java.util.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; -import java.util.stream.Stream; import static org.jackhuang.hmcl.ui.FXUtils.stringConverter; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; @@ -74,7 +74,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP private final BooleanProperty failed = new SimpleBooleanProperty(false); private final boolean versionSelection; private final ObjectProperty version = new SimpleObjectProperty<>(); - private final ListProperty items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList()); + private final ListProperty items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList()); private final ObservableList versions = FXCollections.observableArrayList(); private final StringProperty selectedVersion = new SimpleStringProperty(); private final DownloadPage.DownloadCallback callback; @@ -85,23 +85,18 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP protected final StringProperty downloadSource = new SimpleStringProperty(); private final WeakListenerHolder listenerHolder = new WeakListenerHolder(); private TaskExecutor executor; + protected RemoteModRepository repository; - /** - * @see org.jackhuang.hmcl.mod.curse.CurseModManager#SECTION_MODPACK - * @see org.jackhuang.hmcl.mod.curse.CurseModManager#SECTION_MOD - */ - private final int section; - - public DownloadListPage(int section) { - this(section, null); + public DownloadListPage(RemoteModRepository repository) { + this(repository, null); } - public DownloadListPage(int section, DownloadPage.DownloadCallback callback) { - this(section, callback, false); + public DownloadListPage(RemoteModRepository repository, DownloadPage.DownloadCallback callback) { + this(repository, callback, false); } - public DownloadListPage(int section, DownloadPage.DownloadCallback callback, boolean versionSelection) { - this.section = section; + public DownloadListPage(RemoteModRepository repository, DownloadPage.DownloadCallback callback, boolean versionSelection) { + this.repository = repository; this.callback = callback; this.versionSelection = versionSelection; } @@ -119,7 +114,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP if (!searchInitialized) { searchInitialized = true; - search("", 0, 0, "", 0); + search("", null, 0, "", 0); } if (versionSelection) { @@ -154,7 +149,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP this.loading.set(loading); } - public void search(String userGameVersion, int category, int pageOffset, String searchFilter, int sort) { + public void search(String userGameVersion, RemoteModRepository.Category category, int pageOffset, String searchFilter, int sort) { setLoading(true); setFailed(false); File versionJar = StringUtils.isNotBlank(version.get().getVersion()) @@ -173,7 +168,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP } return gameVersion; }).thenApplyAsync(gameVersion -> { - return searchImpl(gameVersion, category, section, pageOffset, searchFilter, sort); + return repository.search(gameVersion, category, pageOffset, 50, searchFilter, sort); }).whenComplete(Schedulers.javafx(), (result, exception) -> { setLoading(false); if (exception == null) { @@ -185,14 +180,14 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP }).executor(true); } - protected Stream searchImpl(String gameVersion, int category, int section, int pageOffset, String searchFilter, int sort) throws Exception { - return CurseModManager.searchPaginated(gameVersion, category, section, pageOffset, searchFilter, sort).stream().map(CurseAddon::toMod); - } - protected String getLocalizedCategory(String category) { return i18n("curse.category." + category); } + protected String getLocalizedCategoryIndent(ModDownloadListPageSkin.CategoryIndented category) { + return StringUtils.repeats(' ', category.indent * 4) + getLocalizedCategory(category.getCategory() == null ? "0" : category.getCategory().getId()); + } + protected String getLocalizedOfficialPage() { return i18n("mods.curseforge"); } @@ -263,7 +258,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP JFXComboBox gameVersionField = new JFXComboBox<>(); gameVersionField.setMaxWidth(Double.MAX_VALUE); gameVersionField.setEditable(true); - gameVersionField.getItems().setAll(DownloadManager.DEFAULT_GAME_VERSIONS); + gameVersionField.getItems().setAll(RemoteModRepository.DEFAULT_GAME_VERSIONS); Label lblGameVersion = new Label(i18n("world.game_version")); searchPane.addRow(rowIndex++, new Label(i18n("mods.name")), nameField, lblGameVersion, gameVersionField); @@ -284,17 +279,18 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP StackPane categoryStackPane = new StackPane(); JFXComboBox categoryComboBox = new JFXComboBox<>(); - categoryComboBox.getItems().setAll(new CategoryIndented(0, 0)); + categoryComboBox.getItems().setAll(new CategoryIndented(0, null)); categoryStackPane.getChildren().setAll(categoryComboBox); categoryComboBox.prefWidthProperty().bind(categoryStackPane.widthProperty()); categoryComboBox.getStyleClass().add("fit-width"); categoryComboBox.setPromptText(i18n("mods.category")); categoryComboBox.getSelectionModel().select(0); - Task.supplyAsync(() -> CurseModManager.getCategories(getSkinnable().section)) + categoryComboBox.setConverter(stringConverter(getSkinnable()::getLocalizedCategoryIndent)); + Task.supplyAsync(() -> getSkinnable().repository.getCategories()) .thenAcceptAsync(Schedulers.javafx(), categories -> { List result = new ArrayList<>(); - result.add(new CategoryIndented(0, 0)); - for (CurseModManager.Category category : categories) { + result.add(new CategoryIndented(0, null)); + for (RemoteModRepository.Category category : Lang.toIterable(categories)) { resolveCategory(category, 0, result); } categoryComboBox.getItems().setAll(result); @@ -331,8 +327,8 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP EventHandler searchAction = e -> getSkinnable() .search(gameVersionField.getSelectionModel().getSelectedItem(), Optional.ofNullable(categoryComboBox.getSelectionModel().getSelectedItem()) - .map(CategoryIndented::getCategoryId) - .orElse(0), + .map(CategoryIndented::getCategory) + .orElse(null), 0, nameField.getText(), sortComboBox.getSelectionModel().getSelectedIndex()); @@ -355,16 +351,16 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP } }, getSkinnable().failedProperty())); - JFXListView listView = new JFXListView<>(); + JFXListView listView = new JFXListView<>(); spinnerPane.setContent(listView); Bindings.bindContent(listView.getItems(), getSkinnable().items); listView.setOnMouseClicked(e -> { if (listView.getSelectionModel().getSelectedIndex() < 0) return; - DownloadManager.Mod selectedItem = listView.getSelectionModel().getSelectedItem(); + RemoteModRepository.Mod selectedItem = listView.getSelectionModel().getSelectedItem(); Controllers.navigate(new DownloadPage(getSkinnable(), selectedItem, getSkinnable().getProfileVersion(), getSkinnable().callback)); }); - listView.setCellFactory(x -> new FloatListCell(listView) { + listView.setCellFactory(x -> new FloatListCell(listView) { TwoLineListItem content = new TwoLineListItem(); ImageView imageView = new ImageView(); @@ -377,7 +373,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP } @Override - protected void updateControl(DownloadManager.Mod dataItem, boolean empty) { + protected void updateControl(RemoteModRepository.Mod dataItem, boolean empty) { if (empty) return; ModTranslations.Mod mod = ModTranslations.getModByCurseForgeId(dataItem.getSlug()); content.setTitle(mod != null ? mod.getDisplayName() : dataItem.getTitle()); @@ -398,30 +394,25 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP private static class CategoryIndented { private final int indent; - private final int categoryId; + private final RemoteModRepository.Category category; - public CategoryIndented(int indent, int categoryId) { + public CategoryIndented(int indent, RemoteModRepository.Category category) { this.indent = indent; - this.categoryId = categoryId; + this.category = category; } public int getIndent() { return indent; } - public int getCategoryId() { - return categoryId; - } - - @Override - public String toString() { - return StringUtils.repeats(' ', indent) + i18n("curse.category." + categoryId); + public RemoteModRepository.Category getCategory() { + return category; } } - private static void resolveCategory(CurseModManager.Category category, int indent, List result) { - result.add(new CategoryIndented(indent, category.getId())); - for (CurseModManager.Category subcategory : category.getSubcategories()) { + private static void resolveCategory(RemoteModRepository.Category category, int indent, List result) { + result.add(new CategoryIndented(indent, category)); + for (RemoteModRepository.Category subcategory : category.getSubcategories()) { resolveCategory(subcategory, indent + 1, result); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index 2fd58013e..72e8e3e2e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -36,8 +36,8 @@ import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.*; import javafx.stage.FileChooser; -import org.jackhuang.hmcl.mod.DownloadManager; import org.jackhuang.hmcl.mod.ModManager; +import org.jackhuang.hmcl.mod.RemoteModRepository; import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Theme; import org.jackhuang.hmcl.task.FileDownloadTask; @@ -69,16 +69,16 @@ public class DownloadPage extends Control implements DecoratorPage { private final BooleanProperty loaded = new SimpleBooleanProperty(false); private final BooleanProperty loading = new SimpleBooleanProperty(false); private final BooleanProperty failed = new SimpleBooleanProperty(false); - private final DownloadManager.Mod addon; + private final RemoteModRepository.Mod addon; private final ModTranslations.Mod mod; private final Profile.ProfileVersion version; private final DownloadCallback callback; private final DownloadListPage page; - private List dependencies; - private SimpleMultimap versions; + private List dependencies; + private SimpleMultimap versions; - public DownloadPage(DownloadListPage page, DownloadManager.Mod addon, Profile.ProfileVersion version, @Nullable DownloadCallback callback) { + public DownloadPage(DownloadListPage page, RemoteModRepository.Mod addon, Profile.ProfileVersion version, @Nullable DownloadCallback callback) { this.page = page; this.addon = addon; this.mod = ModTranslations.getModByCurseForgeId(addon.getSlug()); @@ -95,7 +95,7 @@ public class DownloadPage extends Control implements DecoratorPage { Task.allOf( Task.supplyAsync(() -> addon.getData().loadDependencies()), Task.supplyAsync(() -> { - Stream versions = addon.getData().loadVersions(); + Stream versions = addon.getData().loadVersions(); // if (StringUtils.isNotBlank(version.getVersion())) { // Optional gameVersion = GameVersion.minecraftVersion(versionJar); // if (gameVersion.isPresent()) { @@ -108,9 +108,9 @@ public class DownloadPage extends Control implements DecoratorPage { .whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null) { @SuppressWarnings("unchecked") - List dependencies = (List) result.get(0); + List dependencies = (List) result.get(0); @SuppressWarnings("unchecked") - SimpleMultimap versions = (SimpleMultimap) result.get(1); + SimpleMultimap versions = (SimpleMultimap) result.get(1); this.dependencies = dependencies; this.versions = versions; @@ -126,9 +126,9 @@ public class DownloadPage extends Control implements DecoratorPage { this.state.set(State.fromTitle(addon.getTitle())); } - private SimpleMultimap sortVersions(Stream versions) { - SimpleMultimap classifiedVersions - = new SimpleMultimap(HashMap::new, ArrayList::new); + private SimpleMultimap sortVersions(Stream versions) { + SimpleMultimap classifiedVersions + = new SimpleMultimap(HashMap::new, ArrayList::new); versions.forEach(version -> { for (String gameVersion : version.getGameVersions()) { classifiedVersions.put(gameVersion, version); @@ -136,13 +136,13 @@ public class DownloadPage extends Control implements DecoratorPage { }); for (String gameVersion : classifiedVersions.keys()) { - List versionList = (List) classifiedVersions.get(gameVersion); - versionList.sort(Comparator.comparing(DownloadManager.Version::getDatePublished).reversed()); + List versionList = (List) classifiedVersions.get(gameVersion); + versionList.sort(Comparator.comparing(RemoteModRepository.Version::getDatePublished).reversed()); } return classifiedVersions; } - public DownloadManager.Mod getAddon() { + public RemoteModRepository.Mod getAddon() { return addon; } @@ -174,7 +174,7 @@ public class DownloadPage extends Control implements DecoratorPage { this.failed.set(failed); } - public void download(DownloadManager.Version file) { + public void download(RemoteModRepository.Version file) { if (this.callback == null) { saveAs(file); } else { @@ -182,7 +182,7 @@ public class DownloadPage extends Control implements DecoratorPage { } } - public void saveAs(DownloadManager.Version file) { + public void saveAs(RemoteModRepository.Version file) { String extension = StringUtils.substringAfterLast(file.getFile().getFilename(), '.'); FileChooser fileChooser = new FileChooser(); @@ -328,7 +328,7 @@ public class DownloadPage extends Control implements DecoratorPage { private static final class DependencyModItem extends StackPane { - DependencyModItem(DownloadListPage page, DownloadManager.Mod addon, Profile.ProfileVersion version, DownloadCallback callback) { + DependencyModItem(DownloadListPage page, RemoteModRepository.Mod addon, Profile.ProfileVersion version, DownloadCallback callback) { HBox pane = new HBox(8); pane.setPadding(new Insets(8)); pane.setAlignment(Pos.CENTER_LEFT); @@ -355,7 +355,7 @@ public class DownloadPage extends Control implements DecoratorPage { } private static final class ModItem extends StackPane { - ModItem(DownloadManager.Version dataItem, DownloadPage selfPage) { + ModItem(RemoteModRepository.Version dataItem, DownloadPage selfPage) { HBox pane = new HBox(8); pane.setPadding(new Insets(8)); pane.setAlignment(Pos.CENTER_LEFT); @@ -399,6 +399,6 @@ public class DownloadPage extends Control implements DecoratorPage { private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL).withLocale(Locale.getDefault()).withZone(ZoneId.systemDefault()); public interface DownloadCallback { - void download(Profile profile, @Nullable String version, DownloadManager.Version file); + void download(Profile profile, @Nullable String version, RemoteModRepository.Version file); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModDownloadListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModDownloadListPage.java index 58a19d2d5..01794aeff 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModDownloadListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModDownloadListPage.java @@ -17,55 +17,76 @@ */ package org.jackhuang.hmcl.ui.versions; -import org.jackhuang.hmcl.mod.DownloadManager; -import org.jackhuang.hmcl.mod.curse.CurseAddon; -import org.jackhuang.hmcl.mod.curse.CurseModManager; -import org.jackhuang.hmcl.mod.modrinth.Modrinth; +import org.jackhuang.hmcl.mod.RemoteModRepository; +import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository; +import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository; import org.jackhuang.hmcl.util.StringUtils; +import java.io.IOException; +import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.stream.Stream; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public class ModDownloadListPage extends DownloadListPage { - public ModDownloadListPage(int section, DownloadPage.DownloadCallback callback, boolean versionSelection) { - super(section, callback, versionSelection); + public ModDownloadListPage(DownloadPage.DownloadCallback callback, boolean versionSelection) { + super(null, callback, versionSelection); + + repository = new Repository(); + supportChinese.set(true); downloadSources.get().setAll("mods.curseforge", "mods.modrinth"); downloadSource.set("mods.curseforge"); } - @Override - protected Stream searchImpl(String gameVersion, int category, int section, int pageOffset, String searchFilter, int sort) throws Exception { - if (StringUtils.CHINESE_PATTERN.matcher(searchFilter).find()) { - List mods = ModTranslations.searchMod(searchFilter); - List searchFilters = new ArrayList<>(); - int count = 0; - for (ModTranslations.Mod mod : mods) { - String englishName = mod.getName(); - if (StringUtils.isNotBlank(mod.getSubname())) { - englishName = mod.getSubname(); + private class Repository implements RemoteModRepository { + + @Override + public Stream search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, int sort) throws IOException { + String newSearchFilter; + if (StringUtils.CHINESE_PATTERN.matcher(searchFilter).find()) { + List mods = ModTranslations.searchMod(searchFilter); + List searchFilters = new ArrayList<>(); + int count = 0; + for (ModTranslations.Mod mod : mods) { + String englishName = mod.getName(); + if (StringUtils.isNotBlank(mod.getSubname())) { + englishName = mod.getSubname(); + } + + searchFilters.add(englishName); + + count++; + if (count >= 3) break; } - - searchFilters.add(englishName); - - count++; - if (count >= 3) break; + newSearchFilter = String.join(" ", searchFilters); + } else { + newSearchFilter = searchFilter; } - return search(gameVersion, category, section, pageOffset, String.join(" ", searchFilters), sort); - } else { - return search(gameVersion, category, section, pageOffset, searchFilter, sort); - } - } - private Stream search(String gameVersion, int category, int section, int pageOffset, String searchFilter, int sort) throws Exception { - if ("mods.modrinth".equals(downloadSource.get())) { - return Modrinth.searchPaginated(gameVersion, pageOffset, searchFilter).stream().map(Modrinth.ModResult::toMod); - } else { - return CurseModManager.searchPaginated(gameVersion, category, section, pageOffset, searchFilter, sort).stream().map(CurseAddon::toMod); + if ("mods.modrinth".equals(downloadSource.get())) { + return ModrinthRemoteModRepository.INSTANCE.search(gameVersion, category, pageOffset, pageSize, newSearchFilter, sort); + } else { + return CurseForgeRemoteModRepository.MODS.search(gameVersion, category, pageOffset, pageSize, newSearchFilter, sort); + } + } + + @Override + public Optional getRemoteVersionByLocalFile(Path file) { + throw new UnsupportedOperationException(); + } + + @Override + public Stream getCategories() throws IOException { + if ("mods.modrinth".equals(downloadSource.get())) { + return ModrinthRemoteModRepository.INSTANCE.getCategories(); + } else { + return CurseForgeRemoteModRepository.MODS.getCategories(); + } } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java index 2999f1e8f..5a6a2d5c8 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java @@ -25,7 +25,7 @@ import org.jackhuang.hmcl.download.game.GameAssetDownloadTask; import org.jackhuang.hmcl.game.GameDirectoryType; import org.jackhuang.hmcl.game.GameRepository; import org.jackhuang.hmcl.game.LauncherHelper; -import org.jackhuang.hmcl.mod.DownloadManager; +import org.jackhuang.hmcl.mod.RemoteModRepository; import org.jackhuang.hmcl.setting.Accounts; import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profiles; @@ -77,15 +77,7 @@ public final class Versions { } } - public static void downloadModpack() { - Profile profile = Profiles.getSelectedProfile(); - if (profile.getRepository().isLoaded()) { - Controllers.getModpackDownloadListPage().loadVersion(profile, null); - Controllers.navigate(Controllers.getModpackDownloadListPage()); - } - } - - public static void downloadModpackImpl(Profile profile, String version, DownloadManager.Version file) { + public static void downloadModpackImpl(Profile profile, String version, RemoteModRepository.Version file) { Path modpack; URL downloadURL; try { @@ -101,7 +93,7 @@ public final class Versions { new FileDownloadTask(downloadURL, modpack.toFile()) .whenComplete(Schedulers.javafx(), e -> { if (e == null) { - Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack.toFile())); + Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(profile, modpack.toFile())); } else { Controllers.dialog( i18n("install.failed.downloading.detail", file.getFile().getUrl()) + "\n" + StringUtils.getStackTrace(e), diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/DownloadManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java similarity index 83% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/mod/DownloadManager.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java index 431b4339c..c897a5455 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/DownloadManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/RemoteModRepository.java @@ -18,22 +18,53 @@ package org.jackhuang.hmcl.mod; import java.io.IOException; +import java.nio.file.Path; import java.time.Instant; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Stream; -public final class DownloadManager { - private DownloadManager() { - } +public interface RemoteModRepository { - public interface IMod { + Stream search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, int sort) + throws IOException; + + Optional getRemoteVersionByLocalFile(Path file) throws IOException; + + Stream getCategories() throws IOException; + + interface IMod { List loadDependencies() throws IOException; Stream loadVersions() throws IOException; } - public static class Mod { + class Category { + private final Object self; + private final String id; + private final List subcategories; + + public Category(Object self, String id, List subcategories) { + this.self = self; + this.id = id; + this.subcategories = subcategories; + } + + public Object getSelf() { + return self; + } + + public String getId() { + return id; + } + + public List getSubcategories() { + return subcategories; + } + } + + class Mod { private final String slug; private final String author; private final String title; @@ -87,13 +118,13 @@ public final class DownloadManager { } } - public enum VersionType { + enum VersionType { Release, Beta, Alpha } - public static class Version { + class Version { private final Object self; private final String name; private final String version; @@ -159,7 +190,7 @@ public final class DownloadManager { } } - public static class File { + class File { private final Map hashes; private final String url; private final String filename; @@ -183,7 +214,7 @@ public final class DownloadManager { } } - public static final String[] DEFAULT_GAME_VERSIONS = new String[]{ + String[] DEFAULT_GAME_VERSIONS = new String[]{ "1.17.1", "1.17", "1.16.5", "1.16.4", "1.16.3", "1.16.2", "1.16.1", "1.16", "1.15.2", "1.15.1", "1.15", 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 6a5a2a075..b3c5cbeaa 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,7 +17,7 @@ */ package org.jackhuang.hmcl.mod.curse; -import org.jackhuang.hmcl.mod.DownloadManager; +import org.jackhuang.hmcl.mod.RemoteModRepository; import org.jackhuang.hmcl.util.Immutable; import java.io.IOException; @@ -30,7 +30,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; @Immutable -public class CurseAddon implements DownloadManager.IMod { +public class CurseAddon implements RemoteModRepository.IMod { private final int id; private final String name; private final List authors; @@ -39,6 +39,7 @@ public class CurseAddon implements DownloadManager.IMod { private final int gameId; private final String summary; private final int defaultFileId; + private final LatestFile file; private final List latestFiles; private final List categories; private final int status; @@ -53,7 +54,7 @@ public class CurseAddon implements DownloadManager.IMod { private final boolean isAvailable; private final boolean isExperimental; - public CurseAddon(int id, String name, List authors, List attachments, String websiteUrl, int gameId, String summary, int defaultFileId, List latestFiles, List categories, int status, int primaryCategoryId, String slug, List gameVersionLatestFiles, boolean isFeatured, double popularityScore, int gamePopularityRank, String primaryLanguage, List modLoaders, boolean isAvailable, boolean isExperimental) { + public CurseAddon(int id, String name, List authors, List attachments, String websiteUrl, int gameId, String summary, int defaultFileId, LatestFile file, List latestFiles, List categories, int status, int primaryCategoryId, String slug, List gameVersionLatestFiles, boolean isFeatured, double popularityScore, int gamePopularityRank, String primaryLanguage, List modLoaders, boolean isAvailable, boolean isExperimental) { this.id = id; this.name = name; this.authors = authors; @@ -62,6 +63,7 @@ public class CurseAddon implements DownloadManager.IMod { this.gameId = gameId; this.summary = summary; this.defaultFileId = defaultFileId; + this.file = file; this.latestFiles = latestFiles; this.categories = categories; this.status = status; @@ -109,6 +111,10 @@ public class CurseAddon implements DownloadManager.IMod { return defaultFileId; } + public LatestFile getFile() { + return file; + } + public List getLatestFiles() { return latestFiles; } @@ -162,26 +168,26 @@ public class CurseAddon implements DownloadManager.IMod { } @Override - public List loadDependencies() throws IOException { + public List loadDependencies() throws IOException { Set dependencies = latestFiles.stream() .flatMap(latestFile -> latestFile.getDependencies().stream()) .filter(dep -> dep.getType() == 3) .map(Dependency::getAddonId) .collect(Collectors.toSet()); - List mods = new ArrayList<>(); + List mods = new ArrayList<>(); for (int dependencyId : dependencies) { - mods.add(CurseModManager.getAddon(dependencyId).toMod()); + mods.add(CurseForgeRemoteModRepository.MODS.getAddon(dependencyId).toMod()); } return mods; } @Override - public Stream loadVersions() throws IOException { - return CurseModManager.getFiles(this).stream() + public Stream loadVersions() throws IOException { + return CurseForgeRemoteModRepository.MODS.getFiles(this).stream() .map(CurseAddon.LatestFile::toVersion); } - public DownloadManager.Mod toMod() { + public RemoteModRepository.Mod toMod() { String iconUrl = null; for (CurseAddon.Attachment attachment : attachments) { if (attachment.isDefault()) { @@ -189,7 +195,7 @@ public class CurseAddon implements DownloadManager.IMod { } } - return new DownloadManager.Mod( + return new RemoteModRepository.Mod( slug, "", name, @@ -475,31 +481,31 @@ public class CurseAddon implements DownloadManager.IMod { return fileDataInstant; } - public DownloadManager.Version toVersion() { - DownloadManager.VersionType versionType; + public RemoteModRepository.Version toVersion() { + RemoteModRepository.VersionType versionType; switch (getReleaseType()) { case 1: - versionType = DownloadManager.VersionType.Release; + versionType = RemoteModRepository.VersionType.Release; break; case 2: - versionType = DownloadManager.VersionType.Beta; + versionType = RemoteModRepository.VersionType.Beta; break; case 3: - versionType = DownloadManager.VersionType.Alpha; + versionType = RemoteModRepository.VersionType.Alpha; break; default: - versionType = DownloadManager.VersionType.Release; + versionType = RemoteModRepository.VersionType.Release; break; } - return new DownloadManager.Version( + return new RemoteModRepository.Version( this, getDisplayName(), null, null, getParsedFileDate(), versionType, - new DownloadManager.File(Collections.emptyMap(), getDownloadUrl(), getFileName()), + new RemoteModRepository.File(Collections.emptyMap(), getDownloadUrl(), getFileName()), Collections.emptyList(), gameVersion.stream().filter(ver -> ver.startsWith("1.") || ver.contains("w")).collect(Collectors.toList()), Collections.emptyList() diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseModManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java similarity index 53% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseModManager.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java index fb733bf57..8c84fa8d9 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseModManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseForgeRemoteModRepository.java @@ -18,27 +18,43 @@ package org.jackhuang.hmcl.mod.curse; import com.google.gson.reflect.TypeToken; +import org.jackhuang.hmcl.mod.RemoteModRepository; +import org.jackhuang.hmcl.util.MurmurHash; 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.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; import static org.jackhuang.hmcl.util.Lang.mapOf; import static org.jackhuang.hmcl.util.Pair.pair; -public final class CurseModManager { - private CurseModManager() { +public final class CurseForgeRemoteModRepository implements RemoteModRepository { + + private static final String PREFIX = "https://addons-ecs.forgesvc.net/api/v2"; + + private final int section; + + public CurseForgeRemoteModRepository(int section) { + this.section = section; } - public static List searchPaginated(String gameVersion, int category, int section, int pageOffset, String searchFilter, int sort) throws IOException { - String response = NetworkUtils.doGet(new URL(NetworkUtils.withQuery("https://addons-ecs.forgesvc.net/api/v2/addon/search", mapOf( + 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( pair("categoryId", Integer.toString(category)), pair("gameId", "432"), pair("gameVersion", gameVersion), pair("index", Integer.toString(pageOffset)), - pair("pageSize", "50"), + pair("pageSize", Integer.toString(pageSize)), pair("searchFilter", searchFilter), pair("sectionId", Integer.toString(section)), pair("sort", Integer.toString(sort)) @@ -47,25 +63,63 @@ public final class CurseModManager { }.getType()); } - public static CurseAddon getAddon(int id) throws IOException { - String response = NetworkUtils.doGet(NetworkUtils.toURL("https://addons-ecs.forgesvc.net/api/v2/addon/" + id)); + @Override + public Stream search(String gameVersion, RemoteModRepository.Category category, int pageOffset, int pageSize, String searchFilter, int sort) throws IOException { + int categoryId = 0; + if (category != null) categoryId = ((Category) category.getSelf()).getId(); + return searchPaginated(gameVersion, categoryId, pageOffset, pageSize, searchFilter, sort).stream() + .map(CurseAddon::toMod); + } + + @Override + public Optional getRemoteVersionByLocalFile(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); + } + } + } + + int hash = MurmurHash.hash32(baos.toByteArray(), baos.size(), 1); + + FingerprintResponse response = HttpRequest.POST(PREFIX + "/fingerprint") + .json(Collections.singletonList(hash)) + .getJson(FingerprintResponse.class); + + if (response.getExactMatches() == null || response.getExactMatches().isEmpty()) { + return Optional.empty(); + } + + return Optional.of(response.getExactMatches().get(0).getFile().toVersion()); + } + + public CurseAddon getAddon(int id) throws IOException { + String response = NetworkUtils.doGet(NetworkUtils.toURL(PREFIX + "/addon/" + id)); return JsonUtils.fromNonNullJson(response, CurseAddon.class); } - public static List getFiles(CurseAddon addon) throws IOException { - String response = NetworkUtils.doGet(NetworkUtils.toURL("https://addons-ecs.forgesvc.net/api/v2/addon/" + addon.getId() + "/files")); + public List getFiles(CurseAddon addon) throws IOException { + String response = NetworkUtils.doGet(NetworkUtils.toURL(PREFIX + "/addon/" + addon.getId() + "/files")); return JsonUtils.fromNonNullJson(response, new TypeToken>() { }.getType()); } - public static List getCategories(int section) throws IOException { - String response = NetworkUtils.doGet(NetworkUtils.toURL("https://addons-ecs.forgesvc.net/api/v2/category/section/" + section)); + public List getCategoriesImpl() throws IOException { + String response = NetworkUtils.doGet(NetworkUtils.toURL(PREFIX + "/category/section/" + section)); List categories = JsonUtils.fromNonNullJson(response, new TypeToken>() { }.getType()); return reorganizeCategories(categories, section); } - private static List reorganizeCategories(List categories, int rootId) { + @Override + public Stream getCategories() throws IOException { + return getCategoriesImpl().stream().map(Category::toCategory); + } + + private List reorganizeCategories(List categories, int rootId) { List result = new ArrayList<>(); Map categoryMap = new HashMap<>(); @@ -98,6 +152,11 @@ public final class CurseModManager { public static final int SECTION_UNKNOWN2 = 4979; public static final int SECTION_UNKNOWN3 = 4984; + public static final CurseForgeRemoteModRepository MODS = new CurseForgeRemoteModRepository(SECTION_MOD); + public static final CurseForgeRemoteModRepository MODPACKS = new CurseForgeRemoteModRepository(SECTION_MODPACK); + public static final CurseForgeRemoteModRepository RESOURCE_PACKS = new CurseForgeRemoteModRepository(SECTION_RESOURCE_PACK); + public static final CurseForgeRemoteModRepository WORLDS = new CurseForgeRemoteModRepository(SECTION_WORLD); + public static class Category { private final int id; private final String name; @@ -155,5 +214,36 @@ public final class CurseModManager { public List getSubcategories() { return subcategories; } + + public RemoteModRepository.Category toCategory() { + return new RemoteModRepository.Category( + this, + Integer.toString(id), + getSubcategories().stream().map(Category::toCategory).collect(Collectors.toList())); + } + } + + private static class FingerprintResponse { + private final boolean isCacheBuilt; + private final List exactMatches; + private final List exactFingerprints; + + public FingerprintResponse(boolean isCacheBuilt, List exactMatches, List exactFingerprints) { + this.isCacheBuilt = isCacheBuilt; + this.exactMatches = exactMatches; + this.exactFingerprints = exactFingerprints; + } + + public boolean isCacheBuilt() { + return isCacheBuilt; + } + + public List getExactMatches() { + return exactMatches; + } + + public List getExactFingerprints() { + return exactFingerprints; + } } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/Modrinth.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java similarity index 78% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/Modrinth.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java index 07de4339c..6310bad75 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/Modrinth.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java @@ -19,51 +19,89 @@ package org.jackhuang.hmcl.mod.modrinth; import com.google.gson.annotations.SerializedName; import com.google.gson.reflect.TypeToken; -import org.jackhuang.hmcl.mod.DownloadManager; +import org.jackhuang.hmcl.mod.RemoteModRepository; +import org.jackhuang.hmcl.util.DigestUtils; +import org.jackhuang.hmcl.util.Hex; import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.io.HttpRequest; import org.jackhuang.hmcl.util.io.NetworkUtils; +import org.jackhuang.hmcl.util.io.ResponseCodeException; import java.io.IOException; +import java.nio.file.Path; import java.time.Instant; -import java.util.*; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; import java.util.stream.Stream; import static org.jackhuang.hmcl.util.Lang.mapOf; import static org.jackhuang.hmcl.util.Pair.pair; -public final class Modrinth { - private Modrinth() { +public final class ModrinthRemoteModRepository implements RemoteModRepository { + public static final ModrinthRemoteModRepository INSTANCE = new ModrinthRemoteModRepository(); + + private static final String PREFIX = "https://api.modrinth.com"; + + private ModrinthRemoteModRepository() { } - public static List searchPaginated(String gameVersion, int pageOffset, String searchFilter) throws IOException { + public List searchPaginated(String gameVersion, int pageOffset, int pageSize, String searchFilter) throws IOException { Map query = mapOf( pair("query", searchFilter), pair("offset", Integer.toString(pageOffset)), - pair("limit", "50") + pair("limit", Integer.toString(pageSize)) ); if (StringUtils.isNotBlank(gameVersion)) { query.put("version", "versions=" + gameVersion); } - Response response = HttpRequest.GET(NetworkUtils.withQuery("https://api.modrinth.com/api/v1/mod", query)) + Response response = HttpRequest.GET(NetworkUtils.withQuery(PREFIX + "/api/v1/mod", query)) .getJson(new TypeToken>() { }.getType()); return response.getHits(); } - public static List getFiles(ModResult mod) throws IOException { - String id = StringUtils.removePrefix(mod.getModId(), "local-"); - List versions = HttpRequest.GET("https://api.modrinth.com/api/v1/mod/" + id + "/version") - .getJson(new TypeToken>() { - }.getType()); - return versions; + @Override + public Stream search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, int sort) throws IOException { + return searchPaginated(gameVersion, pageOffset, pageSize, searchFilter).stream() + .map(ModResult::toMod); } - public static List getCategories() throws IOException { - List categories = HttpRequest.GET("https://api.modrinth.com/api/v1/tag/category").getJson(new TypeToken>() { + @Override + public Optional getRemoteVersionByLocalFile(Path file) throws IOException { + String sha1 = Hex.encodeHex(DigestUtils.digest("SHA-1", file)); + + try { + ModVersion mod = HttpRequest.GET(PREFIX + "/api/v1/version_file/" + sha1, + pair("algorithm", "sha1")) + .getJson(ModVersion.class); + return mod.toVersion(); + } catch (ResponseCodeException e) { + if (e.getResponseCode() == 404) { + return Optional.empty(); + } else { + throw e; + } + } + } + + public List getFiles(ModResult mod) throws IOException { + String id = StringUtils.removePrefix(mod.getModId(), "local-"); + return HttpRequest.GET("https://api.modrinth.com/api/v1/mod/" + id + "/version") + .getJson(new TypeToken>() { + }.getType()); + } + + public List getCategoriesImpl() throws IOException { + return HttpRequest.GET("https://api.modrinth.com/api/v1/tag/category").getJson(new TypeToken>() { }.getType()); - return categories; + } + + public Stream getCategories() throws IOException { + return getCategoriesImpl().stream() + .map(name -> new Category(null, name, Collections.emptyList())); } public static class Mod { @@ -250,23 +288,23 @@ public final class Modrinth { return loaders; } - public Optional toVersion() { - DownloadManager.VersionType type; + public Optional toVersion() { + RemoteModRepository.VersionType type; if ("release".equals(versionType)) { - type = DownloadManager.VersionType.Release; + type = RemoteModRepository.VersionType.Release; } else if ("beta".equals(versionType)) { - type = DownloadManager.VersionType.Beta; + type = RemoteModRepository.VersionType.Beta; } else if ("alpha".equals(versionType)) { - type = DownloadManager.VersionType.Alpha; + type = RemoteModRepository.VersionType.Alpha; } else { - type = DownloadManager.VersionType.Release; + type = RemoteModRepository.VersionType.Release; } if (files.size() == 0) { return Optional.empty(); } - return Optional.of(new DownloadManager.Version( + return Optional.of(new RemoteModRepository.Version( this, name, versionNumber, @@ -304,12 +342,12 @@ public final class Modrinth { return filename; } - public DownloadManager.File toFile() { - return new DownloadManager.File(hashes, url, filename); + public RemoteModRepository.File toFile() { + return new RemoteModRepository.File(hashes, url, filename); } } - public static class ModResult implements DownloadManager.IMod { + public static class ModResult implements RemoteModRepository.IMod { @SerializedName("mod_id") private final String modId; @@ -419,19 +457,19 @@ public final class Modrinth { } @Override - public List loadDependencies() throws IOException { + public List loadDependencies() throws IOException { return Collections.emptyList(); } @Override - public Stream loadVersions() throws IOException { - return Modrinth.getFiles(this).stream() + public Stream loadVersions() throws IOException { + return ModrinthRemoteModRepository.INSTANCE.getFiles(this).stream() .map(ModVersion::toVersion) .flatMap(Lang::toStream); } - public DownloadManager.Mod toMod() { - return new DownloadManager.Mod( + public RemoteModRepository.Mod toMod() { + return new RemoteModRepository.Mod( slug, author, title, diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/MurmurHash.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/MurmurHash.java new file mode 100644 index 000000000..9f9d2e397 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/MurmurHash.java @@ -0,0 +1,214 @@ +/* + * 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)); + } +}