refactor(download): RemoteModRepository

This commit is contained in:
huanghongxun
2021-10-04 16:11:02 +08:00
parent 4ba897660b
commit bb6bd86e71
11 changed files with 571 additions and 204 deletions

View File

@@ -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> rootPage = new Lazy<>(RootPage::new);
private static DecoratorController decorator;
private static Lazy<DownloadListPage> modDownloadListPage = new Lazy<>(() -> {
return new DownloadListPage(CurseModManager.SECTION_MODPACK, Versions::downloadModpackImpl) {
{
state.set(State.fromTitle(i18n("modpack.download")));
}
};
});
private static Lazy<DownloadPage> downloadPage = new Lazy<>(DownloadPage::new);
private static Lazy<AccountListPage> 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;

View File

@@ -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();

View File

@@ -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<Profile.ProfileVersion> version = new SimpleObjectProperty<>();
private final ListProperty<DownloadManager.Mod> items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList());
private final ListProperty<RemoteModRepository.Mod> items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList());
private final ObservableList<String> 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<DownloadManager.Mod> 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<String> 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<CategoryIndented> 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<CategoryIndented> 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<ActionEvent> 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<DownloadManager.Mod> listView = new JFXListView<>();
JFXListView<RemoteModRepository.Mod> 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<DownloadManager.Mod>(listView) {
listView.setCellFactory(x -> new FloatListCell<RemoteModRepository.Mod>(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<CategoryIndented> result) {
result.add(new CategoryIndented(indent, category.getId()));
for (CurseModManager.Category subcategory : category.getSubcategories()) {
private static void resolveCategory(RemoteModRepository.Category category, int indent, List<CategoryIndented> result) {
result.add(new CategoryIndented(indent, category));
for (RemoteModRepository.Category subcategory : category.getSubcategories()) {
resolveCategory(subcategory, indent + 1, result);
}
}

View File

@@ -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<DownloadManager.Mod> dependencies;
private SimpleMultimap<String, DownloadManager.Version> versions;
private List<RemoteModRepository.Mod> dependencies;
private SimpleMultimap<String, RemoteModRepository.Version> 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<DownloadManager.Version> versions = addon.getData().loadVersions();
Stream<RemoteModRepository.Version> versions = addon.getData().loadVersions();
// if (StringUtils.isNotBlank(version.getVersion())) {
// Optional<String> 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<DownloadManager.Mod> dependencies = (List<DownloadManager.Mod>) result.get(0);
List<RemoteModRepository.Mod> dependencies = (List<RemoteModRepository.Mod>) result.get(0);
@SuppressWarnings("unchecked")
SimpleMultimap<String, DownloadManager.Version> versions = (SimpleMultimap<String, DownloadManager.Version>) result.get(1);
SimpleMultimap<String, RemoteModRepository.Version> versions = (SimpleMultimap<String, RemoteModRepository.Version>) 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<String, DownloadManager.Version> sortVersions(Stream<DownloadManager.Version> versions) {
SimpleMultimap<String, DownloadManager.Version> classifiedVersions
= new SimpleMultimap<String, DownloadManager.Version>(HashMap::new, ArrayList::new);
private SimpleMultimap<String, RemoteModRepository.Version> sortVersions(Stream<RemoteModRepository.Version> versions) {
SimpleMultimap<String, RemoteModRepository.Version> classifiedVersions
= new SimpleMultimap<String, RemoteModRepository.Version>(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<DownloadManager.Version> versionList = (List<DownloadManager.Version>) classifiedVersions.get(gameVersion);
versionList.sort(Comparator.comparing(DownloadManager.Version::getDatePublished).reversed());
List<RemoteModRepository.Version> versionList = (List<RemoteModRepository.Version>) 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);
}
}

View File

@@ -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<DownloadManager.Mod> searchImpl(String gameVersion, int category, int section, int pageOffset, String searchFilter, int sort) throws Exception {
if (StringUtils.CHINESE_PATTERN.matcher(searchFilter).find()) {
List<ModTranslations.Mod> mods = ModTranslations.searchMod(searchFilter);
List<String> 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<Mod> 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<ModTranslations.Mod> mods = ModTranslations.searchMod(searchFilter);
List<String> 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<DownloadManager.Mod> 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<Version> getRemoteVersionByLocalFile(Path file) {
throw new UnsupportedOperationException();
}
@Override
public Stream<Category> getCategories() throws IOException {
if ("mods.modrinth".equals(downloadSource.get())) {
return ModrinthRemoteModRepository.INSTANCE.getCategories();
} else {
return CurseForgeRemoteModRepository.MODS.getCategories();
}
}
}

View File

@@ -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),

View File

@@ -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<Mod> search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, int sort)
throws IOException;
Optional<Version> getRemoteVersionByLocalFile(Path file) throws IOException;
Stream<Category> getCategories() throws IOException;
interface IMod {
List<Mod> loadDependencies() throws IOException;
Stream<Version> loadVersions() throws IOException;
}
public static class Mod {
class Category {
private final Object self;
private final String id;
private final List<Category> subcategories;
public Category(Object self, String id, List<Category> subcategories) {
this.self = self;
this.id = id;
this.subcategories = subcategories;
}
public Object getSelf() {
return self;
}
public String getId() {
return id;
}
public List<Category> 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<String, String> 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",

View File

@@ -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<Author> 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<LatestFile> latestFiles;
private final List<Category> 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<Author> authors, List<Attachment> attachments, String websiteUrl, int gameId, String summary, int defaultFileId, List<LatestFile> latestFiles, List<Category> categories, int status, int primaryCategoryId, String slug, List<GameVersionLatestFile> gameVersionLatestFiles, boolean isFeatured, double popularityScore, int gamePopularityRank, String primaryLanguage, List<String> modLoaders, boolean isAvailable, boolean isExperimental) {
public CurseAddon(int id, String name, List<Author> authors, List<Attachment> attachments, String websiteUrl, int gameId, String summary, int defaultFileId, LatestFile file, List<LatestFile> latestFiles, List<Category> categories, int status, int primaryCategoryId, String slug, List<GameVersionLatestFile> gameVersionLatestFiles, boolean isFeatured, double popularityScore, int gamePopularityRank, String primaryLanguage, List<String> 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<LatestFile> getLatestFiles() {
return latestFiles;
}
@@ -162,26 +168,26 @@ public class CurseAddon implements DownloadManager.IMod {
}
@Override
public List<DownloadManager.Mod> loadDependencies() throws IOException {
public List<RemoteModRepository.Mod> loadDependencies() throws IOException {
Set<Integer> dependencies = latestFiles.stream()
.flatMap(latestFile -> latestFile.getDependencies().stream())
.filter(dep -> dep.getType() == 3)
.map(Dependency::getAddonId)
.collect(Collectors.toSet());
List<DownloadManager.Mod> mods = new ArrayList<>();
List<RemoteModRepository.Mod> 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<DownloadManager.Version> loadVersions() throws IOException {
return CurseModManager.getFiles(this).stream()
public Stream<RemoteModRepository.Version> 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()

View File

@@ -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<CurseAddon> 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<CurseAddon> 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<RemoteModRepository.Mod> 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<RemoteModRepository.Version> 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<CurseAddon.LatestFile> getFiles(CurseAddon addon) throws IOException {
String response = NetworkUtils.doGet(NetworkUtils.toURL("https://addons-ecs.forgesvc.net/api/v2/addon/" + addon.getId() + "/files"));
public List<CurseAddon.LatestFile> getFiles(CurseAddon addon) throws IOException {
String response = NetworkUtils.doGet(NetworkUtils.toURL(PREFIX + "/addon/" + addon.getId() + "/files"));
return JsonUtils.fromNonNullJson(response, new TypeToken<List<CurseAddon.LatestFile>>() {
}.getType());
}
public static List<Category> getCategories(int section) throws IOException {
String response = NetworkUtils.doGet(NetworkUtils.toURL("https://addons-ecs.forgesvc.net/api/v2/category/section/" + section));
public List<Category> getCategoriesImpl() throws IOException {
String response = NetworkUtils.doGet(NetworkUtils.toURL(PREFIX + "/category/section/" + section));
List<Category> categories = JsonUtils.fromNonNullJson(response, new TypeToken<List<Category>>() {
}.getType());
return reorganizeCategories(categories, section);
}
private static List<Category> reorganizeCategories(List<Category> categories, int rootId) {
@Override
public Stream<RemoteModRepository.Category> getCategories() throws IOException {
return getCategoriesImpl().stream().map(Category::toCategory);
}
private List<Category> reorganizeCategories(List<Category> categories, int rootId) {
List<Category> result = new ArrayList<>();
Map<Integer, Category> 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<Category> 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<CurseAddon> exactMatches;
private final List<Integer> exactFingerprints;
public FingerprintResponse(boolean isCacheBuilt, List<CurseAddon> exactMatches, List<Integer> exactFingerprints) {
this.isCacheBuilt = isCacheBuilt;
this.exactMatches = exactMatches;
this.exactFingerprints = exactFingerprints;
}
public boolean isCacheBuilt() {
return isCacheBuilt;
}
public List<CurseAddon> getExactMatches() {
return exactMatches;
}
public List<Integer> getExactFingerprints() {
return exactFingerprints;
}
}
}

View File

@@ -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<ModResult> searchPaginated(String gameVersion, int pageOffset, String searchFilter) throws IOException {
public List<ModResult> searchPaginated(String gameVersion, int pageOffset, int pageSize, String searchFilter) throws IOException {
Map<String, String> 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<ModResult> response = HttpRequest.GET(NetworkUtils.withQuery("https://api.modrinth.com/api/v1/mod", query))
Response<ModResult> response = HttpRequest.GET(NetworkUtils.withQuery(PREFIX + "/api/v1/mod", query))
.getJson(new TypeToken<Response<ModResult>>() {
}.getType());
return response.getHits();
}
public static List<ModVersion> getFiles(ModResult mod) throws IOException {
String id = StringUtils.removePrefix(mod.getModId(), "local-");
List<ModVersion> versions = HttpRequest.GET("https://api.modrinth.com/api/v1/mod/" + id + "/version")
.getJson(new TypeToken<List<ModVersion>>() {
}.getType());
return versions;
@Override
public Stream<RemoteModRepository.Mod> 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<String> getCategories() throws IOException {
List<String> categories = HttpRequest.GET("https://api.modrinth.com/api/v1/tag/category").getJson(new TypeToken<List<String>>() {
@Override
public Optional<RemoteModRepository.Version> 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<ModVersion> 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<List<ModVersion>>() {
}.getType());
}
public List<String> getCategoriesImpl() throws IOException {
return HttpRequest.GET("https://api.modrinth.com/api/v1/tag/category").getJson(new TypeToken<List<String>>() {
}.getType());
return categories;
}
public Stream<Category> 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<DownloadManager.Version> toVersion() {
DownloadManager.VersionType type;
public Optional<RemoteModRepository.Version> 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<DownloadManager.Mod> loadDependencies() throws IOException {
public List<RemoteModRepository.Mod> loadDependencies() throws IOException {
return Collections.emptyList();
}
@Override
public Stream<DownloadManager.Version> loadVersions() throws IOException {
return Modrinth.getFiles(this).stream()
public Stream<RemoteModRepository.Version> 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,

View File

@@ -0,0 +1,214 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
*/
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));
}
}