feat(mod): mod upgrade detection.
This commit is contained in:
@@ -111,6 +111,8 @@ public class Navigator extends TransitionPane {
|
||||
Logging.LOG.info("Closed page " + from);
|
||||
|
||||
Node poppedNode = stack.pop();
|
||||
NavigationEvent exited = new NavigationEvent(this, poppedNode, Navigation.NavigationDirection.PREVIOUS, NavigationEvent.EXITED);
|
||||
poppedNode.fireEvent(exited);
|
||||
if (poppedNode instanceof PageAware) ((PageAware) poppedNode).onPageHidden();
|
||||
|
||||
backable.set(canGoBack());
|
||||
|
||||
@@ -39,7 +39,7 @@ import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
|
||||
|
||||
public class TabHeader extends Control implements TabControl {
|
||||
public class TabHeader extends Control implements TabControl, PageAware {
|
||||
|
||||
public TabHeader(Tab<?>... tabs) {
|
||||
getStyleClass().setAll("tab-header");
|
||||
@@ -86,6 +86,26 @@ public class TabHeader extends Control implements TabControl {
|
||||
getSelectionModel().select(tab);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageShown() {
|
||||
Tab<?> tab = getSelectionModel().getSelectedItem();
|
||||
if (tab != null) {
|
||||
if (tab.getNode() instanceof PageAware) {
|
||||
((PageAware) tab.getNode()).onPageShown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageHidden() {
|
||||
Tab<?> tab = getSelectionModel().getSelectedItem();
|
||||
if (tab != null) {
|
||||
if (tab.getNode() instanceof PageAware) {
|
||||
((PageAware) tab.getNode()).onPageHidden();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The position to place the tabs.
|
||||
*/
|
||||
|
||||
@@ -144,7 +144,7 @@ public final class TaskListPane extends StackPane {
|
||||
Platform.runLater(() -> {
|
||||
ProgressListNode node = new ProgressListNode(task);
|
||||
nodes.put(task, node);
|
||||
StageNode stageNode = stageNodes.stream().filter(x -> x.stage.equals(task.getStage())).findAny().orElse(null);
|
||||
StageNode stageNode = stageNodes.stream().filter(x -> x.stage.equals(task.getInheritedStage())).findAny().orElse(null);
|
||||
listBox.add(listBox.indexOf(stageNode) + 1, node);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
|
||||
import org.jackhuang.hmcl.ui.construct.PageAware;
|
||||
import org.jackhuang.hmcl.ui.construct.TabHeader;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
@@ -34,7 +35,7 @@ import org.jackhuang.hmcl.ui.versions.VersionSettingsPage;
|
||||
import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class LauncherSettingsPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
public class LauncherSettingsPage extends DecoratorAnimatedPage implements DecoratorPage, PageAware {
|
||||
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("settings"), -1));
|
||||
private final TabHeader tab;
|
||||
private final TabHeader.Tab<VersionSettingsPage> gameTab = new TabHeader.Tab<>("versionSettingsPage");
|
||||
@@ -124,6 +125,16 @@ public class LauncherSettingsPage extends DecoratorAnimatedPage implements Decor
|
||||
setCenter(transitionPane);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageShown() {
|
||||
tab.onPageShown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageHidden() {
|
||||
tab.onPageHidden();
|
||||
}
|
||||
|
||||
public void showGameSettings(Profile profile) {
|
||||
gameTab.getNode().loadVersion(profile, null);
|
||||
tab.select(gameTab);
|
||||
|
||||
@@ -187,7 +187,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
if (exception != null) {
|
||||
Controllers.dialog("Failed to check updates", "failed", MessageDialogPane.MessageType.ERROR);
|
||||
} else {
|
||||
Controllers.dialog(new ModUpdatesDialog(result));
|
||||
Controllers.dialog(new ModUpdatesPane(result));
|
||||
}
|
||||
})
|
||||
.withStagesHint(Collections.singletonList("mods.check_updates"))
|
||||
|
||||
@@ -38,7 +38,7 @@ public class ModUpdateTask extends Task<List<LocalMod.ModUpdate>> {
|
||||
dependents = mods.stream()
|
||||
.map(mod -> Task.supplyAsync(() -> {
|
||||
return mod.checkUpdates(gameVersion, CurseForgeRemoteModRepository.MODS);
|
||||
}).setSignificance(TaskSignificance.MAJOR).withCounter("mods.check_updates"))
|
||||
}).setSignificance(TaskSignificance.MAJOR).setName(mod.getFileName()).withCounter("mods.check_updates"))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
setStage("mods.check_updates");
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
/*
|
||||
* 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.ui.versions;
|
||||
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import org.jackhuang.hmcl.mod.LocalMod;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseAddon;
|
||||
import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.MDListCell;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class ModUpdatesDialog extends DialogPane {
|
||||
|
||||
public ModUpdatesDialog(List<LocalMod.ModUpdate> updates) {
|
||||
setTitle(i18n("mods.check_updates"));
|
||||
|
||||
JFXListView<LocalMod.ModUpdate> listView = new JFXListView<>();
|
||||
listView.getItems().setAll(updates);
|
||||
listView.setCellFactory(l -> new ModUpdateCell(listView));
|
||||
setBody(listView);
|
||||
}
|
||||
|
||||
public static class ModUpdateCell extends MDListCell<LocalMod.ModUpdate> {
|
||||
TwoLineListItem content = new TwoLineListItem();
|
||||
|
||||
public ModUpdateCell(JFXListView<LocalMod.ModUpdate> listView) {
|
||||
super(listView);
|
||||
|
||||
getContainer().getChildren().setAll(content);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateControl(LocalMod.ModUpdate item, boolean empty) {
|
||||
if (empty) return;
|
||||
ModTranslations.Mod mod = ModTranslations.getModById(item.getLocalMod().getId());
|
||||
content.setTitle(mod != null ? mod.getDisplayName() : item.getCurrentVersion().getName());
|
||||
content.setSubtitle(item.getLocalMod().getFileName());
|
||||
content.getTags().setAll();
|
||||
|
||||
if (item.getCurrentVersion().getSelf() instanceof CurseAddon.LatestFile) {
|
||||
content.getTags().add("Curseforge");
|
||||
} else if (item.getCurrentVersion().getSelf() instanceof ModrinthRemoteModRepository.ModVersion) {
|
||||
content.getTags().add("Modrinth");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* 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.ui.versions;
|
||||
|
||||
import com.jfoenix.controls.*;
|
||||
import com.jfoenix.controls.datamodels.treetable.RecursiveTreeObject;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import org.jackhuang.hmcl.mod.LocalMod;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseAddon;
|
||||
import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository;
|
||||
import org.jackhuang.hmcl.ui.construct.MDListCell;
|
||||
import org.jackhuang.hmcl.ui.construct.PageCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class ModUpdatesPane extends BorderPane implements DecoratorPage {
|
||||
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(DecoratorPage.State.fromTitle(i18n("download"), -1));
|
||||
|
||||
public ModUpdatesPane(List<LocalMod.ModUpdate> updates) {
|
||||
|
||||
JFXTreeTableColumn<ModUpdateObject, String> fileNameColumn = new JFXTreeTableColumn<>(i18n("mods.check_updates.file"));
|
||||
fileNameColumn.setCellValueFactory(data -> {
|
||||
if (fileNameColumn.validateValue(data)) {
|
||||
return data.getValue().getValue().fileName;
|
||||
} else {
|
||||
return fileNameColumn.getComputedValue(data);
|
||||
}
|
||||
});
|
||||
|
||||
JFXTreeTableColumn<ModUpdateObject, String> currentVersionColumn = new JFXTreeTableColumn<>(i18n("mods.check_updates.current_version"));
|
||||
currentVersionColumn.setCellValueFactory(data -> {
|
||||
if (currentVersionColumn.validateValue(data)) {
|
||||
return data.getValue().getValue().currentVersion;
|
||||
} else {
|
||||
return currentVersionColumn.getComputedValue(data);
|
||||
}
|
||||
});
|
||||
|
||||
JFXTreeTableColumn<ModUpdateObject, String> targetVersionColumn = new JFXTreeTableColumn<>(i18n("mods.check_updates.target_version"));
|
||||
targetVersionColumn.setCellValueFactory(data -> {
|
||||
if (targetVersionColumn.validateValue(data)) {
|
||||
return data.getValue().getValue().targetVersion;
|
||||
} else {
|
||||
return targetVersionColumn.getComputedValue(data);
|
||||
}
|
||||
});
|
||||
|
||||
ObservableList<ModUpdateObject> objects = FXCollections.observableList(updates.stream().map(ModUpdateObject::new).collect(Collectors.toList()));
|
||||
|
||||
RecursiveTreeItem<ModUpdateObject> root = new RecursiveTreeItem<>(
|
||||
objects,
|
||||
RecursiveTreeObject::getChildren);
|
||||
|
||||
JFXTreeTableView<ModUpdateObject> table = new JFXTreeTableView<>(root);
|
||||
table.setShowRoot(false);
|
||||
table.setEditable(true);
|
||||
table.getColumns().setAll(fileNameColumn, currentVersionColumn, targetVersionColumn);
|
||||
|
||||
setCenter(table);
|
||||
|
||||
HBox actions = new HBox(8);
|
||||
actions.setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
JFXButton nextButton = new JFXButton();
|
||||
nextButton.getStyleClass().add("jfx-button-raised");
|
||||
nextButton.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
nextButton.setOnAction(e -> updateMods());
|
||||
|
||||
JFXButton cancelButton = new JFXButton();
|
||||
cancelButton.getStyleClass().add("jfx-button-raised");
|
||||
cancelButton.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
cancelButton.setOnAction(e -> fireEvent(new PageCloseEvent()));
|
||||
onEscPressed(this, cancelButton::fire);
|
||||
|
||||
actions.getChildren().setAll(nextButton, cancelButton);
|
||||
setBottom(actions);
|
||||
}
|
||||
|
||||
private void updateMods() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadOnlyObjectWrapper<State> stateProperty() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public static class ModUpdateCell extends MDListCell<LocalMod.ModUpdate> {
|
||||
TwoLineListItem content = new TwoLineListItem();
|
||||
|
||||
public ModUpdateCell(JFXListView<LocalMod.ModUpdate> listView) {
|
||||
super(listView);
|
||||
|
||||
getContainer().getChildren().setAll(content);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateControl(LocalMod.ModUpdate item, boolean empty) {
|
||||
if (empty) return;
|
||||
ModTranslations.Mod mod = ModTranslations.getModById(item.getLocalMod().getId());
|
||||
content.setTitle(mod != null ? mod.getDisplayName() : item.getCurrentVersion().getName());
|
||||
content.setSubtitle(item.getLocalMod().getFileName());
|
||||
content.getTags().setAll();
|
||||
|
||||
if (item.getCurrentVersion().getSelf() instanceof CurseAddon.LatestFile) {
|
||||
content.getTags().add("Curseforge");
|
||||
} else if (item.getCurrentVersion().getSelf() instanceof ModrinthRemoteModRepository.ModVersion) {
|
||||
content.getTags().add("Modrinth");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class ModUpdateObject extends RecursiveTreeObject<ModUpdateObject> {
|
||||
final LocalMod.ModUpdate data;
|
||||
final StringProperty fileName = new SimpleStringProperty();
|
||||
final StringProperty currentVersion = new SimpleStringProperty();
|
||||
final StringProperty targetVersion = new SimpleStringProperty();
|
||||
|
||||
public ModUpdateObject(LocalMod.ModUpdate data) {
|
||||
this.data = data;
|
||||
|
||||
fileName.set(data.getLocalMod().getFileName());
|
||||
currentVersion.set(data.getCurrentVersion().getName());
|
||||
targetVersion.set(data.getCandidates().get(0).getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -581,6 +581,9 @@ mods.add.failed=添加模组 %s 失败。
|
||||
mods.add.success=成功添加模组 %s。
|
||||
mods.category=类别
|
||||
mods.check_updates=检查模组更新
|
||||
mods.check_updates.current_version=当前版本
|
||||
mods.check_updates.file=文件
|
||||
mods.check_updates.target_version=目标版本
|
||||
mods.choose_mod=选择模组
|
||||
mods.curseforge=CurseForge
|
||||
mods.dependencies=前置 Mod
|
||||
|
||||
@@ -148,6 +148,7 @@ public final class LocalMod implements Comparable<LocalMod> {
|
||||
.filter(version -> version.getLoaders().contains(modLoaderType))
|
||||
.sorted(Comparator.comparing(RemoteMod.Version::getDatePublished).reversed())
|
||||
.collect(Collectors.toList());
|
||||
if (remoteVersions.isEmpty()) return null;
|
||||
return new ModUpdate(this, currentVersion.get(), remoteVersions);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.mod.curse;
|
||||
|
||||
import org.jackhuang.hmcl.mod.ModLoaderType;
|
||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
|
||||
@@ -508,7 +509,7 @@ public class CurseAddon implements RemoteMod.IMod {
|
||||
new RemoteMod.File(Collections.emptyMap(), getDownloadUrl(), getFileName()),
|
||||
Collections.emptyList(),
|
||||
gameVersion.stream().filter(ver -> ver.startsWith("1.") || ver.contains("w")).collect(Collectors.toList()),
|
||||
Collections.emptyList()
|
||||
Collections.singletonList(ModLoaderType.FORGE)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,15 +21,12 @@ import com.google.gson.reflect.TypeToken;
|
||||
import org.jackhuang.hmcl.mod.LocalMod;
|
||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||
import org.jackhuang.hmcl.mod.RemoteModRepository;
|
||||
import org.jackhuang.hmcl.util.MurmurHash;
|
||||
import org.jackhuang.hmcl.util.MurmurHash2;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.HttpRequest;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.*;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@@ -42,7 +39,7 @@ import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
public final class CurseForgeRemoteModRepository implements RemoteModRepository {
|
||||
|
||||
private static final String PREFIX = "https://addons-ecs.forgesvc.net/api/v2";
|
||||
private static final String PREFIX = "https://addons-ecs.forgesvc.net";
|
||||
|
||||
private final int section;
|
||||
|
||||
@@ -51,7 +48,7 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
|
||||
}
|
||||
|
||||
public List<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(
|
||||
String response = NetworkUtils.doGet(new URL(NetworkUtils.withQuery(PREFIX + "/api/v2/addon/search", mapOf(
|
||||
pair("categoryId", Integer.toString(category)),
|
||||
pair("gameId", "432"),
|
||||
pair("gameVersion", gameVersion),
|
||||
@@ -76,18 +73,22 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
|
||||
@Override
|
||||
public Optional<RemoteMod.Version> getRemoteVersionByLocalFile(LocalMod localMod, Path file) throws IOException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(Files.newInputStream(file)))) {
|
||||
int b;
|
||||
while ((b = reader.read()) != -1) {
|
||||
if (b != 0x9 && b != 0xa && b != 0xd && b != 0x20) {
|
||||
baos.write(b);
|
||||
try (InputStream stream = Files.newInputStream(file)) {
|
||||
byte[] buf = new byte[1024];
|
||||
int len;
|
||||
while ((len = stream.read(buf, 0, buf.length)) != -1) {
|
||||
for (int i = 0; i < len; i++) {
|
||||
byte b = buf[i];
|
||||
if (b != 0x9 && b != 0xa && b != 0xd && b != 0x20) {
|
||||
baos.write(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int hash = MurmurHash.hash32(baos.toByteArray(), baos.size(), 1);
|
||||
long hash = Integer.toUnsignedLong(MurmurHash2.hash32(baos.toByteArray(), baos.size(), 1));
|
||||
|
||||
FingerprintResponse response = HttpRequest.POST(PREFIX + "/fingerprint")
|
||||
FingerprintResponse response = HttpRequest.POST(PREFIX + "/api/v2/fingerprint")
|
||||
.json(Collections.singletonList(hash))
|
||||
.getJson(FingerprintResponse.class);
|
||||
|
||||
@@ -100,20 +101,20 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
|
||||
|
||||
@Override
|
||||
public RemoteMod getModById(String id) throws IOException {
|
||||
String response = NetworkUtils.doGet(NetworkUtils.toURL(PREFIX + "/addon/" + id));
|
||||
String response = NetworkUtils.doGet(NetworkUtils.toURL(PREFIX + "/api/v2/addon/" + id));
|
||||
return JsonUtils.fromNonNullJson(response, CurseAddon.class).toMod();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<RemoteMod.Version> getRemoteVersionsById(String id) throws IOException {
|
||||
String response = NetworkUtils.doGet(NetworkUtils.toURL(PREFIX + "/addon/" + id + "/files"));
|
||||
String response = NetworkUtils.doGet(NetworkUtils.toURL(PREFIX + "/api/v2/addon/" + id + "/files"));
|
||||
List<CurseAddon.LatestFile> files = JsonUtils.fromNonNullJson(response, new TypeToken<List<CurseAddon.LatestFile>>() {
|
||||
}.getType());
|
||||
return files.stream().map(CurseAddon.LatestFile::toVersion);
|
||||
}
|
||||
|
||||
public List<Category> getCategoriesImpl() throws IOException {
|
||||
String response = NetworkUtils.doGet(NetworkUtils.toURL(PREFIX + "/category/section/" + section));
|
||||
String response = NetworkUtils.doGet(NetworkUtils.toURL(PREFIX + "/api/v2/category/section/" + section));
|
||||
List<Category> categories = JsonUtils.fromNonNullJson(response, new TypeToken<List<Category>>() {
|
||||
}.getType());
|
||||
return reorganizeCategories(categories, section);
|
||||
@@ -231,9 +232,9 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
|
||||
private static class FingerprintResponse {
|
||||
private final boolean isCacheBuilt;
|
||||
private final List<CurseAddon> exactMatches;
|
||||
private final List<Integer> exactFingerprints;
|
||||
private final List<Long> exactFingerprints;
|
||||
|
||||
public FingerprintResponse(boolean isCacheBuilt, List<CurseAddon> exactMatches, List<Integer> exactFingerprints) {
|
||||
public FingerprintResponse(boolean isCacheBuilt, List<CurseAddon> exactMatches, List<Long> exactFingerprints) {
|
||||
this.isCacheBuilt = isCacheBuilt;
|
||||
this.exactMatches = exactMatches;
|
||||
this.exactFingerprints = exactFingerprints;
|
||||
@@ -247,7 +248,7 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
|
||||
return exactMatches;
|
||||
}
|
||||
|
||||
public List<Integer> getExactFingerprints() {
|
||||
public List<Long> getExactFingerprints() {
|
||||
return exactFingerprints;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,6 +208,11 @@ public final class AsyncTaskExecutor extends TaskExecutor {
|
||||
|
||||
task.setCancelled(this::isCancelled);
|
||||
task.setState(Task.TaskState.READY);
|
||||
if (task.getStage() != null) {
|
||||
task.setInheritedStage(task.getStage());
|
||||
} else if (parentTask != null) {
|
||||
task.setInheritedStage(parentTask.getInheritedStage());
|
||||
}
|
||||
task.setNotifyPropertiesChanged(() -> taskListeners.forEach(it -> it.onPropertiesUpdate(task)));
|
||||
|
||||
if (task.getSignificance().shouldLog())
|
||||
|
||||
@@ -99,6 +99,16 @@ public abstract class Task<T> {
|
||||
this.stage = stage;
|
||||
}
|
||||
|
||||
private String inheritedStage = null;
|
||||
|
||||
public String getInheritedStage() {
|
||||
return inheritedStage;
|
||||
}
|
||||
|
||||
void setInheritedStage(String inheritedStage) {
|
||||
this.inheritedStage = inheritedStage;
|
||||
}
|
||||
|
||||
// properties
|
||||
Map<String, Object> properties;
|
||||
|
||||
|
||||
@@ -1,214 +0,0 @@
|
||||
/*
|
||||
* 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));
|
||||
}
|
||||
}
|
||||
319
HMCLCore/src/main/java/org/jackhuang/hmcl/util/MurmurHash2.java
Normal file
319
HMCLCore/src/main/java/org/jackhuang/hmcl/util/MurmurHash2.java
Normal file
@@ -0,0 +1,319 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* Implementation of the MurmurHash2 32-bit and 64-bit hash functions.
|
||||
*
|
||||
* <p>MurmurHash is a non-cryptographic hash function suitable for general
|
||||
* hash-based lookup. The name comes from two basic operations, multiply (MU)
|
||||
* and rotate (R), used in its inner loop. Unlike cryptographic hash functions,
|
||||
* it is not specifically designed to be difficult to reverse by an adversary,
|
||||
* making it unsuitable for cryptographic purposes.</p>
|
||||
*
|
||||
* <p>This contains a Java port of the 32-bit hash function {@code MurmurHash2}
|
||||
* and the 64-bit hash function {@code MurmurHash64A} from Austin Applyby's
|
||||
* original {@code c++} code in SMHasher.</p>
|
||||
*
|
||||
* <p>This is a re-implementation of the original C code plus some additional
|
||||
* features.</p>
|
||||
*
|
||||
* <p>This is public domain code with no copyrights. From home page of
|
||||
* <a href="https://github.com/aappleby/smhasher">SMHasher</a>:</p>
|
||||
*
|
||||
* <blockquote>
|
||||
* "All MurmurHash versions are public domain software, and the author
|
||||
* disclaims all copyright to their code."
|
||||
* </blockquote>
|
||||
*
|
||||
* @see <a href="https://en.wikipedia.org/wiki/MurmurHash">MurmurHash</a>
|
||||
* @see <a href="https://github.com/aappleby/smhasher/blob/master/src/MurmurHash2.cpp">
|
||||
* Original MurmurHash2 c++ code</a>
|
||||
* @since 1.13
|
||||
*/
|
||||
public class MurmurHash2 {
|
||||
|
||||
// Constants for 32-bit variant
|
||||
private static final int M32 = 0x5bd1e995;
|
||||
private static final int R32 = 24;
|
||||
|
||||
// Constants for 64-bit variant
|
||||
private static final long M64 = 0xc6a4a7935bd1e995L;
|
||||
private static final int R64 = 47;
|
||||
|
||||
/** No instance methods. */
|
||||
private MurmurHash2() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a 32-bit hash from byte array with the given length and seed.
|
||||
*
|
||||
* @param data The input byte array
|
||||
* @param length The length of the array
|
||||
* @param seed The initial seed value
|
||||
* @return The 32-bit hash
|
||||
*/
|
||||
public static int hash32(final byte[] data, final int length, final int seed) {
|
||||
// Initialize the hash to a random value
|
||||
int h = seed ^ length;
|
||||
|
||||
// Mix 4 bytes at a time into the hash
|
||||
final int nblocks = length >> 2;
|
||||
|
||||
// body
|
||||
for (int i = 0; i < nblocks; i++) {
|
||||
final int index = (i << 2);
|
||||
int k = getLittleEndianInt(data, index);
|
||||
k *= M32;
|
||||
k ^= k >>> R32;
|
||||
k *= M32;
|
||||
h *= M32;
|
||||
h ^= k;
|
||||
}
|
||||
|
||||
// Handle the last few bytes of the input array
|
||||
final int index = (nblocks << 2);
|
||||
switch (length - index) {
|
||||
case 3:
|
||||
h ^= (data[index + 2] & 0xff) << 16;
|
||||
case 2:
|
||||
h ^= (data[index + 1] & 0xff) << 8;
|
||||
case 1:
|
||||
h ^= (data[index] & 0xff);
|
||||
h *= M32;
|
||||
}
|
||||
|
||||
// Do a few final mixes of the hash to ensure the last few
|
||||
// bytes are well-incorporated.
|
||||
h ^= h >>> 13;
|
||||
h *= M32;
|
||||
h ^= h >>> 15;
|
||||
|
||||
return h;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a 32-bit hash from byte array with the given length and a default seed value.
|
||||
* This is a helper method that will produce the same result as:
|
||||
*
|
||||
* <pre>
|
||||
* int seed = 0x9747b28c;
|
||||
* int hash = MurmurHash2.hash32(data, length, seed);
|
||||
* </pre>
|
||||
*
|
||||
* @param data The input byte array
|
||||
* @param length The length of the array
|
||||
* @return The 32-bit hash
|
||||
* @see #hash32(byte[], int, int)
|
||||
*/
|
||||
public static int hash32(final byte[] data, final int length) {
|
||||
return hash32(data, length, 0x9747b28c);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a 32-bit hash from a string with a default seed.
|
||||
* <p>
|
||||
* Before 1.14 the string was converted using default encoding.
|
||||
* Since 1.14 the string is converted to bytes using UTF-8 encoding.
|
||||
* </p>
|
||||
* This is a helper method that will produce the same result as:
|
||||
*
|
||||
* <pre>
|
||||
* int seed = 0x9747b28c;
|
||||
* byte[] bytes = data.getBytes(StandardCharsets.UTF_8);
|
||||
* int hash = MurmurHash2.hash32(bytes, bytes.length, seed);
|
||||
* </pre>
|
||||
*
|
||||
* @param text The input string
|
||||
* @return The 32-bit hash
|
||||
* @see #hash32(byte[], int, int)
|
||||
*/
|
||||
public static int hash32(final String text) {
|
||||
final byte[] bytes = text.getBytes(StandardCharsets.UTF_8);
|
||||
return hash32(bytes, bytes.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a 32-bit hash from a substring with a default seed value.
|
||||
* The string is converted to bytes using the default encoding.
|
||||
* This is a helper method that will produce the same result as:
|
||||
*
|
||||
* <pre>
|
||||
* int seed = 0x9747b28c;
|
||||
* byte[] bytes = text.substring(from, from + length).getBytes(StandardCharsets.UTF_8);
|
||||
* int hash = MurmurHash2.hash32(bytes, bytes.length, seed);
|
||||
* </pre>
|
||||
*
|
||||
* @param text The input string
|
||||
* @param from The starting index
|
||||
* @param length The length of the substring
|
||||
* @return The 32-bit hash
|
||||
* @see #hash32(byte[], int, int)
|
||||
*/
|
||||
public static int hash32(final String text, final int from, final int length) {
|
||||
return hash32(text.substring(from, from + length));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a 64-bit hash from byte array of the given length and seed.
|
||||
*
|
||||
* @param data The input byte array
|
||||
* @param length The length of the array
|
||||
* @param seed The initial seed value
|
||||
* @return The 64-bit hash of the given array
|
||||
*/
|
||||
public static long hash64(final byte[] data, final int length, final int seed) {
|
||||
long h = (seed & 0xffffffffL) ^ (length * M64);
|
||||
|
||||
final int nblocks = length >> 3;
|
||||
|
||||
// body
|
||||
for (int i = 0; i < nblocks; i++) {
|
||||
final int index = (i << 3);
|
||||
long k = getLittleEndianLong(data, index);
|
||||
|
||||
k *= M64;
|
||||
k ^= k >>> R64;
|
||||
k *= M64;
|
||||
|
||||
h ^= k;
|
||||
h *= M64;
|
||||
}
|
||||
|
||||
final int index = (nblocks << 3);
|
||||
switch (length - index) {
|
||||
case 7:
|
||||
h ^= ((long) data[index + 6] & 0xff) << 48;
|
||||
case 6:
|
||||
h ^= ((long) data[index + 5] & 0xff) << 40;
|
||||
case 5:
|
||||
h ^= ((long) data[index + 4] & 0xff) << 32;
|
||||
case 4:
|
||||
h ^= ((long) data[index + 3] & 0xff) << 24;
|
||||
case 3:
|
||||
h ^= ((long) data[index + 2] & 0xff) << 16;
|
||||
case 2:
|
||||
h ^= ((long) data[index + 1] & 0xff) << 8;
|
||||
case 1:
|
||||
h ^= ((long) data[index] & 0xff);
|
||||
h *= M64;
|
||||
}
|
||||
|
||||
h ^= h >>> R64;
|
||||
h *= M64;
|
||||
h ^= h >>> R64;
|
||||
|
||||
return h;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a 64-bit hash from byte array with given length and a default seed value.
|
||||
* This is a helper method that will produce the same result as:
|
||||
*
|
||||
* <pre>
|
||||
* int seed = 0xe17a1465;
|
||||
* int hash = MurmurHash2.hash64(data, length, seed);
|
||||
* </pre>
|
||||
*
|
||||
* @param data The input byte array
|
||||
* @param length The length of the array
|
||||
* @return The 64-bit hash
|
||||
* @see #hash64(byte[], int, int)
|
||||
*/
|
||||
public static long hash64(final byte[] data, final int length) {
|
||||
return hash64(data, length, 0xe17a1465);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a 64-bit hash from a string with a default seed.
|
||||
* <p>
|
||||
* Before 1.14 the string was converted using default encoding.
|
||||
* Since 1.14 the string is converted to bytes using UTF-8 encoding.
|
||||
* </p>
|
||||
* This is a helper method that will produce the same result as:
|
||||
*
|
||||
* <pre>
|
||||
* int seed = 0xe17a1465;
|
||||
* byte[] bytes = data.getBytes(StandardCharsets.UTF_8);
|
||||
* int hash = MurmurHash2.hash64(bytes, bytes.length, seed);
|
||||
* </pre>
|
||||
*
|
||||
* @param text The input string
|
||||
* @return The 64-bit hash
|
||||
* @see #hash64(byte[], int, int)
|
||||
*/
|
||||
public static long hash64(final String text) {
|
||||
final byte[] bytes = text.getBytes(StandardCharsets.UTF_8);
|
||||
return hash64(bytes, bytes.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a 64-bit hash from a substring with a default seed value.
|
||||
* The string is converted to bytes using the default encoding.
|
||||
* This is a helper method that will produce the same result as:
|
||||
*
|
||||
* <pre>
|
||||
* int seed = 0xe17a1465;
|
||||
* byte[] bytes = text.substring(from, from + length).getBytes(StandardCharsets.UTF_8);
|
||||
* int hash = MurmurHash2.hash64(bytes, bytes.length, seed);
|
||||
* </pre>
|
||||
*
|
||||
* @param text The The input string
|
||||
* @param from The starting index
|
||||
* @param length The length of the substring
|
||||
* @return The 64-bit hash
|
||||
* @see #hash64(byte[], int, int)
|
||||
*/
|
||||
public static long hash64(final String text, final int from, final int length) {
|
||||
return hash64(text.substring(from, from + length));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the little-endian int from 4 bytes starting at the specified index.
|
||||
*
|
||||
* @param data The data
|
||||
* @param index The index
|
||||
* @return The little-endian int
|
||||
*/
|
||||
private static int getLittleEndianInt(final byte[] data, final int index) {
|
||||
return ((data[index ] & 0xff) ) |
|
||||
((data[index + 1] & 0xff) << 8) |
|
||||
((data[index + 2] & 0xff) << 16) |
|
||||
((data[index + 3] & 0xff) << 24);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the little-endian long from 8 bytes starting at the specified index.
|
||||
*
|
||||
* @param data The data
|
||||
* @param index The index
|
||||
* @return The little-endian long
|
||||
*/
|
||||
private static long getLittleEndianLong(final byte[] data, final int index) {
|
||||
return (((long) data[index ] & 0xff) ) |
|
||||
(((long) data[index + 1] & 0xff) << 8) |
|
||||
(((long) data[index + 2] & 0xff) << 16) |
|
||||
(((long) data[index + 3] & 0xff) << 24) |
|
||||
(((long) data[index + 4] & 0xff) << 32) |
|
||||
(((long) data[index + 5] & 0xff) << 40) |
|
||||
(((long) data[index + 6] & 0xff) << 48) |
|
||||
(((long) data[index + 7] & 0xff) << 56);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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.mod.curse;
|
||||
|
||||
import org.jackhuang.hmcl.util.MurmurHash2;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
public class CurseForgeRemoteModRepositoryTest {
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void testMurmurHash() throws Exception {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
try (InputStream is = Files.newInputStream(Paths.get("C:\\Users\\huang\\Downloads\\JustEnoughCalculation-1.16.5-3.8.5.jar"))) {
|
||||
byte[] buf = new byte[1024];
|
||||
int len;
|
||||
while ((len = is.read(buf, 0, buf.length)) > 0) {
|
||||
for (int i = 0; i < len; i++) {
|
||||
byte b = buf[i];
|
||||
if (b != 9 && b != 10 && b != 13 && b != 32) {
|
||||
baos.write(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
long hash = Integer.toUnsignedLong(MurmurHash2.hash32(baos.toByteArray(), baos.size(), 1));
|
||||
|
||||
Assert.assertEquals(hash, 3333498611L);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user