feat(mod): mod upgrade detection.

This commit is contained in:
huanghongxun
2021-10-05 16:50:05 +08:00
parent ba0a7ddfa8
commit 23750e1222
17 changed files with 607 additions and 307 deletions

View File

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

View File

@@ -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.
*/

View File

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

View File

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

View File

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

View File

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

View File

@@ -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");
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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;
}
}

View File

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

View File

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

View File

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

View 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);
}
}

View File

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