Compare commits
18
Commits
main
...
release/3.16
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fbcc17472f | ||
|
|
7b85f0dfce | ||
|
|
b8a830dd40 | ||
|
|
ce368caa67 | ||
|
|
850ad82aa1 | ||
|
|
552276e673 | ||
|
|
e79855685e | ||
|
|
ad5afe7e06 | ||
|
|
8210c77de9 | ||
|
|
b11b298312 | ||
|
|
5c605c58d2 | ||
|
|
01d4100a2f | ||
|
|
d07eb2f748 | ||
|
|
345bb176fd | ||
|
|
83a38fadf0 | ||
|
|
9980dc8aea | ||
|
|
7dfbf76b69 | ||
|
|
7f2f77f54f |
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
@@ -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;
|
||||
|
||||
+57
-13
@@ -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
|
||||
-54
@@ -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);
|
||||
}
|
||||
}
|
||||
+73
@@ -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
@@ -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"
|
||||
|
||||
@@ -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(">", ">");
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Vendored
+1
-1
@@ -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
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user