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);
|
Logging.LOG.info("Closed page " + from);
|
||||||
|
|
||||||
Node poppedNode = stack.pop();
|
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();
|
if (poppedNode instanceof PageAware) ((PageAware) poppedNode).onPageHidden();
|
||||||
|
|
||||||
backable.set(canGoBack());
|
backable.set(canGoBack());
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ import javafx.util.Duration;
|
|||||||
import org.jackhuang.hmcl.ui.FXUtils;
|
import org.jackhuang.hmcl.ui.FXUtils;
|
||||||
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
|
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) {
|
public TabHeader(Tab<?>... tabs) {
|
||||||
getStyleClass().setAll("tab-header");
|
getStyleClass().setAll("tab-header");
|
||||||
@@ -86,6 +86,26 @@ public class TabHeader extends Control implements TabControl {
|
|||||||
getSelectionModel().select(tab);
|
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.
|
* The position to place the tabs.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ public final class TaskListPane extends StackPane {
|
|||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
ProgressListNode node = new ProgressListNode(task);
|
ProgressListNode node = new ProgressListNode(task);
|
||||||
nodes.put(task, node);
|
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);
|
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.ContainerAnimations;
|
||||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||||
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
|
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.construct.TabHeader;
|
||||||
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
|
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
|
||||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
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.ui.versions.VersionPage.wrap;
|
||||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
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 ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("settings"), -1));
|
||||||
private final TabHeader tab;
|
private final TabHeader tab;
|
||||||
private final TabHeader.Tab<VersionSettingsPage> gameTab = new TabHeader.Tab<>("versionSettingsPage");
|
private final TabHeader.Tab<VersionSettingsPage> gameTab = new TabHeader.Tab<>("versionSettingsPage");
|
||||||
@@ -124,6 +125,16 @@ public class LauncherSettingsPage extends DecoratorAnimatedPage implements Decor
|
|||||||
setCenter(transitionPane);
|
setCenter(transitionPane);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPageShown() {
|
||||||
|
tab.onPageShown();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPageHidden() {
|
||||||
|
tab.onPageHidden();
|
||||||
|
}
|
||||||
|
|
||||||
public void showGameSettings(Profile profile) {
|
public void showGameSettings(Profile profile) {
|
||||||
gameTab.getNode().loadVersion(profile, null);
|
gameTab.getNode().loadVersion(profile, null);
|
||||||
tab.select(gameTab);
|
tab.select(gameTab);
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
|||||||
if (exception != null) {
|
if (exception != null) {
|
||||||
Controllers.dialog("Failed to check updates", "failed", MessageDialogPane.MessageType.ERROR);
|
Controllers.dialog("Failed to check updates", "failed", MessageDialogPane.MessageType.ERROR);
|
||||||
} else {
|
} else {
|
||||||
Controllers.dialog(new ModUpdatesDialog(result));
|
Controllers.dialog(new ModUpdatesPane(result));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.withStagesHint(Collections.singletonList("mods.check_updates"))
|
.withStagesHint(Collections.singletonList("mods.check_updates"))
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ public class ModUpdateTask extends Task<List<LocalMod.ModUpdate>> {
|
|||||||
dependents = mods.stream()
|
dependents = mods.stream()
|
||||||
.map(mod -> Task.supplyAsync(() -> {
|
.map(mod -> Task.supplyAsync(() -> {
|
||||||
return mod.checkUpdates(gameVersion, CurseForgeRemoteModRepository.MODS);
|
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());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
setStage("mods.check_updates");
|
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.add.success=成功添加模组 %s。
|
||||||
mods.category=类别
|
mods.category=类别
|
||||||
mods.check_updates=检查模组更新
|
mods.check_updates=检查模组更新
|
||||||
|
mods.check_updates.current_version=当前版本
|
||||||
|
mods.check_updates.file=文件
|
||||||
|
mods.check_updates.target_version=目标版本
|
||||||
mods.choose_mod=选择模组
|
mods.choose_mod=选择模组
|
||||||
mods.curseforge=CurseForge
|
mods.curseforge=CurseForge
|
||||||
mods.dependencies=前置 Mod
|
mods.dependencies=前置 Mod
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ public final class LocalMod implements Comparable<LocalMod> {
|
|||||||
.filter(version -> version.getLoaders().contains(modLoaderType))
|
.filter(version -> version.getLoaders().contains(modLoaderType))
|
||||||
.sorted(Comparator.comparing(RemoteMod.Version::getDatePublished).reversed())
|
.sorted(Comparator.comparing(RemoteMod.Version::getDatePublished).reversed())
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
if (remoteVersions.isEmpty()) return null;
|
||||||
return new ModUpdate(this, currentVersion.get(), remoteVersions);
|
return new ModUpdate(this, currentVersion.get(), remoteVersions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.jackhuang.hmcl.mod.curse;
|
package org.jackhuang.hmcl.mod.curse;
|
||||||
|
|
||||||
|
import org.jackhuang.hmcl.mod.ModLoaderType;
|
||||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||||
import org.jackhuang.hmcl.util.Immutable;
|
import org.jackhuang.hmcl.util.Immutable;
|
||||||
|
|
||||||
@@ -508,7 +509,7 @@ public class CurseAddon implements RemoteMod.IMod {
|
|||||||
new RemoteMod.File(Collections.emptyMap(), getDownloadUrl(), getFileName()),
|
new RemoteMod.File(Collections.emptyMap(), getDownloadUrl(), getFileName()),
|
||||||
Collections.emptyList(),
|
Collections.emptyList(),
|
||||||
gameVersion.stream().filter(ver -> ver.startsWith("1.") || ver.contains("w")).collect(Collectors.toList()),
|
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.LocalMod;
|
||||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||||
import org.jackhuang.hmcl.mod.RemoteModRepository;
|
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.gson.JsonUtils;
|
||||||
import org.jackhuang.hmcl.util.io.HttpRequest;
|
import org.jackhuang.hmcl.util.io.HttpRequest;
|
||||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.*;
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
@@ -42,7 +39,7 @@ import static org.jackhuang.hmcl.util.Pair.pair;
|
|||||||
|
|
||||||
public final class CurseForgeRemoteModRepository implements RemoteModRepository {
|
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;
|
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 {
|
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("categoryId", Integer.toString(category)),
|
||||||
pair("gameId", "432"),
|
pair("gameId", "432"),
|
||||||
pair("gameVersion", gameVersion),
|
pair("gameVersion", gameVersion),
|
||||||
@@ -76,18 +73,22 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
|
|||||||
@Override
|
@Override
|
||||||
public Optional<RemoteMod.Version> getRemoteVersionByLocalFile(LocalMod localMod, Path file) throws IOException {
|
public Optional<RemoteMod.Version> getRemoteVersionByLocalFile(LocalMod localMod, Path file) throws IOException {
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(Files.newInputStream(file)))) {
|
try (InputStream stream = Files.newInputStream(file)) {
|
||||||
int b;
|
byte[] buf = new byte[1024];
|
||||||
while ((b = reader.read()) != -1) {
|
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) {
|
if (b != 0x9 && b != 0xa && b != 0xd && b != 0x20) {
|
||||||
baos.write(b);
|
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))
|
.json(Collections.singletonList(hash))
|
||||||
.getJson(FingerprintResponse.class);
|
.getJson(FingerprintResponse.class);
|
||||||
|
|
||||||
@@ -100,20 +101,20 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RemoteMod getModById(String id) throws IOException {
|
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();
|
return JsonUtils.fromNonNullJson(response, CurseAddon.class).toMod();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Stream<RemoteMod.Version> getRemoteVersionsById(String id) throws IOException {
|
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>>() {
|
List<CurseAddon.LatestFile> files = JsonUtils.fromNonNullJson(response, new TypeToken<List<CurseAddon.LatestFile>>() {
|
||||||
}.getType());
|
}.getType());
|
||||||
return files.stream().map(CurseAddon.LatestFile::toVersion);
|
return files.stream().map(CurseAddon.LatestFile::toVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Category> getCategoriesImpl() throws IOException {
|
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>>() {
|
List<Category> categories = JsonUtils.fromNonNullJson(response, new TypeToken<List<Category>>() {
|
||||||
}.getType());
|
}.getType());
|
||||||
return reorganizeCategories(categories, section);
|
return reorganizeCategories(categories, section);
|
||||||
@@ -231,9 +232,9 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
|
|||||||
private static class FingerprintResponse {
|
private static class FingerprintResponse {
|
||||||
private final boolean isCacheBuilt;
|
private final boolean isCacheBuilt;
|
||||||
private final List<CurseAddon> exactMatches;
|
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.isCacheBuilt = isCacheBuilt;
|
||||||
this.exactMatches = exactMatches;
|
this.exactMatches = exactMatches;
|
||||||
this.exactFingerprints = exactFingerprints;
|
this.exactFingerprints = exactFingerprints;
|
||||||
@@ -247,7 +248,7 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
|
|||||||
return exactMatches;
|
return exactMatches;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Integer> getExactFingerprints() {
|
public List<Long> getExactFingerprints() {
|
||||||
return exactFingerprints;
|
return exactFingerprints;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -208,6 +208,11 @@ public final class AsyncTaskExecutor extends TaskExecutor {
|
|||||||
|
|
||||||
task.setCancelled(this::isCancelled);
|
task.setCancelled(this::isCancelled);
|
||||||
task.setState(Task.TaskState.READY);
|
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)));
|
task.setNotifyPropertiesChanged(() -> taskListeners.forEach(it -> it.onPropertiesUpdate(task)));
|
||||||
|
|
||||||
if (task.getSignificance().shouldLog())
|
if (task.getSignificance().shouldLog())
|
||||||
|
|||||||
@@ -99,6 +99,16 @@ public abstract class Task<T> {
|
|||||||
this.stage = stage;
|
this.stage = stage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String inheritedStage = null;
|
||||||
|
|
||||||
|
public String getInheritedStage() {
|
||||||
|
return inheritedStage;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setInheritedStage(String inheritedStage) {
|
||||||
|
this.inheritedStage = inheritedStage;
|
||||||
|
}
|
||||||
|
|
||||||
// properties
|
// properties
|
||||||
Map<String, Object> 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