Compare commits

...
18 Commits
Author SHA1 Message Date
Glavo fbcc17472f [release/3.16] Add version entries for 26.3 snapshots and release candidates (#6879)
https://github.com/HMCL-dev/HMCL/pull/6849
https://github.com/HMCL-dev/HMCL/pull/6877
2026-09-16 21:48:50 +08:00
Glavo 7b85f0dfce [release/3.16] 优化加载模组列表的性能 (#6878)
https://github.com/HMCL-dev/HMCL/pull/6834
https://github.com/HMCL-dev/HMCL/pull/6837
2026-09-16 21:45:50 +08:00
GlavoandApostrophe b8a830dd40 [release/3.16] 解决导出 Modrinth 整合包因大文件导致的 Out Of Memory (#6876)
https://github.com/HMCL-dev/HMCL/pull/6631

Co-authored-by: Apostrophe <Hylfrd@outlook.com>
2026-09-16 21:35:00 +08:00
GlavoandToobLac ce368caa67 [release/3.16] [Bugfix] 修复无法读取 zip 后缀名的模组的问题 (#6875)
https://github.com/HMCL-dev/HMCL/pull/6712

Co-authored-by: ToobLac <tooblac39@gmail.com>
2026-09-16 21:29:04 +08:00
Glavo 850ad82aa1 [release/3.16] fix: LegacyFabric 实例图标异常 (#6874)
https://github.com/HMCL-dev/HMCL/pull/6784
2026-09-16 21:22:58 +08:00
Glavo 552276e673 [release/3.16] 优化 SVG 图像支持 (#6873)
https://github.com/HMCL-dev/HMCL/pull/6613
https://github.com/HMCL-dev/HMCL/pull/6789
https://github.com/HMCL-dev/HMCL/pull/6790
2026-09-16 21:19:57 +08:00
Glavo e79855685e [release/3.16] 更新模组数据 (2026-07-21) (#6872)
https://github.com/HMCL-dev/HMCL/pull/6543
2026-09-16 21:14:22 +08:00
Glavo ad5afe7e06 [release/3.16] Add support for Minecraft 26.3-snapshot-10+ to unofficial supported platforms (#6871)
https://github.com/HMCL-dev/HMCL/pull/6870
2026-09-16 21:05:11 +08:00
GlavoandApostrophe 8210c77de9 [release/3.16] 修复游戏 Java 路径显示滞后问题 (#6508)
https://github.com/HMCL-dev/HMCL/pull/6419

Co-authored-by: Apostrophe <Hylfrd@outlook.com>
2026-07-25 21:39:06 +08:00
Glavo b11b298312 [release/3.16] 更新内置主题相关译文 (#6507)
https://github.com/HMCL-dev/HMCL/pull/6429
2026-07-25 21:34:11 +08:00
Glavoand辞庐 5c605c58d2 [release/3.16] 修复 GameAdvancedListItem#loadVersion UI 操作可能不会 JavaFX 线程运行的问题 (#6506)
https://github.com/HMCL-dev/HMCL/pull/6445

Co-authored-by: 辞庐 <109708109+CiiLu@users.noreply.github.com>
2026-07-25 21:32:06 +08:00
Glavo 01d4100a2f [release/3.16] 复制实例时同步保存游戏设置 (#6503)
https://github.com/HMCL-dev/HMCL/pull/6492
2026-07-25 21:26:38 +08:00
Glavoand辞庐 d07eb2f748 [release/3.16] fix: 切换版本隔离设置后自动刷新其他子页面 功能失效 (#6505)
https://github.com/HMCL-dev/HMCL/pull/6468

Co-authored-by: 辞庐 <109708109+CiiLu@users.noreply.github.com>
2026-07-25 21:26:09 +08:00
Glavo 345bb176fd [release/3.16] Add support for Minecraft 26.3-snapshot-4+ to unofficial supported platforms (#6504)
https://github.com/HMCL-dev/HMCL/pull/6425
https://github.com/HMCL-dev/HMCL/pull/6481
2026-07-25 21:23:57 +08:00
Glavo 83a38fadf0 [release/3.16] 完善游戏设置页面帮助文本 (#6416)
https://github.com/HMCL-dev/HMCL/pull/6415
2026-07-15 21:22:17 +08:00
Glavoand辞庐 9980dc8aea [release/3.16] Revert "Refactor: Remove redundant downloadSourceComboBox from ModDownloadListPageSkin" (#6413)
https://github.com/HMCL-dev/HMCL/pull/6410

Co-authored-by: 辞庐 <109708109+CiiLu@users.noreply.github.com>
2026-07-15 20:54:59 +08:00
GlavoandToobLac 7dfbf76b69 [release/3.16] 修复 1.21.1 安装信雅互联仍显示不支持 Fabric 模组的问题 (#6414)
https://github.com/HMCL-dev/HMCL/pull/6404

Co-authored-by: ToobLac <tooblac39@gmail.com>
2026-07-15 20:54:45 +08:00
Glavo 7f2f77f54f [release/3.16] Bump Gradle Wrapper Neo to 0.2.0 (#6412)
https://github.com/HMCL-dev/HMCL/pull/6397
2026-07-15 20:07:13 +08:00
33 changed files with 6727 additions and 1009 deletions

No files matched your search

@@ -297,7 +297,7 @@ public final class HMCLGameRepository extends DefaultGameRepository {
newGameSettings.getOverrideProperties().add(GameSettings.PROPERTY_RUNNING_DIRECTORY);
newGameSettings.runningDirectoryProperty().setValue("");
initInstanceGameSettings(dstId, newGameSettings);
saveGameSettings(dstId);
saveGameSettingsSync(dstId);
Path dstGameDir = getRunDirectory(dstId);
@@ -47,6 +47,7 @@ public enum VersionIconType {
case FORGE -> VersionIconType.FORGE;
case NEO_FORGE -> VersionIconType.NEO_FORGE;
case FABRIC -> VersionIconType.FABRIC;
case LEGACY_FABRIC -> VersionIconType.LEGACY_FABRIC;
case QUILT -> VersionIconType.QUILT;
case LITE_LOADER -> VersionIconType.CHICKEN;
case CLEANROOM -> VersionIconType.CLEANROOM;
@@ -29,7 +29,10 @@ import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.layout.*;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import org.jetbrains.annotations.Nullable;
public class ComponentList extends Control implements NoPaddingComponent {
@@ -136,13 +139,25 @@ public class ComponentList extends Control implements NoPaddingComponent {
}
public static Node createComponentListTitle(String title) {
HBox node = new HBox();
return createComponentListTitle(title, null);
}
public static Node createComponentListTitle(String title, @Nullable String helpMessage) {
HBox node = new HBox(4);
node.setAlignment(Pos.CENTER_LEFT);
node.setPadding(new Insets(8, 0, 0, 0));
{
Label advanced = new Label(title);
node.getChildren().setAll(advanced);
}
if (helpMessage != null) {
var helpIcon = new StackPane(SVG.HELP.createIcon(16));
FXUtils.installFastTooltip(helpIcon, helpMessage);
node.getChildren().add(helpIcon);
}
return node;
}
@@ -85,8 +85,8 @@ public final class RadioChoiceList<T extends @UnknownNullability Object> extends
group.selectedToggleProperty().addListener((observable, oldValue, newValue) -> {
Choice<T> choice = newValue != null ? choiceByToggle.get(newValue) : null;
selectedChoice.set(choice);
selectedValue.set(choice != null ? choice.getValue() : null);
selectedChoice.set(choice);
});
selectedValue.addListener((observable, oldValue, newValue) -> {
@@ -22,7 +22,10 @@ import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.controls.JFXSlider;
import com.jfoenix.controls.JFXTextField;
import javafx.beans.InvalidationListener;
import javafx.beans.property.*;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.Property;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
@@ -32,11 +35,7 @@ import javafx.geometry.Pos;
import javafx.geometry.Rectangle2D;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.TextFormatter;
import javafx.scene.control.Tooltip;
import javafx.scene.control.*;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.*;
import javafx.stage.FileChooser;
@@ -162,11 +161,13 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
{
if (isPresetSetting) {
var presetSettings = new ComponentList();
rootPane.getChildren().addAll(
ComponentList.createComponentListTitle(i18n("settings.type.global.preset")),
presetSettings,
ComponentList.createComponentListTitle(i18n("settings.game.section.basic")),
rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("settings.game.section.basic")),
basicSettings,
ComponentList.createComponentListTitle(
i18n("settings.type.global.preset"),
i18n("settings.type.global.preset.help")
),
presetSettings,
ComponentList.createComponentListTitle(i18n("settings.game.section.game")),
gameSettings,
ComponentList.createComponentListTitle(i18n("settings.launcher")),
@@ -195,6 +196,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
var parentGameSettingsPane = new LineSelectButton<GameSettings.@Nullable Preset>();
basicSettings.getContent().add(parentGameSettingsPane);
parentGameSettingsPane.setTitle(i18n("settings.type.global.preset"));
parentGameSettingsPane.setSubtitle(i18n("settings.type.global.preset.subtitle"));
parentGameSettingsPane.setConverter(setting -> setting != null
? PresetManagementPane.getPresetDisplayName(setting)
: i18n("settings.type.global.preset.default"));
@@ -1718,6 +1720,8 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
updating.value = false;
}
refresh.invalidated(instance);
fireEvent(new VersionPage.WorkingDirChangedEvent());
});
currentSetting.addListener((observable, oldValue, newValue) -> {
@@ -1806,6 +1810,9 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
updating.value = false;
}
refresh.invalidated(instance);
fireEvent(new VersionPage.WorkingDirChangedEvent());
event.consume();
});
@@ -1825,6 +1832,8 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
} finally {
updating.value = false;
}
fireEvent(new VersionPage.WorkingDirChangedEvent());
});
currentSetting.addListener((observable, oldValue, newValue) -> {
@@ -40,6 +40,7 @@ import org.jackhuang.hmcl.ui.image.apng.chunks.PngFrameControl;
import org.jackhuang.hmcl.ui.image.apng.error.PngException;
import org.jackhuang.hmcl.ui.image.apng.error.PngIntegrityException;
import org.jackhuang.hmcl.ui.image.internal.AnimationImageImpl;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
@@ -50,13 +51,16 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
/// Provides image-format detection and loaders for image formats not handled
/// directly by JavaFX.
///
/// @author Glavo
@NotNullByDefault
public final class ImageUtils {
// ImageLoaders
@@ -81,38 +85,37 @@ public final class ImageUtils {
LoaderParameters parameters = new LoaderParameters();
parameters.autoStartAnimations = false;
SVGImage image;
Supplier<Image> supplier = () -> {
SVGImage image = SVGLoader.load(content, parameters);
var snapshotParameters = new SnapshotParameters();
snapshotParameters.setFill(Color.TRANSPARENT);
if (requestedWidth <= 0. || requestedHeight <= 0.) {
return image.toImage(snapshotParameters);
}
double scaleX = requestedWidth / image.getScaledWidth();
double scaleY = requestedHeight / image.getScaledHeight();
if (preserveRatio || scaleX == scaleY) {
double scale = Math.min(scaleX, scaleY);
return image.scale(scale).toImage(snapshotParameters);
} else {
// FIXME: Use DEFAULT_SVG_SNAPSHOT_PARAMS
return image.toImageScaled(ScaleQuality.RENDER_QUALITY, scaleX, scaleY);
}
};
@Nullable Image image;
if (Platform.isFxApplicationThread()) {
image = SVGLoader.load(content, parameters);
image = supplier.get();
} else {
// TODO: Currently, SVGLoader.load(...) requires the javafx.swing module if it operates on a non-JavaFX thread.
image = CompletableFuture.supplyAsync(
() -> SVGLoader.load(content, parameters),
Schedulers.javafx()
).get();
image = CompletableFuture.supplyAsync(supplier, Schedulers.javafx()).get();
}
if (image == null)
throw new IOException("Failed to load SVG image");
var snapshotParameters = new SnapshotParameters();
snapshotParameters.setFill(Color.TRANSPARENT);
if (requestedWidth <= 0. || requestedHeight <= 0.) {
return image.toImage(snapshotParameters);
}
double scaleX = requestedWidth / image.getScaledWidth();
double scaleY = requestedHeight / image.getScaledHeight();
if (preserveRatio || scaleX == scaleY) {
double scale = Math.min(scaleX, scaleY);
return image.scale(scale).toImage(snapshotParameters);
} else {
// FIXME: Use DEFAULT_SVG_SNAPSHOT_PARAMS
return image.toImageScaled(ScaleQuality.RENDER_QUALITY, scaleX, scaleY);
}
return image;
};
public static final ImageLoader APNG = (input, requestedWidth, requestedHeight, preserveRatio, smooth) -> {
@@ -198,6 +201,7 @@ public final class ImageUtils {
// ------
/// Maximum number of leading bytes inspected when selecting an image loader.
public static final int HEADER_BUFFER_SIZE = 1024;
private static final byte[] RIFF_HEADER = {'R', 'I', 'F', 'F'};
@@ -209,12 +213,272 @@ public final class ImageUtils {
&& Arrays.equals(headerBuffer, 8, 12, WEBP_HEADER, 0, 4);
}
private static final byte[] SVG_HEADER = "<svg".getBytes(StandardCharsets.US_ASCII);
/// ASCII local name of an SVG document element.
private static final String SVG_ELEMENT_NAME = "svg";
// This is currently a simple check, more complex checks can be considered in the future
/// ASCII prefix of an XML comment.
private static final String XML_COMMENT_START = "<!--";
/// ASCII suffix of an XML comment.
private static final String XML_COMMENT_END = "-->";
/// ASCII prefix of an XML processing instruction.
private static final String XML_PROCESSING_INSTRUCTION_START = "<?";
/// ASCII suffix of an XML processing instruction.
private static final String XML_PROCESSING_INSTRUCTION_END = "?>";
/// Case-sensitive ASCII prefix of an XML document type declaration.
private static final String XML_DOCTYPE_START = "<!DOCTYPE";
/// Returns whether the supplied prefix identifies an SVG XML document.
///
/// An optional UTF-8 byte-order mark, XML whitespace, comments, processing
/// instructions, and a document type declaration may precede the document
/// element. The method identifies the document element only; it does not
/// validate the remainder of the XML document. XML markup must use its
/// ASCII byte representation; UTF-16 and UTF-32 prefixes are not recognized.
///
/// @param headerBuffer leading bytes of the candidate image
/// @return whether the first XML document element is `svg`
public static boolean isSVG(byte[] headerBuffer) {
return headerBuffer.length > SVG_HEADER.length
&& Arrays.equals(headerBuffer, 0, SVG_HEADER.length, SVG_HEADER, 0, SVG_HEADER.length);
int offset = hasUtf8ByteOrderMark(headerBuffer) ? 3 : 0;
while (true) {
offset = skipXmlWhitespace(headerBuffer, offset);
if (isSvgElementStart(headerBuffer, offset))
return true;
if (matchesAscii(headerBuffer, offset, XML_COMMENT_START)) {
offset = indexAfterAscii(
headerBuffer,
offset + XML_COMMENT_START.length(),
XML_COMMENT_END
);
} else if (matchesAscii(headerBuffer, offset, XML_PROCESSING_INSTRUCTION_START)) {
offset = indexAfterAscii(
headerBuffer,
offset + XML_PROCESSING_INSTRUCTION_START.length(),
XML_PROCESSING_INSTRUCTION_END
);
} else if (isDoctypeStart(headerBuffer, offset)) {
offset = skipDoctype(headerBuffer, offset);
} else {
return false;
}
if (offset < 0)
return false;
}
}
/// Returns whether the byte array starts with a UTF-8 byte-order mark.
///
/// @param data bytes to inspect
/// @return whether `data` begins with `EF BB BF`
private static boolean hasUtf8ByteOrderMark(byte[] data) {
return svgScanLimit(data) >= 3
&& data[0] == (byte) 0xef
&& data[1] == (byte) 0xbb
&& data[2] == (byte) 0xbf;
}
/// Returns the bounded length available to SVG detection.
///
/// @param data source bytes
/// @return the lesser of the array length and [#HEADER_BUFFER_SIZE]
private static int svgScanLimit(byte[] data) {
return Math.min(data.length, HEADER_BUFFER_SIZE);
}
/// Skips XML whitespace from an array offset.
///
/// @param data source bytes
/// @param start inclusive offset at which to begin
/// @return the first offset that does not contain XML whitespace
private static int skipXmlWhitespace(byte[] data, int start) {
int limit = svgScanLimit(data);
while (start < limit && isXmlWhitespace(data[start]))
start++;
return start;
}
/// Returns whether an SVG document-element qualified name begins at an array offset.
///
/// @param data source bytes
/// @param start offset of a possible element opening delimiter
/// @return whether the unprefixed or prefixed element has the local name `svg`
private static boolean isSvgElementStart(byte[] data, int start) {
int limit = svgScanLimit(data);
if (start < 0 || start >= limit || data[start] != '<')
return false;
int nameStart = start + 1;
int localNameStart = nameStart;
boolean hasPrefix = false;
for (int index = nameStart; index < limit; index++) {
byte value = data[index];
if (value == '>' || isXmlWhitespace(value)
|| value == '/' && index + 1 < limit && data[index + 1] == '>') {
return index - localNameStart == SVG_ELEMENT_NAME.length()
&& matchesAscii(data, localNameStart, SVG_ELEMENT_NAME);
}
if (value == ':') {
if (hasPrefix || index == nameStart)
return false;
hasPrefix = true;
localNameStart = index + 1;
continue;
}
boolean nameStartCharacter = index == nameStart || index == localNameStart;
if (nameStartCharacter
? !isAsciiXmlNameStart(value)
: !isAsciiXmlNamePart(value)) {
return false;
}
}
return false;
}
/// Returns whether a byte can begin an ASCII XML name component.
///
/// @param value byte to inspect
/// @return whether the byte is an ASCII letter or underscore
private static boolean isAsciiXmlNameStart(byte value) {
return value >= 'A' && value <= 'Z'
|| value >= 'a' && value <= 'z'
|| value == '_';
}
/// Returns whether a byte can continue an ASCII XML name component.
///
/// @param value byte to inspect
/// @return whether the byte is accepted after the first name character
private static boolean isAsciiXmlNamePart(byte value) {
return isAsciiXmlNameStart(value)
|| value >= '0' && value <= '9'
|| value == '-'
|| value == '.';
}
/// Returns whether an XML document type declaration begins at an offset.
///
/// @param data source bytes
/// @param start offset of a possible declaration
/// @return whether the case-sensitive keyword is followed by XML whitespace
private static boolean isDoctypeStart(byte[] data, int start) {
int keywordEnd = start + XML_DOCTYPE_START.length();
return matchesAscii(data, start, XML_DOCTYPE_START)
&& keywordEnd < svgScanLimit(data)
&& isXmlWhitespace(data[keywordEnd]);
}
/// Skips an XML document type declaration, including its internal subset.
///
/// Quoted values, comments, and processing instructions are ignored while
/// locating the declaration-closing `>`.
///
/// @param data source bytes
/// @param start offset of the `<!DOCTYPE` prefix
/// @return the offset after the declaration, or `-1` if it is incomplete
private static int skipDoctype(byte[] data, int start) {
int internalSubsetDepth = 0;
byte quote = 0;
int limit = svgScanLimit(data);
for (int index = start + XML_DOCTYPE_START.length(); index < limit; index++) {
byte value = data[index];
if (quote != 0) {
if (value == quote)
quote = 0;
continue;
}
if (value == '\'' || value == '"') {
quote = value;
continue;
}
if (matchesAscii(data, index, XML_COMMENT_START)) {
int commentEnd = indexAfterAscii(
data,
index + XML_COMMENT_START.length(),
XML_COMMENT_END
);
if (commentEnd < 0)
return -1;
index = commentEnd - 1;
continue;
}
if (matchesAscii(data, index, XML_PROCESSING_INSTRUCTION_START)) {
int instructionEnd = indexAfterAscii(
data,
index + XML_PROCESSING_INSTRUCTION_START.length(),
XML_PROCESSING_INSTRUCTION_END
);
if (instructionEnd < 0)
return -1;
index = instructionEnd - 1;
continue;
}
if (value == '[') {
internalSubsetDepth++;
} else if (value == ']' && internalSubsetDepth > 0) {
internalSubsetDepth--;
} else if (value == '>' && internalSubsetDepth == 0) {
return index + 1;
}
}
return -1;
}
/// Finds an ASCII sequence in a byte array.
///
/// @param data source bytes
/// @param start inclusive offset at which to begin searching
/// @param asciiValue ASCII sequence to find
/// @return the offset after the first match, or `-1` if no match exists
private static int indexAfterAscii(byte[] data, int start, String asciiValue) {
int lastStart = svgScanLimit(data) - asciiValue.length();
for (int index = start; index <= lastStart; index++) {
if (matchesAscii(data, index, asciiValue))
return index + asciiValue.length();
}
return -1;
}
/// Compares an ASCII string with bytes at an array offset.
///
/// @param data source bytes
/// @param start offset at which to compare
/// @param asciiValue ASCII sequence to compare
/// @return whether the complete sequence matches
private static boolean matchesAscii(byte[] data, int start, String asciiValue) {
if (start < 0 || start > svgScanLimit(data) - asciiValue.length())
return false;
for (int index = 0; index < asciiValue.length(); index++) {
if (Byte.toUnsignedInt(data[start + index]) != asciiValue.charAt(index))
return false;
}
return true;
}
/// Returns whether a byte is XML whitespace.
///
/// @param value byte to inspect
/// @return whether the byte is space, tab, carriage return, or line feed
private static boolean isXmlWhitespace(byte value) {
return value == ' ' || value == '\t' || value == '\r' || value == '\n';
}
private static final byte[] PNG_HEADER = {
@@ -249,7 +513,7 @@ public final class ImageUtils {
ByteBuffer buffer = ByteBuffer.wrap(headerBuffer, 8, headerBuffer.length - 8);
PngChunkHeader header;
@Nullable PngChunkHeader header;
while ((header = PngChunkHeader.readHeader(buffer)) != null) {
// https://wiki.mozilla.org/APNG_Specification#Structure
// To be recognized as an APNG, an `acTL` chunk must appear in the stream before any `IDAT` chunks.
@@ -436,7 +700,7 @@ public final class ImageUtils {
}
}
PngAnimationControl animationControl = sequence.getAnimationControl();
@Nullable PngAnimationControl animationControl = sequence.getAnimationControl();
int cycleCount;
if (animationControl != null) {
cycleCount = animationControl.numPlays();
@@ -465,6 +729,7 @@ public final class ImageUtils {
return argb;
}
/// Prevents instantiation.
private ImageUtils() {
}
}
@@ -273,18 +273,30 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
{
int rowIndex = 0;
if (control.versionSelection) {
if (control.versionSelection || !control.downloadSources.isEmpty()) {
searchPane.addRow(rowIndex);
int columns = 0;
Node lastNode = null;
if (control.versionSelection) {
JFXComboBox<String> versionsComboBox = new JFXComboBox<>();
versionsComboBox.setMaxWidth(Double.MAX_VALUE);
Bindings.bindContent(versionsComboBox.getItems(), control.versions);
selectedItemPropertyFor(versionsComboBox).bindBidirectional(control.selectedVersion);
JFXComboBox<String> versionsComboBox = new JFXComboBox<>();
versionsComboBox.setMaxWidth(Double.MAX_VALUE);
Bindings.bindContent(versionsComboBox.getItems(), control.versions);
selectedItemPropertyFor(versionsComboBox).bindBidirectional(control.selectedVersion);
searchPane.add(new Label(i18n("version")), columns++, rowIndex);
searchPane.add(lastNode = versionsComboBox, columns++, rowIndex);
}
searchPane.add(new Label(i18n("version")), columns++, rowIndex);
searchPane.add(lastNode = versionsComboBox, columns++, rowIndex);
if (control.downloadSources.getSize() > 1) {
JFXComboBox<String> downloadSourceComboBox = new JFXComboBox<>();
downloadSourceComboBox.setMaxWidth(Double.MAX_VALUE);
downloadSourceComboBox.getItems().setAll(control.downloadSources.get());
downloadSourceComboBox.setConverter(stringConverter(I18n::i18n));
selectedItemPropertyFor(downloadSourceComboBox).bindBidirectional(control.downloadSource);
searchPane.add(new Label(i18n("settings.launcher.download_source")), columns++, rowIndex);
searchPane.add(lastNode = downloadSourceComboBox, columns++, rowIndex);
}
if (columns == 2) {
GridPane.setColumnSpan(lastNode, 3);
@@ -59,12 +59,12 @@ public class GameAdvancedListItem extends AdvancedListItem {
if (repositoryChanged) {
repository = GameDirectoryManager.getSelectedRepository();
onVersionIconChangedListener = repository.onVersionIconChanged.registerWeak(event -> {
this.loadVersion();
FXUtils.runInFX(this::loadVersion);
});
if (!repository.isLoaded()) {
onRefreshedVersionsListener = EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class)
.registerWeak(event -> loadVersion());
.registerWeak(event -> FXUtils.runInFX(this::loadVersion));
return;
}
}
@@ -163,7 +163,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
}
// Sinytra Connector
if (analyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE) && modManager.hasMod("connectormod", ModLoaderType.NEO_FORGE)
if (analyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE) && (modManager.hasMod("connector", ModLoaderType.NEO_FORGE) || modManager.hasMod("connectormod", ModLoaderType.NEO_FORGE))
|| "1.20.1".equals(gameVersion) && analyzer.has(LibraryAnalyzer.LibraryType.FORGE) && modManager.hasMod("connectormod", ModLoaderType.FORGE)) {
supportedLoaders.add(ModLoaderType.FABRIC);
}
@@ -172,7 +172,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
public void add() {
FileChooser chooser = new FileChooser();
chooser.setTitle(i18n("mods.add.title"));
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("extension.mod"), "*.jar", "*.litemod"));
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("extension.mod"), "*.jar", "*.zip", "*.litemod"));
List<Path> res = FileUtils.toPaths(chooser.showOpenMultipleDialog(Controllers.getStage()));
if (res == null) return;
@@ -1634,11 +1634,14 @@ settings.type.global.preset=Preset
settings.type.global.preset.create=Create Preset
settings.type.global.preset.default=Use Default Preset
settings.type.global.preset.duplicate_name=Preset name already exists
settings.type.global.preset.help=A game settings preset manages a set of game settings that you can adjust on this page.\n\
All instances inherit game settings from the current preset by default. You can override specific settings on the instance game settings page.
settings.type.global.preset.manage_all=Current Preset
settings.type.global.preset.name=Preset Name
settings.type.global.preset.rename=Rename Preset
settings.type.global.preset.remove=Delete Preset
settings.type.global.preset.remove.confirm=Are you sure you want to delete preset "%s"? Instances using this preset will fall back to the default preset.
settings.type.global.preset.subtitle=The current instance will inherit game settings from the specified preset
settings.type.global.preset.auto_name=Preset %d
shaderpack.download.title=Download Shader - %1s
@@ -1294,7 +1294,7 @@ settings.game.inherit_global=目前繼承遊戲設定預設。點擊後使用實
settings.game.instance_settings.unsupported=目前遊戲設定檔案是由更高版本的 HMCL 儲存的,目前版本的 HMCL 無法正常載入。
settings.game.presets.unsupported=遊戲設定預設檔案是由更高版本的 HMCL 儲存的,目前版本的 HMCL 無法修改全域遊戲設定。
settings.game.isolation=版本隔離
settings.game.isolation.subtitle=啟用後,目前實例將使用獨立的遊戲執行路徑設定,而不是繼承全域設定。
settings.game.isolation.subtitle=啟用後,目前實例將使用獨立的遊戲執行路徑設定,而不是繼承全域設定
settings.game.java_directory=遊戲 Java
settings.game.java_directory.auto=自動選取合適的 Java
settings.game.java_directory.auto.not_found=沒有合適的 Java
@@ -1425,11 +1425,14 @@ settings.type.global.preset=預設
settings.type.global.preset.create=新增預設
settings.type.global.preset.default=使用預設項目
settings.type.global.preset.duplicate_name=預設名稱已存在
settings.type.global.preset.help=遊戲設定預設管理著一組遊戲設定,你可以在下方調整這些設定。\n\
所有實例預設繼承當前預設中的遊戲設定,你可以在實例遊戲設定頁面中覆蓋特定的設定。
settings.type.global.preset.manage_all=當前預設
settings.type.global.preset.name=預設名稱
settings.type.global.preset.rename=重新命名預設
settings.type.global.preset.remove=刪除預設
settings.type.global.preset.remove.confirm=確定要刪除預設「%s」嗎?使用該預設的實例將改用預設項目。
settings.type.global.preset.subtitle=當前實例將繼承指定預設中的遊戲設定
settings.type.global.preset.auto_name=預設 %d
shaderpack.download.title=光影下載 - %1s
@@ -1299,7 +1299,7 @@ settings.game.inherit_global=当前继承游戏设置预设。点击后使用实
settings.game.instance_settings.unsupported=当前游戏设置文件是由更高版本的 HMCL 保存的,当前版本的 HMCL 无法正常加载。
settings.game.presets.unsupported=游戏设置预设文件是由更高版本的 HMCL 保存的,当前版本的 HMCL 无法修改全局游戏设置。
settings.game.isolation=版本隔离
settings.game.isolation.subtitle=启用后,当前实例将使用独立的游戏运行路径设置,而不是继承全局设置。
settings.game.isolation.subtitle=启用后,当前实例将使用独立的游戏运行路径设置,而不是继承全局设置
settings.game.java_directory=游戏 Java
settings.game.java_directory.auto=自动选择合适的 Java
settings.game.java_directory.auto.not_found=没有合适的 Java
@@ -1430,11 +1430,14 @@ settings.type.global.preset=预设
settings.type.global.preset.create=新建预设
settings.type.global.preset.default=使用默认预设
settings.type.global.preset.duplicate_name=预设名称已存在
settings.type.global.preset.help=游戏设置预设管理着一组游戏设置,你可以在下方调整这些设置。\n\
所有实例默认继承当前预设中的游戏设置,你可以在实例游戏设置页面中覆盖特定的设置。
settings.type.global.preset.manage_all=当前预设
settings.type.global.preset.name=预设名称
settings.type.global.preset.rename=重命名预设
settings.type.global.preset.remove=删除预设
settings.type.global.preset.remove.confirm=确定要删除预设“%s”吗?使用该预设的实例将回退到默认预设。
settings.type.global.preset.subtitle=当前实例将继承指定预设中的游戏设置
settings.type.global.preset.auto_name=预设 %d
shaderpack.download.title=光影下载 - %1s
File diff suppressed because it is too large. Load diff
+140 -36
View File
@@ -152,7 +152,7 @@ scientists-dream;145;;Scientific Dreams;;
abandend;146;;AbandEnd;;
tech-expansion;147;;Tech Expansion;;
minefanstasyworld;148;;我的幻想世界;MineFantasyWorld;
slightly-vanilla-flavoured;149;;Slightly Vanilla Flavoured;;
slightly-vanilla-flavoured;149;;Slightly Flavoured / Slightly Vanilla Flavoured;;
all-the-mods;150;;All the Mods;;ATM1
crackpack-3;151;;Crackpack 3;;
valhelsia-3;152;;Valhelsia 3;;
@@ -213,7 +213,7 @@ better-minecraft-plus;207;;Better Minecraft [PLUS];;
parasites-by-forge-labs;208;;Parasites by Forge Labs;;
create-live;209;;Create Live;;
airaka-tech;211;;艾瑞卡科技;Airaka Tech;
;212;;TeaCon;;
;212;;TeaCon整合包;TeaCon;
originssmp;213;;起源SMP;OriginsSMP;
create-live-2;214;;Create Live 2;;
origin-smp-copy;215;;Origin - SMP;;
@@ -252,7 +252,7 @@ better-minecraft-otv-edition;249;;Better Minecraft (OfflineTV Edition);;
evolution-reset;250;;进化:重启;Evolution: Reset;ER
the-nuclear-wasteland;251;;核荒原;The Nuclear Wasteland;
blessed-or-cursed-expedition-to-infinite-force;252;;祝福或诅咒:无限力量之征;Blessed or Cursed: Expedition to Infinite Force;BoC
;253;;边界生存;Border Survival;BSV
;253;;边界生存:存世;Border Survival;BSV
medieval-mc-forge-mmc1;255;;Medieval Minecraft;;MMC
heavens-of-sorcery;256;;巫术天堂;Heavens of Sorcery;
herodotus;257;;希罗多德;Herodotus-Authentic Edition:Official;HDS
@@ -498,7 +498,7 @@ other-body;504;;Other Body;;
;506;;莱姆星云;Lime Nebula;LN
void-genesis;507;;Void Genesis;;
create-flavored;508;;Create Flavored;;
terrafirmagreg-vintage;509;;TerraFirmaGreg;;TFG
terrafirmagreg-vintage;509;;群峦格雷;TerraFirmaGreg;TFG
create-astral;510;;机械动力:星辰;Create: Astral;
;511;;起源创造;CreateOrigin;CO
random-item-skyblock-recondite-i;512;;Random Item Island - Recondite I;;
@@ -516,7 +516,7 @@ pick-the-stars;523;;摘星;Pick The Stars;PTS
earth-technology;524;;Earth Technology;;
;525;;核乐不为;;ZMC
ftb-stoneblock-3;526;;FTB StoneBlock 3;;
create-mod-plus;527;;Create +;;
create-mod-plus;527;;Create+;;
multiplayer-optimization;528;;RawDiamondMC的优化包;RawDiamondMC's optimization pack;MPO
chroma-endless;529;;Chroma Endless;;
ragnamod-vi-in-the-sky;530;;Ragnamod VI In The Sky;;
@@ -548,7 +548,7 @@ gregic-skies;555;;Gregic Skies;;
;557;;齿轮颂歌:暮光;Gear Carol;
pluma;558;;Pluma, a Journey to the Future!;;Pluma
all-the-mods-7-to-the-sky;559;;All the Mods 7 - To the Sky;;ATM7S
;560;;月见苍穹传说;TskimiCanopyCraft;TSCC
;560;;月见苍穹传说;TsukiCupolaCraft;TSCC
;561;;最后的战役:劫后余生;The Last Stand:Aftermath;LSA/TLS
;562;;小猪配平;Peppa Pig's Burning Broom;2P2B
zerblands-remastered;563;;Zerblands-Remastered;;
@@ -672,7 +672,7 @@ a-new-technological-era;681;;A New Technological Era;;ANTE
musketeer;683;;Musketeer - One for All;;
;684;;匠造之传;The Legend of Tinker;TLT
;685;;我的骑士:怪物大乱斗;;
;686;;塔可努的原生优化整合包;TechNoob's Vanilla Boost Pack;TNVBP
;686;;塔可努的原生优化整合包;TNF-Vanilla / TechNoob's Vanilla Boost Pack;TNVBP
;687;;灵茗屿空岛;LMY SKY;
;688;;红石生电优化;Redstone Survival Optimization;RSO
;689;;恐惧深渊;Abyss of Fear;AoF
@@ -734,7 +734,7 @@ life-in-the-village-3;740;;Life in the village 3;;LitV3
cook-in-skyblock;746;;天空厨房;Cook in skyland;CiS
;747;;被恐惧支配 1;Dominated by Fear 1;DoF
new-vanilla-newhome;748;;新的原版;New Vanilla;NVA
;749;;峦屿梦星群;Terrafirma Rescue Unofficial;TFRU
tfru;749;;峦屿梦星群;Terrafirma Rescue Unofficial;TFRU
stoneopolis;750;;Stoneopolis;;
ftb-skies-expert;751;;FTB Skies Expert;;
wmshd-pack-what-mojang-should-have-done;752;;What Mojang Should Have Done;;WMSHD
@@ -806,7 +806,6 @@ la;818;;胶兽大冒险;Latex Advanture;LA
mystics-monstrosity;819;;Mystic's Monstrosity;;
;820;;更完美的MC;More Perfect Minecraft;MPMC
;821;;动画世界:烂柯人;AnimateCraft: Lankeren;
;822;;New Create;;NC
land-of-memories;823;;回忆之地;;LOM
;824;;异世界冒险2;;
ragnamod-vii;825;;Ragnamod VII;;
@@ -866,7 +865,7 @@ create-vanilla-exploration;879;;Vanilla Create Exploration;;
progress;880;;Progress: Create;;
aeon-automata;881;;Aeon Automata;;AA
;882;;被恐惧支配2:收容失效;Dominated by Fear 2 Containment Breach;DoF2
tensioncraft;883;;剑拔弩张之时;;TC
;883;;剑拔弩张之时;;TC
project-architect-2;884;;Project Architect 2;;
;885;;接入铁路网络:我的世界铁路;MTR Railway Network Access;RNA-MTR
create-mekanized;886;;Create: Mekanized;;
@@ -923,24 +922,23 @@ setgamedifficulty-reika;936;;SetGameDifficulty Reika;;
epic-journey-gtceu;937;;Epic Journey GTCEu;;
terrafirmacraft-leap-the-top;938;;飞渡群峦;TerraFirmaCraft:Leap The Top;TFC:LTT
gregtech-skybound;939;;Gregtech: Skybound;;
simple-tfc;940;;简单群峦;Simple TFC;
simple-tfc;940;;简单群峦;Simple TFC;SPTFC
ftb-presents-direwolf20-s14;941;;FTB Presents Direwolf20 1.21;;
final-judgment-save-the-world;942;;Final Judgment:Save the world;;
;943;;极端世界:重置;Extreme World Reset;EWR
explore-everything;944;;空岛:探索;Explore Everything;SEE
epic-journey-gtceu-conquerors-edition;945;;Epic Journey GTCEu - Conqueror's Edition;;
the-undead-blooms;946;;亡灵绽放 | 真菌孢子启示录 |;The Undead Blooms | Fungal Spore Apocalypse |;
;947;;星地征程;Starland Journey;SJ
;948;;希望与绝望-惊变100天;Hope and Despair;had
;947;;星地征程;Starland Journey re;SJRE
;948;;希望与绝望-惊变100天;Hope and Despair;HAD
;949;;梦篡光阴;Dreams Distort Overture;DDO
;950;;最后的光芒;The Last Light;TLL
endil-expert-edition;951;;Endil's Expert Edition;;EEE
peace-of-mind;952;;Peace of Mind;;
distantly-optimized;953;;Distantly Optimized;;DO
farming-experience;954;;Farming Experience;;
hungry-for-tech-2;955;;Hungry For Tech 2;;
hungry-for-tech-2;955;;渴望科技2;Hungry For Tech 2;
remain;957;;Remain;;
;958;;末日狂徒;;
gregtania6-skyblock;959;;Gregtania6 Skyblock;;
harmonious-engineering;960;;Harmonious Engineering;;
valley-of-farming-and-ranching;961;;农牧物语;Valley of Farming and Ranching;
@@ -985,7 +983,7 @@ industrial-machines;1000;;Industrial Machines;;
auto-terrafirmacraft-tfc-create;1001;;Auto-TerraFirmaCraft (TFC + Create);;ATFC
;1002;;时间之书:星的日记;The Book of Time: A Star's Journal;TASL
;1003;;特摄世界;Tokusatsu World;TW
journey-of-reincarnation;1004;;转世之旅;Tensura: Journey of Reincarnation;
;1004;;转世之旅;Tensura: Journey of Reincarnation;
;1005;;冒险与休闲;Adventure and Leisure;AL
enigmatica10;1006;;Enigmatica 10;;E10
mc-chocolate-edition;1007;;Chocolate Edition Minecraft;;
@@ -1002,7 +1000,7 @@ kumo-o-tagayasu;1017;;耕云钓月;Kumo O Tagayasu;KOTS
;1018;;无限战争4;Infinity War4;INF4
;1019;;群峦传说:工业长路;;
;1020;;自然冒险 新生;Nature Adventure Newborn;NAB
hmbo;1021;;羽舲的基础优化;Hanekmio's Basis Optimization;
asig;1021;;浅忆基础整合;Asai Integrated Modpack;
create-star-valley;1022;;Create: Gears of History;;
;1023;;阿尔法计划;AlphaPlan;AP
spell-dimension;1024;;咒次元;Spell Dimension;SD
@@ -1015,7 +1013,7 @@ terminal;1030;;终末;Terminal;
isolated-crystal-3;1031;;异次元水晶 3;Isolated Crystal 3;
;1032;;机械动力:无限总是小于零;Create:Infinity Always Smaller Than Zero;CIASZ
;1033;;踪灭I-起源;;
;1034;;踪灭II;;
;1034;;踪灭II;Endtime II;
rabbits-dawn-craft;1035;;某兔子的破晓之界;A Certain Rabbit's DawnCraft - Dawn of the Deep Soul;RDC
;1036;;铁砧天空;;ASKY
grand-architect-journey;1037;;伟大工程巡礼;Grand Architecture Journey;GAJ
@@ -1075,7 +1073,7 @@ roguelike-adventures-and-dungeons-3;1090;;冒险与地牢3;Roguelike Adventures
wheelchair-gregtech;1093;;Wheelchair Gregtech;;
egged-up;1094;;Egged Up;;
;1095;;香草纪元:食旅纪行;VanillaEra: FaresChron;VEFC
;1096;;愚者;The Fool;TF
the-fool;1096;;愚者;The Fool;TF
;1097;;时间之书2:星梦指南;BookofTime II Stardream Compass;BKSC
;1098;;群峦之上:无垠伊土;EndlessFirma;EnF
;1099;;血月低语:暗夜惊魂;Bloodmoon's Whisper;BW
@@ -1119,11 +1117,11 @@ life-in-the-village-4;1136;;Life in the village 4;;LITV4
real-island-2-modular-craft;1137;;真实空岛2:模块化之旅;Real Island 2 - Modular Craft;RI2
;1138;;奥术咖啡馆;Arcane Cofferia;
unyielding;1139;;Unyielding;;
;1140;;剑与王国;;SNK
;1140;;剑与王国;;sword_and_kingdom
the-fungal-infection-coop;1141;;The Fungal Infection | Co-Op Base Defense;;
mc-eternal-2;1142;;永恒的MC 2;MC Eternal 2;MCE2
solo-leveling-reawakening;1143;;Solo Leveling - Reawakening;;
create-ultimate-selection;1144;;Create: Ultimate Selection - QUESTS NOW!;;
create-ultimate-selection;1144;;Create: Ultimate Selection;;
;1145;;元素觉醒;Elemental Awakening;
endless-horror-the-ultimate-horror-modpack;1146;;Endless Horror - The ULTIMATE Horror Experience;;
;1147;;锻造之旅;;
@@ -1145,13 +1143,13 @@ blue-archive-x-mineraft;1163;;我的世界x蔚蓝档案;BlueArchive x Minecraft;
;1164;;Fresh & Smooth;;
;1165;;机动重启工程:风之旅;The Caravan of Winds;TCW
;1166;;Furry Fandom Club Boss Rush Official Offline;;FFCBR
immersive-ultra;1167;;极致沉浸;Immersive Ultra;
;1167;;极致沉浸;Immersive Ultra;
;1168;;Polymania!;;
survivearchive;1169;;SurviveArchive - A Blue Archive modpack;;
beloong;1170;;化龍;BeLoong;
;1171;;畅游自然;;SiF
;1171;;畅游自然;;SiN
;1172;;退休工艺;Retirement Craft;RmC
;1173;;溯光暗潮:云影;NS's Random Gunfight;NRG
;1173;;溯光暗潮;TRACING LIGHT · DARK TIDE;NRG
;1174;;雪狐的基础整合;SF Integrated Modpack;SFI
;1175;;硬核惊变;Hardcore Surprise;HS
society-sunlit-valley;1176;;Society: Sunlit Valley;;
@@ -1179,7 +1177,7 @@ repalmon;1198;;Re帕鲁梦;RePalmon;RPLM
structural-ingenuity;1199;;结构型奇思妙想;Structural Ingenuity;
arcaneaxis-ars;1200;;秘法象限;ArcaneAxis;ARS
gregtech-easy;1201;;格雷科技简单版;GregTech Easy;GTE
;1202;;机境交响;Creation Exploring the Road of;CETR
create-exploring-the-path-of;1202;;机境交响;Creation Exploring the Road of;CETR
multiblockful-skies;1203;;Multiblockful Skies;;
;1204;;刀剑物语;SwordStory;SS
;1205;;无垠征途;The Unbounded Expedition;TUE
@@ -1194,13 +1192,13 @@ farmy-skyblock;1213;;Farmy Skyblock;;
bloody-skyblock-modpack;1214;;Bloody Skyblock;;
starages;1215;;Star Space Ages;;
create-spectral-cuisine;1216;;Create: Spectral Cuisine;;
project-infinity-0-1;1217;;Project Infinity 0.1;;
project-infinity-0-1;1217;;无限计划;Project Infinity 0.1;
create-live-6;1218;;Create Live 6;;
;1219;;全面包围-硬核惊变;Full encirclement;FLET
;1220;;科波菲尔;Copperfield;CPF
;1221;;试用核心/I.O.P的水槽基础整合;Trial Essential;
tnp-limitless-8;1222;;TNP Limitless 8;;
;1223;;重度机械症;Mechanomania;
mechanomania;1223;;重度机械症;Mechanomania;
;1224;;寻此苦旅3;Per Aspera3;PA3
calamity-rpg-iii-wounds-of-the-world;1225;;Calamity RPG III : Wounds of the World;;
;1226;;烛枫公会优化整合包;;ZFGH
@@ -1221,17 +1219,17 @@ erstwhile-journey-2;1240;;往昔之旅2;Erstwhile Journey 2;EJ2
;1241;;诡厄:使徒;;GRAE
;1242;;时间之书 : 星之旅行-重制版;BookofTime StarTravel;BKSL
cook-in-skyland-elegant-feast;1243;;天空厨房:典雅盛宴;Cook in Skyland Elegant Feast;CiS2
ftb-stoneblock-4;1244;;FTB StoneBlock 4;;SB4
ftb-stoneblock-4;1244;;石头世界4;FTB StoneBlock 4;SB4
techevdiscovery;1245;;TechEv || Discovery;;
g3-project;1246;;G3 project;;
;1247;;新州;Xin Zhou;XZ
;1247;;新州;Xinzhou;XZ
emc-and-fe-plus3;1248;;EMC & FE +++;;E&F2
terracraft-confluence;1249;;泰拉工艺:汇流;TerraCraft:Confluence;TCC
;1250;;探索领域;Explore Realm;ER
beyond-cosmo;1251;;Beyond Cosmo;;
;1252;;闲居物语;Travel Architecture Dreams;TAD
;1252;;闲居物语;Travel Serere Homestead;TSH
high-altitude-alchemy;1253;;High-Altitude Alchemy;;
;1254;;机械动力: 界位工程师;Create Boundary Engineer;CBE
create-boundary-engineer;1254;;机械动力: 界位工程师;Create Boundary Engineer;CBE
skyopolis-evolved;1255;;Skyopolis: Evolved;;
;1256;;IFR的1.12.2优化整合包;IFR's Forgified Optimization;IFO
;1257;;无穷匠艺;;
@@ -1262,7 +1260,7 @@ mythcrafts;1281;;MYTHCRAFT 5;;MC
;1282;;牧场物语;Harvest Moon;
;1283;;逃逸:寄生末日;SRP: Escape Parasite Doom;SRP:epd
;1284;;Ice and M;;IAM
;1285;;秋水;autumn floods;AF
autumn-floods;1285;;秋水;autumn floods;AF
;1286;;V2O5;;
;1287;;地牢冒险者;Dungeon Adventurer;DA
ultramodern-optimization;1289;;超现代优化;Ultramodern optimization;UO
@@ -1273,14 +1271,14 @@ pandemonium-nexus-anarchic-aeon;1293;;万象枢纽:无序纪元;Pandemonium Ne
;1294;;菌染亵渎;Fungal Infection Blasphemy;FIB
;1295;;Ice and M:Sky;;IAMS
;1296;;TUFF PERFORMANCE;;
;1297;;灾厄巫咒;;
;1297;;灾厄巫咒;Curlamoety;
;1298;;子弹呼啸 : 重制;Bullet Whizz : Remake;BWR
mybiomesobeautiful;1299;;My Biomes Is So Beautiful;;
create-capitalist;1300;;Create Capitalist;;
diving-beyond;1301;;Diving Beyond;;
;1302;;奇星·奥术;Mystic Star_Arcane;
guild-adventures;1303;;Guild Adventures;;
;1304;;艾斯爱慕:新视野;Spore and M:New Horizons;SMNH
;1304;;艾斯爱慕:无限构件:现代工业;Spore and M:Infinity Mechanism:Modern Industrialization;SMIM
otherworld-dnd;1305;;Otherworld [Dungeons & Dragons];;
;1306;;奇幻起源;Fantasy Origins;
projects;1307;;ProjectS;;
@@ -1318,7 +1316,7 @@ farming-crossing-6-dinner-party;1334;;Farming Crossing 6: Dinner Party!;;
;1339;;机械动力:枪火不断;Create: Gunfire Never Stops;CGNS
;1340;;逆转未来;Battle For The Future;BFF
day-of-mind;1341;;DayOfMind;;DOM
;1342;;机械动力:探险生存 2;Create Adventure Survival 2;
create-adventure-survival-ii;1342;;机械动力:探险生存 2;Create Adventure Survival II;
;1343;;云游四海;WAYFARING;WF
;1344;;阿卡迪亚的天启;The Apocalypse of Arcadia;TAOA
;1345;;纯净原版・焕新;Pure Vanilla Renewal;PVR
@@ -1327,4 +1325,110 @@ day-of-mind;1341;;DayOfMind;;DOM
dragns-o-suite-pack;1348;;DragN's O-Suite Pack;;
;1349;;机械动力:工业与牧歌;Create: Industry and Pastoral;CIAP
;1350;;灾厄纪行;;
beebeeblock-the-skyhive;1351;;蜂蜂空坊;Beebeeblock: The Skyhive;BTS
beebeeblock-the-skyhive;1351;;蜂蜂空坊;Beebeeblock: The Skyhive;BBB
the-last-world-shines;1352;;末世生辉;The Last World Shines;TLWS
;1353;;MineOptimiz;;
journey-to-infinity;1354;;Journey To Infinity;;
;1355;;混元登仙;;HATI
flying-path;1356;;飞舞之路;Flying Path;
create-craft-quiet;1357;;机械动力:匠途闲境;Create:Craft&Quiet;ccq
sky-new-horizons;1358;;Sky New Horizons;;SkyNH
;1359;;天启之路;The Path Of Revelation;TPOR
mana-and-mechanisms;1360;;Mana & Mechanisms;;
;1361;;胶兽异界之旅Ⅱ;;
mechanical-dragon;1362;;龙之机械;Dragon create;DC
;1363;;香草精华;;VE
submerged-2;1364;;Seaopolis Submerged 2;;
;1365;;天煞孤星;A STAR DOOMED TO LONELINESS;ALONG
;1366;;全亡灵启示录;All Undead Apocalypse;AUA
;1367;;空流整合包;SkyFlow;
;1368;;飞升者的陨落;Fall of Ascension;FOA
;1369;;万华工艺录;KaleidoCraftia;KlC
;1370;;多方块笙息空岛;;DSK
etbw2-demo;1371;;探索光明的世界2;Explore the Bright World 2;EtBW2
stellarisworkshop;1372;;孤星工坊;StellarisWorkshop;GTSW
enders-journey-beyond-the-end;1373;;Ender's Journey - Beyond The End;;
mysticalblockreloaded;1374;;Mystical Block Reloaded;;
cozy-mizuno-minecraft;1375;;A Cozy Mizuno World;;
;1376;;新生异土;Tensura Neo Otherworld;
extraordinary-energy-modern;1377;;Extraordinary Energy : Modern;;
;1378;;铁砧工艺-七天创世;;
avaritia-project;1379;;Avaritia Project;;
after-the-nukes;1380;;After The Nukes;;
abyssal-ascent;1381;;Abyssal Ascent;;
desolate-planet;1382;;Desolate Planet;;
;1383;;德雷马尔:神化;Drehmal: Apotheosis;DHA
project-arc-light-the-hanging-pavilion;1384;;弧光计划:空中楼阁;Project Arc Light: The Hanging Pavilion;PAHP
;1385;;文明:创世;Civilization Isolaria;
blockitecture;1386;;方块建筑学;Blockitecture (Block Architecture);
;1388;;曙光空岛;;
;1389;;千界一梦;The Oneiropolis;TOP
;1390;;乌托星岛·创世旅途 3.0;EpicUraNepAdventure 3.0;EUNA3
itaj;1391;;It Takes a Journey;;ITAJ
taltale-pack;1392;;Taltale Pack;;
waccyys-fantasia;1393;;waccyy's Fantasia;;
all-the-mods-11;1394;;All The Mods 11;;ATM11
create-live-7;1395;;Create Live 7;;
;1396;;锻造之旅—无尽征途;;wjzt
;1397;;魔法现代化工业笙息空岛;;MXSK
nuclear-tech-integrated;1398;;核科技:集成;Nuclear Tech: Integrated;
crashed;1399;;Crashed;;
;1400;;格雷科技:万子科技;Greg Tech : Wanzi Technology;GTWT
moddedtogether;1401;;Modded Together;;
;1402;;附魔工程;;
;1403;;机械动力:航空学新视野2.0;Create Aero: New Horizons2.0;CANH
;1404;;Better with Datapack;;
restlesshorizons;1405;;Restless Horizons;;RH
;1406;;虚空行者;Void Wayfarer;VW
;1407;;模块化整合包:农业;Modular Modpack: Agriculture;MMA
the-song-of-crafting;1408;;合成颂歌;The Song of Crafting;
awakening;1409;;AMP - Awakening Classic;;
;1410;;末世再现;;CATT
;1411;;死亡突围:朝花暮拾;DeadoutRe:Duskora;DRD
craftoria-2;1412;;Craftoria 2;;
;1413;;这是一个水槽包;This is the sink pack;
;1414;;绝境中的生存;Survival in Desperation;
;1415;;等价无尽机械;PeojectE Avaritia Create;
;1416;;破灭与重生:破晓之途;Ruins and Rebirth:Path to Daybreak;RRPD
;1417;;最后那些天-低性能优化版;Those Last Day Boost;
;1418;;涅槃;Nirvana;UPN
enigmatic-skies;1419;;Enigmatic Skies;;
;1420;;Spark+ 原版红石增强优化;Spark+ Vanilla Redstone Optimization;SVRO
;1421;;混沌之地;TheChaoticnia;
;1422;;消逝的曙光;Dusk of Dawn;DOW
cook-in-skyland-recaked;1423;;天空厨房:重制版;Cook in Skyland Recaked;CiSR
rlcraft-fanmade;1424;;RLCraft Fanmade;;
unaltered;1425;;Unaltered+;;
sendims-banforges;1426;;千界万锻;Myriad Dimensional Forge;SDBF
;1427;;超级优化;Super Optimized;SO
society-sunlit-cobblemon;1428;;Society: Sunlit Cobblemon;;
create-aeronautica;1429;;Create Aeronautica;;
;1430;;群星入侵;Astrincursion;
;1431;;涟漪之篇;As Written By Whom;AWBW
starbuncles-gift;1432;;星宝石兽的礼物;starbuncles gift;
;1433;;Ember Peril: Silent Expanse;;EPSE
sky-bees-reborn;1434;;Sky Bees Reborn;;
who-am-i-not-me;1435;;我非我;Who Am I - Not Me;
all-the-hacks;1436;;All The Hacks;;ATH
;1437;;新:模拟大都市;;NSUK
;1438;;异界工匠;;
;1439;;工业升级指导版;Industrial upgrade 101;IU101
;1440;;假面骑士全明星编年史;;KRASC
;1441;;极速优化;Ultra Fast Optimization;UFO
new-age-science-and-technology;1442;;新时代科技;New Age Science and Technology;NAST
;1443;;屎生万物;EverythingFromPoop;EFP
;1444;;天顶征途;Zenith Conquest;
;1445;;瓶中伊甸;Eden in the Flask;EITF
aircrashed-engineer-ii;1446;;坠机工程师2;Aircrashed Engineer II;ACEII
star-avaritia;1447;;寰宇星光;Star Avaritia;SA
astrids-fnaf-pack;1448;;Astrid's FNAF Pack;;
hp-create-world-of-gears;1449;;HP - Create World of Gears;;
the-cursed-wanderer-reborn;1450;;咒游诸界:重生;The Cursed Wanderer: Reborn;TCWR
;1451;;机械之旅;Mechanical Create;
;1452;;探索MC小马的世界;Explore MLP World;EPW
tech101;1453;;科技入门学;Tech101;
endfield-new-horizons;1454;;终末地工业:新视野;Endfield:New Horizons;efnh
dungeon-train;1455;;地牢列车;Dungeon train;
project-arc-light-dimensional-rift;1456;;弧光计划:维度裂隙;Project Arc Light - Dimensional Rift;PA2
;1457;;恶意世界;HostileCraft;HC
adj;1458;;A Distant Journey;;ADJ
File diff suppressed because it is too large. Load diff
@@ -1,82 +1,119 @@
{
"$schema" : "https://schemas.glavo.site/hmcl/theme-pack/1.0.0",
"id" : "hmcl.classic",
"version" : "1.0.0",
"name" : {
"default" : "Classic",
"zh-Hans" : "经典主题",
"zh-Hant" : "經典主題"
"$schema": "https://schemas.glavo.site/hmcl/theme-pack/1.0.0",
"id": "hmcl.classic",
"version": "1.0.0",
"name": {
"default": "Classic",
"es": "Clásico",
"ja": "クラシック",
"ru": "Классическая",
"uk": "Класична",
"zh-Hans": "经典主题",
"zh-Hant": "經典主題",
"lzh": "舊觀"
},
"description" : {
"default" : "Classic HMCL launcher themes",
"zh-Hans" : "HMCL 启动器经典主题",
"zh-Hant" : "HMCL 啟動器經典主題"
"description": {
"default": "Classic HMCL launcher themes",
"es": "Temas clásicos del launcher HMCL",
"ja": "HMCL ランチャーのクラシックテーマ",
"ru": "Классические темы лаунчера HMCL",
"uk": "Класичні теми лаунчера HMCL",
"zh-Hans": "HMCL 启动器经典主题",
"zh-Hant": "HMCL 啟動器經典主題",
"lzh": "HMCL 啟動器之舊觀"
},
"authors" : [
"authors": [
{
"name" : "HMCL"
"name": {
"default": "HMCL",
"en-Qabs": "ꞀƆWH"
}
}
],
"themes" : [
"themes": [
{
"id" : "2021-08-26",
"name" : "HMCL 3.15",
"description" : {
"default" : "HMCL 3.15 classic appearance",
"zh-Hans" : "HMCL 3.15 经典外观",
"zh-Hant" : "HMCL 3.15 經典外觀"
"id": "2021-08-26",
"name": {
"default": "Current Theme",
"es": "Tema actual",
"ja": "現在のテーマ",
"ru": "Текущая тема",
"uk": "Поточна тема",
"zh-Hans": "当前主题",
"zh-Hant": "當前主題",
"lzh": "今觀"
},
"color" : "#5C6BC0",
"brightness" : "light",
"colorStyle" : "fidelity",
"background" : {
"type" : "builtin",
"id" : "2021-08-26",
"opacity" : 1.0
"description": {
"default": "HMCL current theme",
"es": "Tema actual de HMCL",
"ja": "HMCL の現在のテーマ",
"ru": "Текущая тема HMCL",
"uk": "Поточна тема HMCL",
"zh-Hans": "HMCL 当前主题",
"zh-Hant": "HMCL 當前主題",
"lzh": "HMCL 之今觀"
},
"titleBar" : {
"transparent" : false
"color": "#5C6BC0",
"brightness": "light",
"colorStyle": "fidelity",
"background": {
"type": "builtin",
"id": "2021-08-26",
"opacity": 1.0
},
"titleBar": {
"transparent": false
}
},
{
"id" : "2016-02-25",
"name" : "HMCL 3.3",
"description" : {
"default" : "HMCL 3.3 classic appearance",
"zh-Hans" : "HMCL 3.3 经典外观",
"zh-Hant" : "HMCL 3.3 經典外觀"
"id": "2016-02-25",
"name": "HMCL 3.3",
"description": {
"default": "HMCL 3.3 classic appearance",
"es": "Apariencia clásica de HMCL 3.3",
"ja": "HMCL 3.3 のクラシックな外観",
"ru": "Классический вид HMCL 3.3",
"uk": "Класичний вигляд HMCL 3.3",
"zh-Hans": "HMCL 3.3 经典外观",
"zh-Hant": "HMCL 3.3 經典外觀",
"lzh": "HMCL 3.3 之舊觀"
},
"color" : "#5C6BC0",
"brightness" : "light",
"colorStyle" : "fidelity",
"background" : {
"type" : "builtin",
"id" : "2016-02-25",
"opacity" : 1.0
"color": "#5C6BC0",
"brightness": "light",
"colorStyle": "fidelity",
"background": {
"type": "builtin",
"id": "2016-02-25",
"opacity": 1.0
},
"titleBar" : {
"transparent" : true
"titleBar": {
"transparent": true
}
},
{
"id" : "2015-06-22",
"name" : {
"default" : "HMCL 2.3"
"id": "2015-06-22",
"name": {
"default": "HMCL 2.3"
},
"description" : {
"default" : "HMCL 2.3 classic appearance",
"zh-Hans" : "HMCL 2.3 经典外观",
"zh-Hant" : "HMCL 2.3 經典外觀"
"description": {
"default": "HMCL 2.3 classic appearance",
"es": "Apariencia clásica de HMCL 2.3",
"ja": "HMCL 2.3 のクラシックな外観",
"ru": "Классический вид HMCL 2.3",
"uk": "Класичний вигляд HMCL 2.3",
"zh-Hans": "HMCL 2.3 经典外观",
"zh-Hant": "HMCL 2.3 經典外觀",
"lzh": "HMCL 2.3 之舊觀"
},
"color" : "#E67E22",
"brightness" : "light",
"colorStyle" : "fidelity",
"background" : {
"type" : "builtin",
"id" : "2015-06-22"
"color": "#E67E22",
"brightness": "light",
"colorStyle": "fidelity",
"background": {
"type": "builtin",
"id": "2015-06-22"
},
"titleBar" : {
"transparent" : false
"titleBar": {
"transparent": false
}
}
]
@@ -4,17 +4,31 @@
"version": "1.0.0",
"name": {
"default": "Default",
"en-Qabs": "ʇꞁnɐɟǝᗡ",
"es": "Predeterminado",
"ja": "デフォルト",
"ru": "По умолчанию",
"uk": "За замовчуванням",
"zh-Hans": "默认主题",
"zh-Hant": "預設主題"
"zh-Hant": "預設主題",
"lzh": "預設主題"
},
"description": {
"default": "HMCL Default",
"es": "HMCL Predeterminado",
"ja": "HMCL デフォルト",
"ru": "HMCL По умолчанию",
"uk": "HMCL За замовчуванням",
"zh-Hans": "HMCL 默认主题",
"zh-Hant": "HMCL 預設主題"
"zh-Hant": "HMCL 預設主題",
"lzh": "HMCL 預設主題"
},
"authors": [
{
"name": "HMCL"
"name": {
"default": "HMCL",
"en-Qabs": "ꞀƆWH"
}
}
],
"theme": {}
@@ -17,17 +17,26 @@
*/
package org.jackhuang.hmcl.ui.image;
import org.jetbrains.annotations.NotNullByDefault;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
/// Verifies image-format detection from bounded input prefixes.
@NotNullByDefault
public final class ImageUtilsTest {
/// Reads the image-loader detection prefix of a test resource.
///
/// @param fileName resource file name
/// @return up to [ImageUtils#HEADER_BUFFER_SIZE] leading bytes
private static byte[] readHeaderBuffer(String fileName) {
try (var input = Files.newInputStream(Path.of("src/test/resources/image/" + fileName))) {
return input.readNBytes(ImageUtils.HEADER_BUFFER_SIZE);
@@ -37,6 +46,15 @@ public final class ImageUtilsTest {
}
}
/// Encodes an XML fixture without introducing non-ASCII bytes.
///
/// @param value fixture text
/// @return US-ASCII bytes
private static byte[] asciiBytes(String value) {
return value.getBytes(StandardCharsets.US_ASCII);
}
/// Verifies APNG detection distinguishes animated and static PNG files.
@Test
public void testIsApng() {
assertTrue(ImageUtils.isApng(readHeaderBuffer("16x16.apng")));
@@ -47,6 +65,7 @@ public final class ImageUtilsTest {
assertFalse(ImageUtils.isApng(readHeaderBuffer("16x16-animation-lossy.webp")));
}
/// Verifies WebP detection recognizes all supported WebP variants.
@Test
public void testIsWebP() {
assertFalse(ImageUtils.isWebP(readHeaderBuffer("16x16.apng")));
@@ -57,4 +76,66 @@ public final class ImageUtilsTest {
assertTrue(ImageUtils.isWebP(readHeaderBuffer("16x16-animation-lossy.webp")));
}
/// Verifies SVG detection accepts XML prologs and an SVG document element.
@Test
public void testIsSVG() {
assertTrue(ImageUtils.isSVG(asciiBytes("<svg/>")));
assertTrue(ImageUtils.isSVG(asciiBytes(" \t\r\n<svg viewBox='0 0 16 16'>")));
assertTrue(ImageUtils.isSVG(
"\ufeff<?xml version='1.0' encoding='UTF-8'?>\n<svg/>"
.getBytes(StandardCharsets.UTF_8)
));
assertTrue(ImageUtils.isSVG(asciiBytes(
"<?xml version='1.0'?>"
+ "<?xml-stylesheet href='<svg.css'?>"
+ "<!-- <svg/> -->"
+ "<svg xmlns='http://www.w3.org/2000/svg'>"
)));
assertTrue(ImageUtils.isSVG(asciiBytes(
"<!DOCTYPE svg ["
+ "<!ENTITY quoted ']> <svg>'>"
+ "<!-- ]> <svg -->"
+ "<!ELEMENT svg ANY>"
+ "]><svg/>"
)));
assertTrue(ImageUtils.isSVG(asciiBytes(
"<!--" + "x".repeat(ImageUtils.HEADER_BUFFER_SIZE - 13) + "--><svg/>"
)));
assertTrue(ImageUtils.isSVG(asciiBytes(
"<svg:svg xmlns:svg='http://www.w3.org/2000/svg'/>"
)));
assertTrue(ImageUtils.isSVG(asciiBytes(
"<image:svg xmlns:image='http://www.w3.org/2000/svg'/>"
)));
}
/// Verifies SVG detection rejects incomplete, embedded, and non-SVG roots.
@Test
public void testIsNotSVG() {
assertFalse(ImageUtils.isSVG(new byte[0]));
assertFalse(ImageUtils.isSVG(asciiBytes("<svg")));
assertFalse(ImageUtils.isSVG(asciiBytes("<svg-image/>")));
assertFalse(ImageUtils.isSVG(asciiBytes("<svg:circle/>")));
assertFalse(ImageUtils.isSVG(asciiBytes("<image:svg-icon/>")));
assertFalse(ImageUtils.isSVG(asciiBytes("<SVG/>")));
assertFalse(ImageUtils.isSVG(asciiBytes("plain text <svg/>")));
assertFalse(ImageUtils.isSVG(asciiBytes("<!-- <svg/> -->")));
assertFalse(ImageUtils.isSVG(asciiBytes(
"<?xml version='1.0'?><html><svg/></html>"
)));
assertFalse(ImageUtils.isSVG(asciiBytes(
"<!DOCTYPE svg [<!-- incomplete ]> <svg -->"
)));
assertFalse(ImageUtils.isSVG(asciiBytes(
"<!--" + "x".repeat(ImageUtils.HEADER_BUFFER_SIZE) + "--><svg/>"
)));
}
/// Verifies sniffing routes a prefixed SVG document to the SVG loader.
@Test
public void testGuessLoaderDetectsPrefixedSVG() {
assertSame(ImageUtils.SVG, ImageUtils.guessLoader(asciiBytes(
"<svg:svg xmlns:svg='http://www.w3.org/2000/svg'/>"
)));
}
}
@@ -38,7 +38,7 @@ import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class ModManager extends LocalAddonManager<LocalModFile> {
public static final List<String> MOD_EXTENSIONS = List.of("jar", "litemod");
public static final List<String> MOD_EXTENSIONS = List.of("jar", "zip", "litemod");
@FunctionalInterface
private interface ModMetadataReader {
@@ -58,6 +58,7 @@ public final class ModManager extends LocalAddonManager<LocalModFile> {
);
map.put("jar", zipReaders);
map.put("zip", zipReaders);
map.put("litemod", List.of(pair(LiteModMetadata::fromFile, ModLoaderType.LITE_LOADER)));
READERS = map;
@@ -32,17 +32,19 @@ import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.Nullable;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.Semaphore;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.Checksum;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
@@ -184,23 +186,65 @@ public final class CurseForgeRemoteAddonRepository implements RemoteAddonReposit
}
}
@Override
public Optional<RemoteAddon.Version> getRemoteVersionByLocalFile(Path file) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (InputStream stream = Files.newInputStream(file)) {
byte[] buf = new byte[1024];
int len;
while ((len = stream.read(buf, 0, buf.length)) != -1) {
/// Calculates the CurseForge fingerprint without retaining the filtered file in memory.
static long calculateFingerprint(Path file) throws IOException {
try (SeekableByteChannel channel = Files.newByteChannel(file, StandardOpenOption.READ)) {
long startPosition = channel.position();
byte[] bufferArray = new byte[1024 * 1024];
ByteBuffer buffer = ByteBuffer.wrap(bufferArray);
long filteredLength = 0;
while (channel.read(buffer) > 0) {
int len = buffer.position();
for (int i = 0; i < len; i++) {
byte b = buf[i];
byte b = bufferArray[i];
if (b != 0x9 && b != 0xa && b != 0xd && b != 0x20) {
baos.write(b);
filteredLength++;
}
}
buffer.clear();
}
}
long hash = Integer.toUnsignedLong(MurmurHash2.hash32(baos.toByteArray(), baos.size(), 1));
channel.position(startPosition);
Checksum hasher = MurmurHash2.hash32(filteredLength, 1);
while (channel.read(buffer) > 0) {
int len = buffer.position();
int pos = 0;
while (pos < len) {
byte b = bufferArray[pos];
if (b == 0x9 || b == 0xa || b == 0xd || b == 0x20) {
break;
}
pos++;
}
if (pos < len) {
int pos2 = pos + 1;
while (pos2 < len) {
byte b = bufferArray[pos2];
if (b != 0x9 && b != 0xa && b != 0xd && b != 0x20) {
bufferArray[pos++] = b;
}
pos2++;
}
}
hasher.update(bufferArray, 0, pos);
buffer.clear();
}
return hasher.getValue();
} catch (IllegalArgumentException | IllegalStateException e) {
throw new IOException(e);
}
}
/// Finds the remote CurseForge version matching a local file.
@Override
public Optional<RemoteAddon.Version> getRemoteVersionByLocalFile(Path file) throws IOException {
long hash = calculateFingerprint(file);
if (hash == 811513880) { // Workaround for https://github.com/HMCL-dev/HMCL/issues/4597
return Optional.empty();
}
@@ -17,7 +17,11 @@
*/
package org.jackhuang.hmcl.util;
import org.jetbrains.annotations.NotNullByDefault;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.zip.Checksum;
/**
* Implementation of the MurmurHash2 32-bit and 64-bit hash functions.
@@ -48,6 +52,7 @@ import java.nio.charset.StandardCharsets;
* Original MurmurHash2 c++ code</a>
* @since 1.13
*/
@NotNullByDefault
public final class MurmurHash2 {
// Constants for 32-bit variant
@@ -64,6 +69,177 @@ public final class MurmurHash2 {
private MurmurHash2() {
}
/// Creates a streaming MurmurHash2 32-bit checksum for exactly `length` bytes.
///
/// The length is incorporated into the initial hash state using its low 32 bits. Calls to
/// [Checksum#update(int)] and [Checksum#update(byte[], int, int)] may divide the input at
/// arbitrary byte boundaries. [Checksum#getValue()] returns the hash as an unsigned 32-bit
/// value represented by a `long`, and does not change the checksum state.
///
/// The returned checksum verifies the number of supplied bytes when `getValue()` is called.
/// If too few bytes have been supplied, the caller may continue updating the checksum and call
/// `getValue()` again. Once too many bytes have been supplied, [Checksum#reset()] must be called
/// before a value can be obtained. Resetting retains the configured length and seed. The
/// returned checksum is mutable and is not safe for concurrent use.
///
/// @param length the exact number of input bytes
/// @param seed the initial seed value
/// @return a new checksum initialized with the specified length and seed
/// @throws IllegalArgumentException if `length` is negative
public static Checksum hash32(final long length, final int seed) {
if (length < 0) {
throw new IllegalArgumentException("length must not be negative: " + length);
}
return new Hash32Checksum(length, seed);
}
/// Computes a MurmurHash2 32-bit value from updates whose total length is known in advance.
private static final class Hash32Checksum implements Checksum {
/// The exact number of bytes required before the hash can be obtained.
private final long expectedLength;
/// The hash state restored by [#reset()].
private final int initialHash;
/// The hash state after all complete four-byte blocks received so far.
private int hash;
/// The number of bytes counted before [#inputLengthExceeded] becomes `true`.
private long inputLength;
/// Whether more than [#expectedLength] bytes have been received.
private boolean inputLengthExceeded;
/// Up to three unprocessed bytes packed in little-endian order.
private int tail;
/// The number of bytes currently stored in [#tail].
private int tailLength;
/// Creates a checksum with a precomputed initial hash state.
///
/// @param expectedLength the exact number of input bytes
/// @param seed the initial seed value
private Hash32Checksum(long expectedLength, int seed) {
this.expectedLength = expectedLength;
this.initialHash = seed ^ (int) expectedLength;
this.hash = initialHash;
}
/// Incorporates the low eight bits of `value` into this checksum.
///
/// @param value the value whose low eight bits are incorporated
@Override
public void update(int value) {
addInputLength(1);
appendByte(value);
}
/// Incorporates `length` bytes beginning at `offset` into this checksum.
///
/// @param data the array containing the input bytes
/// @param offset the offset of the first input byte
/// @param length the number of bytes to incorporate
@Override
public void update(byte[] data, int offset, int length) {
Objects.checkFromIndexSize(offset, length, data.length);
addInputLength(length);
int index = offset;
final int end = offset + length;
while (tailLength != 0 && index < end) {
appendByte(data[index++]);
}
while (index <= end - Integer.BYTES) {
mixBlock(ByteArray.getIntLE(data, index));
index += Integer.BYTES;
}
while (index < end) {
appendByte(data[index++]);
}
}
/// Returns the MurmurHash2 value after verifying the exact input length.
///
/// @return the unsigned 32-bit hash value represented by a `long`
/// @throws IllegalStateException if the number of supplied bytes differs from the expected
/// length
@Override
public long getValue() {
if (inputLengthExceeded) {
throw new IllegalStateException(
"Expected " + expectedLength + " bytes, but received more than expected");
}
if (inputLength != expectedLength) {
throw new IllegalStateException(
"Expected " + expectedLength + " bytes, but received " + inputLength);
}
int result = hash;
if (tailLength != 0) {
result ^= tail;
result *= M32;
}
result ^= result >>> 13;
result *= M32;
result ^= result >>> 15;
return Integer.toUnsignedLong(result);
}
/// Restores this checksum to its initial state while retaining its expected length and seed.
@Override
public void reset() {
hash = initialHash;
inputLength = 0;
inputLengthExceeded = false;
tail = 0;
tailLength = 0;
}
/// Records that `length` more input bytes have been supplied.
///
/// @param length the non-negative number of additional bytes
private void addInputLength(int length) {
if (inputLengthExceeded) {
return;
}
if (length > expectedLength - inputLength) {
inputLengthExceeded = true;
} else {
inputLength += length;
}
}
/// Buffers one byte and mixes the resulting block when four bytes are available.
///
/// @param value the value whose low eight bits are appended
private void appendByte(int value) {
tail |= (value & 0xff) << (tailLength * Byte.SIZE);
tailLength++;
if (tailLength == Integer.BYTES) {
mixBlock(tail);
tail = 0;
tailLength = 0;
}
}
/// Mixes one little-endian four-byte block into the current hash state.
///
/// @param block the block to mix
private void mixBlock(int block) {
int mixedBlock = block;
mixedBlock *= M32;
mixedBlock ^= mixedBlock >>> R32;
mixedBlock *= M32;
hash *= M32;
hash ^= mixedBlock;
}
}
/**
* Generates a 32-bit hash from byte array with the given length and seed.
*
@@ -137,14 +137,14 @@ public final class CompressingUtils {
}
public static ZipArchiveReader openZipFile(Path zipFile, Charset charset) throws IOException {
return new ZipArchiveReader(zipFile, charset);
return new ZipArchiveReader(zipFile, charset, true, true);
}
public static ZipArchiveReader openZipFileWithPossibleEncoding(Path zipFile, Charset possibleEncoding) throws IOException {
if (possibleEncoding == null)
possibleEncoding = StandardCharsets.UTF_8;
ZipArchiveReader zipReader = new ZipArchiveReader(Files.newByteChannel(zipFile));
ZipArchiveReader zipReader = new ZipArchiveReader(zipFile, StandardCharsets.UTF_8, true, true);
Charset suitableEncoding;
try {
@@ -161,7 +161,7 @@ public final class CompressingUtils {
}
zipReader.close();
return new ZipArchiveReader(Files.newByteChannel(zipFile), suitableEncoding);
return new ZipArchiveReader(zipFile, suitableEncoding, true, true);
}
public static final class Builder {
@@ -18,7 +18,7 @@
package org.jackhuang.hmcl.util.tree;
import kala.compress.archivers.ArchiveEntry;
import kala.compress.archivers.zip.ZipArchiveReader;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.UnmodifiableView;
@@ -44,7 +44,7 @@ public abstract class ArchiveFileTree<R, E extends ArchiveEntry> implements Clos
String name = namePath.toString();
if (name.endsWith(".jar") || name.endsWith(".zip")) {
return new ZipFileTree(new ZipArchiveReader(file));
return CompressingUtils.openZipTree(file);
} else if (name.endsWith(".tar") || name.endsWith(".tar.gz") || name.endsWith(".tgz")) {
return TarFileTree.open(file);
} else {
@@ -933,4 +933,21 @@
26.2-pre-6
26.2-rc-1
26.2-rc-2
26.2
26.2
26.3-snapshot-1
26.3-snapshot-2
26.3-snapshot-3
26.3-snapshot-4
26.3-snapshot-5
26.3-snapshot-6
26.3-snapshot-7
26.3-snapshot-8
26.3-snapshot-9
26.3-snapshot-10
26.3-pre-1
26.3-pre-2
26.3-pre-3
26.3-rc-1
26.3-rc-2
26.3-rc-3
26.3
@@ -1,54 +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.addon.curse;
import org.jackhuang.hmcl.util.MurmurHash2;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import static org.junit.jupiter.api.Assertions.*;
public class CurseForgeRemoteAddonRepositoryTest {
@Test
@Disabled
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));
assertEquals(hash, 3333498611L);
}
}
@@ -0,0 +1,73 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 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.addon.repository;
import org.jackhuang.hmcl.util.MurmurHash2;
import org.jetbrains.annotations.NotNullByDefault;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Random;
import static org.junit.jupiter.api.Assertions.assertEquals;
/// Tests CurseForge fingerprint calculation for local files.
@NotNullByDefault
public final class CurseForgeRemoteAddonRepositoryTest {
/// Verifies that streaming calculation remains identical to the previous in-memory algorithm.
@Test
public void calculatesFingerprintWithoutChangingTheResult(@TempDir Path tempDir) throws IOException {
byte[] boundarySample = new byte[8192 + 17];
new Random(0).nextBytes(boundarySample);
for (int i = 0; i < boundarySample.length; i += 97) {
boundarySample[i] = 0x20;
}
byte[][] samples = {
{},
{0x9, 0xa, 0xd, 0x20},
{1},
{1, 2},
{1, 2, 3},
{1, 2, 3, 4},
{1, 0x20, 2, 0xa, 3, 0xd, 4, 0x9, 5},
boundarySample
};
for (int i = 0; i < samples.length; i++) {
byte[] sample = samples[i];
Path file = tempDir.resolve("sample-" + i);
Files.write(file, sample);
ByteArrayOutputStream filtered = new ByteArrayOutputStream();
for (byte b : sample) {
if (b != 0x9 && b != 0xa && b != 0xd && b != 0x20) {
filtered.write(b);
}
}
long expected = Integer.toUnsignedLong(MurmurHash2.hash32(filtered.toByteArray(), filtered.size(), 1));
assertEquals(expected, CurseForgeRemoteAddonRepository.calculateFingerprint(file));
}
}
}
@@ -0,0 +1,272 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 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 org.jetbrains.annotations.NotNullByDefault;
import org.junit.jupiter.api.Test;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Random;
import java.util.zip.Checksum;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
/// Tests the streaming MurmurHash2 checksum implementation.
@NotNullByDefault
public final class MurmurHash2Test {
/// Compares whole-array, bulk, byte-wise, and fragmented updates across lengths and seeds.
@Test
public void testStreamingHash32AcrossLengthsAndSeeds() {
byte[] data = new byte[513];
new Random(0x6d75726d75724cL).nextBytes(data);
for (int length = 0; length <= data.length; length++) {
assertStreamingHash32(data, length, 0);
assertStreamingHash32(data, length, -1);
assertStreamingHash32(data, length, 0x9747b28c);
assertStreamingHash32(data, length, 0x9747b28c ^ length * 31);
}
}
/// Compares every two-part split while reading from a non-zero source-array offset.
@Test
public void testStreamingHash32AtEverySplitPoint() {
byte[] data = new byte[73];
new Random(0x73706c697473L).nextBytes(data);
int sourceOffset = 5;
byte[] paddedData = new byte[sourceOffset + data.length + 7];
System.arraycopy(data, 0, paddedData, sourceOffset, data.length);
for (int length = 0; length <= data.length; length++) {
int seed = Integer.rotateLeft(0x4d757232, length & 31);
long expected = hash32(data, length, seed);
for (int split = 0; split <= length; split++) {
Checksum checksum = MurmurHash2.hash32(length, seed);
checksum.update(paddedData, sourceOffset, split);
checksum.update(paddedData, sourceOffset + split, length - split);
assertEquals(expected, checksum.getValue(),
"length=" + length + ", split=" + split);
}
}
}
/// Compares interleaved single-byte and bulk updates after every possible short prefix.
@Test
public void testStreamingHash32WithMixedUpdates() {
byte[] data = new byte[129];
new Random(0x6d69786564L).nextBytes(data);
for (int length = 0; length <= data.length; length++) {
int seed = 0x13579bdf ^ length;
long expected = hash32(data, length, seed);
for (int prefixLength = 0; prefixLength <= Math.min(length, 7); prefixLength++) {
Checksum checksum = MurmurHash2.hash32(length, seed);
int offset = 0;
while (offset < prefixLength) {
checksum.update(data[offset++]);
}
// Exercise an empty bulk update both with and without a buffered tail.
checksum.update(data, offset, 0);
while (offset < length) {
int chunkLength = Math.min(offset % 5 + 1, length - offset);
checksum.update(data, offset, chunkLength);
offset += chunkLength;
if (offset < length) {
checksum.update(data[offset++]);
}
}
assertEquals(expected, checksum.getValue(),
"length=" + length + ", prefixLength=" + prefixLength);
}
}
}
/// Compares array-backed, read-only, direct, and empty-buffer updates.
@Test
public void testStreamingHash32WithByteBuffers() {
byte[] data = new byte[8207];
new Random(0x627566666572L).nextBytes(data);
int seed = 0x2468ace0;
long expected = hash32(data, data.length, seed);
int sourceOffset = 3;
byte[] paddedData = new byte[sourceOffset + data.length + 4];
System.arraycopy(data, 0, paddedData, sourceOffset, data.length);
ByteBuffer arrayBackedBuffer = ByteBuffer.wrap(paddedData);
arrayBackedBuffer.position(sourceOffset);
arrayBackedBuffer.limit(sourceOffset + data.length);
assertTrue(arrayBackedBuffer.hasArray());
Checksum arrayBackedChecksum = MurmurHash2.hash32(data.length, seed);
arrayBackedChecksum.update(arrayBackedBuffer);
assertEquals(arrayBackedBuffer.limit(), arrayBackedBuffer.position());
assertEquals(expected, arrayBackedChecksum.getValue());
ByteBuffer readOnlyBuffer = ByteBuffer.wrap(data).asReadOnlyBuffer();
assertFalse(readOnlyBuffer.hasArray());
Checksum readOnlyChecksum = MurmurHash2.hash32(data.length, seed);
readOnlyChecksum.update(readOnlyBuffer);
assertEquals(readOnlyBuffer.limit(), readOnlyBuffer.position());
assertEquals(expected, readOnlyChecksum.getValue());
ByteBuffer directBuffer = ByteBuffer.allocateDirect(sourceOffset + data.length);
directBuffer.position(sourceOffset);
directBuffer.put(data);
directBuffer.position(sourceOffset);
assertFalse(directBuffer.hasArray());
Checksum directChecksum = MurmurHash2.hash32(data.length, seed);
directChecksum.update(directBuffer);
assertEquals(directBuffer.limit(), directBuffer.position());
assertEquals(expected, directChecksum.getValue());
ByteBuffer emptyBuffer = ByteBuffer.allocate(4);
emptyBuffer.position(2);
emptyBuffer.limit(2);
Checksum emptyFirstChecksum = MurmurHash2.hash32(data.length, seed);
emptyFirstChecksum.update(emptyBuffer);
emptyFirstChecksum.update(data);
assertEquals(2, emptyBuffer.position());
assertEquals(expected, emptyFirstChecksum.getValue());
}
/// Verifies that single-byte updates use only the low eight bits of each value.
@Test
public void testStreamingHash32SingleByteValues() {
byte[] data = {0x12, (byte) 0xff, (byte) 0x80, 0x00};
int seed = 0x12345678;
Checksum checksum = MurmurHash2.hash32(data.length, seed);
checksum.update(0x112);
checksum.update(-1);
checksum.update(0xabcdef80);
checksum.update(0x100);
assertEquals(hash32(data, data.length, seed), checksum.getValue());
}
/// Verifies that rejected array ranges leave the checksum state unchanged.
@Test
public void testStreamingHash32ArrayRangeValidation() {
byte[] data = {0x12, 0x34, 0x56};
int seed = 0x12345678;
Checksum checksum = MurmurHash2.hash32(data.length, seed);
assertThrows(NullPointerException.class, () -> checksum.update(null, 0, 0));
assertThrows(IndexOutOfBoundsException.class, () -> checksum.update(data, -1, 1));
assertThrows(IndexOutOfBoundsException.class, () -> checksum.update(data, 0, -1));
assertThrows(IndexOutOfBoundsException.class, () -> checksum.update(data, data.length, 1));
checksum.update(data);
assertEquals(hash32(data, data.length, seed), checksum.getValue());
}
/// Verifies that `getValue()` rejects incomplete and excessive input without finalizing state.
@Test
public void testExactLengthValidation() {
byte[] data = {0x12, 0x34, 0x56, 0x78, (byte) 0x9a};
int seed = 0x12345678;
long expected = hash32(data, data.length, seed);
Checksum checksum = MurmurHash2.hash32(data.length, seed);
checksum.update(data, 0, data.length - 1);
assertThrows(IllegalStateException.class, checksum::getValue);
checksum.update(data[data.length - 1]);
assertEquals(expected, checksum.getValue());
checksum.update(0xff);
checksum.update(0xfe);
assertThrows(IllegalStateException.class, checksum::getValue);
checksum.reset();
checksum.update(data, 0, data.length);
assertEquals(expected, checksum.getValue());
}
/// Verifies the valid range of the declared input length.
@Test
public void testLengthRange() {
assertThrows(IllegalArgumentException.class, () -> MurmurHash2.hash32(-1, 0));
Checksum checksum = MurmurHash2.hash32(1L << 32, 0);
assertThrows(IllegalStateException.class, checksum::getValue);
}
/// Compares the principal update styles with the byte-array implementation.
///
/// @param data the input array
/// @param length the number of bytes to hash
/// @param seed the initial seed value
private static void assertStreamingHash32(byte[] data, int length, int seed) {
long expected = hash32(data, length, seed);
byte[] exactData = Arrays.copyOf(data, length);
Checksum wholeArray = MurmurHash2.hash32(length, seed);
wholeArray.update(exactData);
assertEquals(expected, wholeArray.getValue());
Checksum bulk = MurmurHash2.hash32(length, seed);
bulk.update(data, 0, length);
assertEquals(expected, bulk.getValue());
assertEquals(expected, bulk.getValue());
Checksum byteWise = MurmurHash2.hash32(length, seed);
for (int i = 0; i < length; i++) {
byteWise.update(data[i]);
}
assertEquals(expected, byteWise.getValue());
Checksum fragmented = MurmurHash2.hash32(length, seed);
int offset = 0;
while (offset < length) {
int chunkLength = Math.min(offset % 7 + 1, length - offset);
fragmented.update(data, offset, chunkLength);
offset += chunkLength;
}
assertEquals(expected, fragmented.getValue());
}
/// Returns the unsigned value produced by the byte-array implementation.
///
/// @param data the input array
/// @param length the number of bytes to hash
/// @param seed the initial seed value
/// @return the unsigned 32-bit hash value represented by a `long`
private static long hash32(byte[] data, int length, int seed) {
return Integer.toUnsignedLong(MurmurHash2.hash32(data, length, seed));
}
/// Prevents instantiation.
private MurmurHash2Test() {
}
}
+1 -1
View File
@@ -4,7 +4,7 @@ import org.jackhuang.hmcl.gradle.l10n.ParseLanguageSubtagRegistry
plugins {
id("checkstyle")
id("org.glavo.gradle-wrapper-neo") version "0.1.0"
id("org.glavo.gradle-wrapper-neo") version "0.2.0"
}
group = "org.jackhuang"
+1
View File
@@ -12,6 +12,7 @@ dependencies {
implementation(libs.jna)
implementation(libs.kala.compress.tar)
implementation(libs.kala.compress.ar)
implementation(libs.weburl)
}
java {
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.gradle.mod;
import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.reflect.TypeToken;
import org.glavo.url.WebURL;
import org.gradle.api.DefaultTask;
import org.gradle.api.GradleException;
import org.gradle.api.file.RegularFileProperty;
@@ -33,7 +34,6 @@ import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.lang.reflect.Type;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
@@ -58,7 +58,7 @@ public abstract class ParseModDataTask extends DefaultTask {
private static final String MOD_SEPARATOR = ",";
private static final Pattern[] CURSEFORGE_PATTERNS = {
Pattern.compile("^/(minecraft|Minecraft|minecraft-bedrock)/(mc-mods|data-packs|modpacks|customization|mc-addons|texture-packs|customization/configuration|addons|scripts)/+(?<modid>[\\w-]+)(/(.*?))?$"),
Pattern.compile("^/(minecraft|Minecraft|minecraft-bedrock)/(mc-mods|data-packs|modpacks|customization|mc-addons|texture-packs|customization/configuration|addons|scripts|bukkit-plugins)/+(?<modid>[\\w-]+)(/(.*?))?$"),
Pattern.compile("^/projects/(?<modid>[\\w-]+)(/(.*?))?$"),
Pattern.compile("^/mc-mods/minecraft/(?<modid>[\\w-]+)(/(.*?))?$"),
Pattern.compile("^/legacy/mc-mods/minecraft/(\\d+)-(?<modid>[\\w-]+)"),
@@ -70,27 +70,6 @@ public abstract class ParseModDataTask extends DefaultTask {
.replace("&gt;", ">");
}
private static String parseCurseforge(String url) {
URI res = URI.create(url.replace(" ", "%20"));
if (!"http".equals(res.getScheme()) && !"https".equals(res.getScheme())) {
return "";
}
if ("edge.forgecdn.net".equals(res.getHost())) {
return "";
}
for (Pattern pattern : CURSEFORGE_PATTERNS) {
Matcher matcher = pattern.matcher(res.getPath());
if (matcher.matches()) {
return matcher.group("modid");
}
}
return "";
}
private static final Pattern MCMOD_PATTERN =
Pattern.compile("^https://www\\.mcmod\\.cn/(class|modpack)/(?<modid>\\d+)\\.html$");
@@ -169,7 +148,8 @@ public abstract class ParseModDataTask extends DefaultTask {
}
if (chineseName.contains(S) || subName.contains(S)) {
throw new GradleException("Error: " + chineseName);
LOGGER.warn("Error chinese name: {}", chineseName);
continue;
}
String curseforgeId = "";
@@ -180,13 +160,40 @@ public abstract class ParseModDataTask extends DefaultTask {
List<ModData.Link> mcmodLinks = links.get("mcmod");
if (curseforgeLinks != null && !curseforgeLinks.isEmpty()) {
boolean reportError = true;
for (ModData.Link link : curseforgeLinks) {
curseforgeId = parseCurseforge(link.url);
WebURL res = WebURL.parse(link.url);
if (!"http".equals(res.getScheme()) && !"https".equals(res.getScheme())) {
curseforgeId = "";
break;
}
if (Set.of(
"files.xmdhs.com",
"edge.forgecdn.net",
"minecraft.curseforge.com",
"mediafilez.forgecdn.net"
).contains(res.getHost())) {
reportError = false;
curseforgeId = "";
break;
}
for (Pattern pattern : CURSEFORGE_PATTERNS) {
Matcher matcher = pattern.matcher(res.getPath());
if (matcher.matches()) {
curseforgeId = matcher.group("modid");
break;
}
}
if (!curseforgeId.isEmpty()) {
break;
}
}
if (curseforgeId.isEmpty()) {
if (curseforgeId.isEmpty() && reportError) {
LOGGER.warn("Error curseforge: {}", chineseName);
}
}
+2 -2
View File
@@ -1,7 +1,7 @@
[versions]
hmclauncher = "3.7.0.1"
jetbrains-annotations = "26.1.0"
kala-compress = "1.27.1-3"
kala-compress = "1.27.1-5"
simple-png-javafx = "0.3.0"
gson = "2.13.2"
tomlj = "1.1.1"
@@ -13,7 +13,7 @@ hello-nbt = "0.3.0"
nanohttpd = "2.3.1"
jsoup = "1.21.2"
chardet = "2.5.0"
fxsvgimage = "1.3"
fxsvgimage = "1.9.1"
jna = "5.18.1"
pci-ids = "0.4.0"
java-info = "1.0"
+1 -1
View File
@@ -1,7 +1,7 @@
/*
* Gradle Wrapper Neo single-file source distribution.
*
* This source file is part of Gradle Wrapper Neo 0.1.0.
* This source file is part of Gradle Wrapper Neo 0.2.0.
* Documentation and updates: https://github.com/Glavo/gradle-wrapper-neo
*
* Place this file at gradle/wrapper/GradleWrapperNeo.java in a Gradle project.
+19 -10
View File
@@ -157,19 +157,17 @@ function Resolve-JavacExecutable {
}
function Find-AppHome {
param([string] $LauncherHome)
param(
[string] $LauncherHome,
[string] $WorkingDirectory
)
$launcherProperties = Join-Path $LauncherHome 'gradle\wrapper\gradle-wrapper.properties'
if (Test-Path -LiteralPath $launcherProperties -PathType Leaf) {
return $LauncherHome
}
$currentLocation = Get-Location
if ($currentLocation.Provider.Name -ne 'FileSystem') {
throw "The current location is not a file system directory: $currentLocation"
}
$searchDirectory = Get-Item -LiteralPath $currentLocation.Path
$searchDirectory = Get-Item -LiteralPath $WorkingDirectory
$searchStart = $searchDirectory.FullName
while ($null -ne $searchDirectory) {
$propertiesFile = Join-Path $searchDirectory.FullName 'gradle\wrapper\gradle-wrapper.properties'
@@ -224,7 +222,8 @@ function ConvertTo-WindowsCommandLineArgument {
function Invoke-NativeApplication {
param(
[string] $Executable,
[string[]] $Arguments
[string[]] $Arguments,
[string] $WorkingDirectory
)
$quotedArguments = New-Object 'System.Collections.Generic.List[string]'
@@ -235,6 +234,7 @@ function Invoke-NativeApplication {
$startInfo = New-Object Diagnostics.ProcessStartInfo
$startInfo.FileName = $Executable
$startInfo.Arguments = [string]::Join(' ', $quotedArguments.ToArray())
$startInfo.WorkingDirectory = $WorkingDirectory
$startInfo.UseShellExecute = $false
$process = New-Object Diagnostics.Process
@@ -295,7 +295,13 @@ function Compile-WrapperSource {
function Invoke-GradleWrapper {
param([string[]] $GradleArguments)
$appHome = Find-AppHome -LauncherHome $script:LauncherHome
$currentLocation = Get-Location
if ($currentLocation.Provider.Name -ne 'FileSystem') {
throw "The current location is not a file system directory: $currentLocation"
}
$workingDirectory = $currentLocation.ProviderPath
$appHome = Find-AppHome -LauncherHome $script:LauncherHome -WorkingDirectory $workingDirectory
$launcherWrapperDirectory = Join-Path $script:LauncherHome 'gradle\wrapper'
$projectSource = Join-Path $appHome 'gradle\wrapper\GradleWrapperNeo.java'
if (Test-Path -LiteralPath $projectSource -PathType Leaf) {
@@ -351,7 +357,10 @@ function Invoke-GradleWrapper {
}
$javaArguments += $GradleArguments
$script:WrapperExitCode = Invoke-NativeApplication -Executable $javaExecutable -Arguments $javaArguments
$script:WrapperExitCode = Invoke-NativeApplication `
-Executable $javaExecutable `
-Arguments $javaArguments `
-WorkingDirectory $workingDirectory
}
try {