Compare commits
46
Commits
v3.7.0.300
...
v3.7.0.302
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df6f35bc8e | ||
|
|
58d3780553 | ||
|
|
58b357d4b8 | ||
|
|
d69d27af57 | ||
|
|
f9a39f1c90 | ||
|
|
f8a4261480 | ||
|
|
2c428faa95 | ||
|
|
da91fe15df | ||
|
|
1df962884c | ||
|
|
8ad5d83946 | ||
|
|
74878bda0c | ||
|
|
b32d3304b0 | ||
|
|
60375c9632 | ||
|
|
33877691f8 | ||
|
|
109d6874ab | ||
|
|
a225e7fac4 | ||
|
|
7dad43570e | ||
|
|
8a166266d8 | ||
|
|
c53fbd0efb | ||
|
|
1e74567a22 | ||
|
|
0b9616f460 | ||
|
|
0b89612ec5 | ||
|
|
642893d2e9 | ||
|
|
2e878e43cb | ||
|
|
c8edf4be62 | ||
|
|
96f16382c0 | ||
|
|
11c45e0ab8 | ||
|
|
08f6f973c0 | ||
|
|
35e63fc8c8 | ||
|
|
23037b8afc | ||
|
|
25323dc385 | ||
|
|
496b403eeb | ||
|
|
7be00f2408 | ||
|
|
08d7974e27 | ||
|
|
f16cbed5b4 | ||
|
|
0988dc53cd | ||
|
|
f1c8791961 | ||
|
|
07e5ac9e0e | ||
|
|
c98dbe6694 | ||
|
|
460134cc1e | ||
|
|
9dd117c454 | ||
|
|
c60af8675f | ||
|
|
411efbe774 | ||
|
|
2e09a40b69 | ||
|
|
ba47c2d10d | ||
|
|
fd90ca8afe |
No files matched your search
@@ -16,6 +16,8 @@ NVIDIA
|
||||
minecraft-exported-crash-info*
|
||||
hmcl-exported-logs-*
|
||||
/.java/
|
||||
/.local/
|
||||
/.cache/
|
||||
|
||||
# gradle build
|
||||
/build/
|
||||
|
||||
@@ -191,7 +191,7 @@ public final class LauncherHelper {
|
||||
LaunchOptions launchOptions = repository.getLaunchOptions(
|
||||
selectedVersion, javaVersionRef.get(), profile.getGameDir(), javaAgents, javaArguments, scriptFile != null);
|
||||
|
||||
LOG.info("Here's the structure of game mod directory:\n" + FileUtils.printFileStructure(repository.getModManager(selectedVersion).getModsDirectory(), 10));
|
||||
LOG.info("Here's the structure of game mod directory:\n" + FileUtils.printFileStructure(repository.getModsDirectory(selectedVersion), 10));
|
||||
|
||||
return new HMCLGameLauncher(
|
||||
repository,
|
||||
|
||||
@@ -48,6 +48,8 @@ public final class GlobalConfig implements Observable {
|
||||
|
||||
private final IntegerProperty agreementVersion = new SimpleIntegerProperty();
|
||||
|
||||
private final IntegerProperty terracottaAgreementVersion = new SimpleIntegerProperty();
|
||||
|
||||
private final IntegerProperty platformPromptVersion = new SimpleIntegerProperty();
|
||||
|
||||
private final IntegerProperty logRetention = new SimpleIntegerProperty();
|
||||
@@ -94,6 +96,18 @@ public final class GlobalConfig implements Observable {
|
||||
this.agreementVersion.set(agreementVersion);
|
||||
}
|
||||
|
||||
public int getTerracottaAgreementVersion() {
|
||||
return terracottaAgreementVersion.get();
|
||||
}
|
||||
|
||||
public IntegerProperty terracottaAgreementVersionProperty() {
|
||||
return terracottaAgreementVersion;
|
||||
}
|
||||
|
||||
public void setTerracottaAgreementVersion(int terracottaAgreementVersion) {
|
||||
this.terracottaAgreementVersion.set(terracottaAgreementVersion);
|
||||
}
|
||||
|
||||
public int getPlatformPromptVersion() {
|
||||
return platformPromptVersion.get();
|
||||
}
|
||||
@@ -153,6 +167,7 @@ public final class GlobalConfig implements Observable {
|
||||
public static final class Serializer implements JsonSerializer<GlobalConfig>, JsonDeserializer<GlobalConfig> {
|
||||
private static final Set<String> knownFields = new HashSet<>(Arrays.asList(
|
||||
"agreementVersion",
|
||||
"terracottaAgreementVersion",
|
||||
"platformPromptVersion",
|
||||
"logRetention",
|
||||
"userJava",
|
||||
@@ -169,6 +184,7 @@ public final class GlobalConfig implements Observable {
|
||||
|
||||
JsonObject jsonObject = new JsonObject();
|
||||
jsonObject.add("agreementVersion", context.serialize(src.getAgreementVersion()));
|
||||
jsonObject.add("terracottaAgreementVersion", context.serialize(src.getTerracottaAgreementVersion()));
|
||||
jsonObject.add("platformPromptVersion", context.serialize(src.getPlatformPromptVersion()));
|
||||
jsonObject.add("logRetention", context.serialize(src.getLogRetention()));
|
||||
jsonObject.add("fontAntiAliasing", context.serialize(src.getFontAntiAliasing()));
|
||||
@@ -196,6 +212,7 @@ public final class GlobalConfig implements Observable {
|
||||
|
||||
GlobalConfig config = new GlobalConfig();
|
||||
config.setAgreementVersion(Optional.ofNullable(obj.get("agreementVersion")).map(JsonElement::getAsInt).orElse(0));
|
||||
config.setTerracottaAgreementVersion(Optional.ofNullable(obj.get("terracottaAgreementVersion")).map(JsonElement::getAsInt).orElse(0));
|
||||
config.setPlatformPromptVersion(Optional.ofNullable(obj.get("platformPromptVersion")).map(JsonElement::getAsInt).orElse(0));
|
||||
config.setLogRetention(Optional.ofNullable(obj.get("logRetention")).map(JsonElement::getAsInt).orElse(20));
|
||||
config.setEnableOfflineAccount(Optional.ofNullable(obj.get("enableOfflineAccount")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import javafx.scene.image.Image;
|
||||
import org.jackhuang.hmcl.mod.ModLoaderType;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
public enum VersionIconType {
|
||||
@@ -39,6 +40,18 @@ public enum VersionIconType {
|
||||
|
||||
// Please append new items at last
|
||||
|
||||
public static VersionIconType getIconType(ModLoaderType modLoaderType) {
|
||||
return switch (modLoaderType) {
|
||||
case FORGE -> VersionIconType.FORGE;
|
||||
case NEO_FORGED -> VersionIconType.NEO_FORGE;
|
||||
case FABRIC -> VersionIconType.FABRIC;
|
||||
case QUILT -> VersionIconType.QUILT;
|
||||
case LITE_LOADER -> VersionIconType.CHICKEN;
|
||||
case CLEANROOM -> VersionIconType.CLEANROOM;
|
||||
default -> VersionIconType.COMMAND;
|
||||
};
|
||||
}
|
||||
|
||||
private final String resourceUrl;
|
||||
|
||||
VersionIconType(String resourceUrl) {
|
||||
@@ -48,8 +61,4 @@ public enum VersionIconType {
|
||||
public Image getIcon() {
|
||||
return FXUtils.newBuiltinImage(resourceUrl);
|
||||
}
|
||||
|
||||
public Image getIcon(int size) {
|
||||
return FXUtils.newBuiltinImage(resourceUrl, size, size, true, true);
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import org.jackhuang.hmcl.terracotta.provider.MacOSProvider;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.LocalizedText;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.OSVersion;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
@@ -86,7 +87,11 @@ public final class TerracottaMetadata {
|
||||
|
||||
public static final ITerracottaProvider PROVIDER;
|
||||
public static final String PACKAGE_NAME;
|
||||
private static final List<Link> PACKAGE_LINKS;
|
||||
public static final List<Link> PACKAGE_LINKS;
|
||||
public static final String FEEDBACK_LINK = NetworkUtils.withQuery("https://docs.hmcl.net/multiplayer/feedback.html", Map.of(
|
||||
"v", "v1",
|
||||
"launcher_version", Metadata.VERSION
|
||||
));
|
||||
|
||||
private static final Pattern LEGACY;
|
||||
private static final List<String> RECENT;
|
||||
@@ -131,10 +136,6 @@ public final class TerracottaMetadata {
|
||||
}
|
||||
}
|
||||
|
||||
public static List<Link> getPackageLinks() {
|
||||
return PACKAGE_LINKS;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static ProviderContext locateProvider(Config config) {
|
||||
String architecture = switch (Architecture.SYSTEM_ARCH) {
|
||||
|
||||
@@ -66,7 +66,7 @@ public final class MacOSProvider implements ITerracottaProvider {
|
||||
"osascript",
|
||||
"-e",
|
||||
String.format(
|
||||
"do shell script \"installer -pkg %s -target /Applications\" with prompt \"%s\" with administrator privileges",
|
||||
"do shell script \"installer -pkg %s -target /\" with prompt \"%s\" with administrator privileges",
|
||||
installer.getPath(),
|
||||
i18n("terracotta.sudo_installing")
|
||||
)
|
||||
|
||||
@@ -36,6 +36,7 @@ import javafx.geometry.Pos;
|
||||
import javafx.geometry.Rectangle2D;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
@@ -1387,6 +1388,16 @@ public final class FXUtils {
|
||||
});
|
||||
}
|
||||
|
||||
public static void clearFocus(Node node) {
|
||||
Scene scene = node.getScene();
|
||||
if (scene != null) {
|
||||
Parent root = scene.getRoot();
|
||||
if (root != null) {
|
||||
root.requestFocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void copyOnDoubleClick(Labeled label) {
|
||||
label.addEventHandler(MouseEvent.MOUSE_CLICKED, e -> {
|
||||
if (e.getButton() == MouseButton.PRIMARY && e.getClickCount() == 2) {
|
||||
|
||||
@@ -41,9 +41,12 @@ import org.jackhuang.hmcl.setting.StyleSheets;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.logging.Logger;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Log4jLevel;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.logging.Logger;
|
||||
import org.jackhuang.hmcl.util.platform.*;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -59,9 +62,9 @@ import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public class GameCrashWindow extends Stage {
|
||||
private final Version version;
|
||||
@@ -283,7 +286,7 @@ public class GameCrashWindow extends Stage {
|
||||
alert.showAndWait();
|
||||
|
||||
return null;
|
||||
});
|
||||
}, Schedulers.javafx());
|
||||
}
|
||||
|
||||
private final class View extends VBox {
|
||||
|
||||
@@ -100,7 +100,10 @@ public abstract class ToolbarListPageSkin<T extends ListPageBase<? extends Node>
|
||||
ret.getStyleClass().add("jfx-tool-bar-button");
|
||||
ret.setGraphic(wrap(svg.createIcon(Theme.blackFill(), -1)));
|
||||
ret.setText(text);
|
||||
ret.setOnAction(e -> onClick.run());
|
||||
ret.setOnAction(e -> {
|
||||
onClick.run();
|
||||
FXUtils.clearFocus(ret);
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,9 +33,8 @@ import org.jackhuang.hmcl.util.AggregatedObservableList;
|
||||
public class TwoLineListItem extends VBox {
|
||||
private static final String DEFAULT_STYLE_CLASS = "two-line-list-item";
|
||||
|
||||
public static Label createTagLabel(String tag) {
|
||||
private static Label createTagLabel(String tag) {
|
||||
Label tagLabel = new Label();
|
||||
tagLabel.getStyleClass().add("tag");
|
||||
tagLabel.setText(tag);
|
||||
HBox.setMargin(tagLabel, new Insets(0, 8, 0, 0));
|
||||
return tagLabel;
|
||||
@@ -111,7 +110,15 @@ public class TwoLineListItem extends VBox {
|
||||
}
|
||||
|
||||
public void addTag(String tag) {
|
||||
getTags().add(createTagLabel(tag));
|
||||
Label tagLabel = createTagLabel(tag);
|
||||
tagLabel.getStyleClass().add("tag");
|
||||
getTags().add(tagLabel);
|
||||
}
|
||||
|
||||
public void addTagWarning(String tag) {
|
||||
Label tagLabel = createTagLabel(tag);
|
||||
tagLabel.getStyleClass().add("tag-warning");
|
||||
getTags().add(tagLabel);
|
||||
}
|
||||
|
||||
public ObservableList<Label> getTags() {
|
||||
|
||||
@@ -19,7 +19,8 @@ package org.jackhuang.hmcl.ui.download;
|
||||
|
||||
import com.jfoenix.controls.*;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
@@ -49,14 +50,20 @@ import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.ui.wizard.Navigation;
|
||||
import org.jackhuang.hmcl.ui.wizard.Refreshable;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardPage;
|
||||
import org.jackhuang.hmcl.util.Holder;
|
||||
import org.jackhuang.hmcl.util.NativePatcher;
|
||||
import org.jackhuang.hmcl.util.SettingsMap;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.Platform;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
|
||||
import java.util.Locale;
|
||||
@@ -65,8 +72,8 @@ import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.*;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class VersionsPage extends Control implements WizardPage, Refreshable {
|
||||
private final String gameVersion;
|
||||
@@ -172,7 +179,10 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
{
|
||||
if ("game".equals(control.libraryId)) {
|
||||
JFXButton wikiButton = newToggleButton4(SVG.GLOBE_BOOK);
|
||||
wikiButton.setOnAction(event -> onOpenWiki());
|
||||
wikiButton.setOnAction(event -> {
|
||||
onOpenWiki();
|
||||
FXUtils.clearFocus(wikiButton);
|
||||
});
|
||||
FXUtils.installFastTooltip(wikiButton, i18n("wiki.tooltip"));
|
||||
actions.getChildren().add(wikiButton);
|
||||
}
|
||||
@@ -233,13 +243,14 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
|
||||
if (remoteVersion instanceof GameRemoteVersion) {
|
||||
RemoteVersion.Type versionType = remoteVersion.getVersionType();
|
||||
GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(remoteVersion.getGameVersion());
|
||||
|
||||
switch (versionType) {
|
||||
case RELEASE:
|
||||
case RELEASE -> {
|
||||
twoLineListItem.addTag(i18n("version.game.release"));
|
||||
imageView.setImage(VersionIconType.GRASS.getIcon());
|
||||
break;
|
||||
case PENDING:
|
||||
case SNAPSHOT:
|
||||
}
|
||||
case PENDING, SNAPSHOT -> {
|
||||
if (versionType == RemoteVersion.Type.SNAPSHOT
|
||||
&& GameVersionNumber.asGameVersion(remoteVersion.getGameVersion()).isAprilFools()) {
|
||||
twoLineListItem.addTag(i18n("version.game.april_fools"));
|
||||
@@ -248,11 +259,16 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
twoLineListItem.addTag(i18n("version.game.snapshot"));
|
||||
imageView.setImage(VersionIconType.COMMAND.getIcon());
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
default -> {
|
||||
twoLineListItem.addTag(i18n("version.game.old"));
|
||||
imageView.setImage(VersionIconType.CRAFT_TABLE.getIcon());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
switch (NativePatcher.checkSupportedStatus(gameVersion, Platform.SYSTEM_PLATFORM, OperatingSystem.SYSTEM_VERSION)) {
|
||||
case UNTESTED -> twoLineListItem.addTagWarning(i18n("version.game.support_status.untested"));
|
||||
case UNSUPPORTED -> twoLineListItem.addTagWarning(i18n("version.game.support_status.unsupported"));
|
||||
}
|
||||
} else {
|
||||
VersionIconType iconType;
|
||||
@@ -361,18 +377,6 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
);
|
||||
}
|
||||
}
|
||||
// {
|
||||
// HBox actionsBox = new HBox(8);
|
||||
// GridPane.setColumnSpan(actionsBox, 4);
|
||||
// actionsBox.setAlignment(Pos.CENTER_RIGHT);
|
||||
//
|
||||
// JFXButton refreshButton = FXUtils.newRaisedButton(i18n("button.refresh"));
|
||||
// refreshButton.setOnAction(event -> control.onRefresh());
|
||||
//
|
||||
// actionsBox.getChildren().setAll(refreshButton);
|
||||
//
|
||||
// searchPane.addRow(rowIndex++, actionsBox);
|
||||
// }
|
||||
}
|
||||
|
||||
{
|
||||
@@ -448,21 +452,16 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
if (filter != null)
|
||||
versions = versions.filter(it -> {
|
||||
RemoteVersion.Type versionType = it.getVersionType();
|
||||
switch (filter) {
|
||||
case RELEASE:
|
||||
return versionType == RemoteVersion.Type.RELEASE;
|
||||
case SNAPSHOTS:
|
||||
return versionType == RemoteVersion.Type.SNAPSHOT
|
||||
|| versionType == RemoteVersion.Type.PENDING;
|
||||
case APRIL_FOOLS:
|
||||
return versionType == RemoteVersion.Type.SNAPSHOT
|
||||
&& GameVersionNumber.asGameVersion(it.getGameVersion()).isAprilFools();
|
||||
case OLD:
|
||||
return versionType == RemoteVersion.Type.OLD;
|
||||
case ALL:
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
return switch (filter) {
|
||||
case RELEASE -> versionType == RemoteVersion.Type.RELEASE;
|
||||
case SNAPSHOTS -> versionType == RemoteVersion.Type.SNAPSHOT
|
||||
|| versionType == RemoteVersion.Type.PENDING;
|
||||
case APRIL_FOOLS -> versionType == RemoteVersion.Type.SNAPSHOT
|
||||
&& GameVersionNumber.asGameVersion(it.getGameVersion()).isAprilFools();
|
||||
case OLD -> versionType == RemoteVersion.Type.OLD;
|
||||
// case ALL,
|
||||
default -> true;
|
||||
};
|
||||
});
|
||||
|
||||
String nameQuery = nameField.getText();
|
||||
|
||||
@@ -152,6 +152,7 @@ public abstract class SettingsView extends StackPane {
|
||||
previewPane = new OptionToggleButton();
|
||||
previewPane.setTitle(i18n("update.preview"));
|
||||
previewPane.selectedProperty().bindBidirectional(config().acceptPreviewUpdateProperty());
|
||||
FXUtils.installFastTooltip(previewPane, i18n("update.preview.tooltip"));
|
||||
|
||||
settingsPane.getContent().add(previewPane);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ import javafx.beans.property.DoubleProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleDoubleProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.WeakChangeListener;
|
||||
import javafx.collections.FXCollections;
|
||||
@@ -41,6 +43,8 @@ import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
import org.jackhuang.hmcl.game.LauncherHelper;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
@@ -79,10 +83,14 @@ import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.globalConfig;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class TerracottaControllerPage extends StackPane {
|
||||
private static final String FEEDBACK_TIP = "terracotta-feedback";
|
||||
private static final ObjectProperty<TerracottaState> UI_STATE = new SimpleObjectProperty<>();
|
||||
|
||||
static {
|
||||
@@ -177,13 +185,25 @@ public class TerracottaControllerPage extends StackPane {
|
||||
download.setSubtitle(i18n("terracotta.status.uninitialized.desc"));
|
||||
download.setRightIcon(SVG.ARROW_FORWARD);
|
||||
FXUtils.onClicked(download, () -> {
|
||||
if (uninitialized.hasLegacy()) {
|
||||
if (globalConfig().getTerracottaAgreementVersion() >= 1) {
|
||||
TerracottaState.Preparing s = TerracottaManager.install(null);
|
||||
if (s != null) {
|
||||
UI_STATE.set(s);
|
||||
}
|
||||
|
||||
if (uninitialized.hasLegacy() && I18n.isUseChinese()) {
|
||||
Object feedback = config().getShownTips().get(FEEDBACK_TIP);
|
||||
if (!(feedback instanceof Number number) || number.intValue() < 1) {
|
||||
Controllers.confirm(i18n("terracotta.feedback.desc"), i18n("terracotta.feedback.title"), () -> {
|
||||
FXUtils.openLink(TerracottaMetadata.FEEDBACK_LINK);
|
||||
config().getShownTips().put(FEEDBACK_TIP, 1);
|
||||
}, () -> {
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Controllers.confirmActionDanger(i18n("terracotta.confirm.desc"), i18n("terracotta.confirm.title"), () -> {
|
||||
globalConfig().setTerracottaAgreementVersion(1);
|
||||
TerracottaState.Preparing s = TerracottaManager.install(null);
|
||||
if (s != null) {
|
||||
UI_STATE.set(s);
|
||||
@@ -265,7 +285,18 @@ public class TerracottaControllerPage extends StackPane {
|
||||
});
|
||||
});
|
||||
|
||||
nodesProperty.setAll(flow, host, guest);
|
||||
if (ThreadLocalRandom.current().nextDouble() < 0.02D) {
|
||||
LineButton feedback = LineButton.of();
|
||||
feedback.setLeftIcon(SVG.FEEDBACK);
|
||||
feedback.setTitle(i18n("terracotta.feedback.title"));
|
||||
feedback.setSubtitle(i18n("terracotta.feedback.desc"));
|
||||
feedback.setRightIcon(SVG.OPEN_IN_NEW);
|
||||
FXUtils.onClicked(feedback, () -> FXUtils.openLink(TerracottaMetadata.FEEDBACK_LINK));
|
||||
|
||||
nodesProperty.setAll(flow, host, guest, feedback);
|
||||
} else {
|
||||
nodesProperty.setAll(flow, host, guest);
|
||||
}
|
||||
} else if (state instanceof TerracottaState.HostScanning) {
|
||||
statusProperty.set(i18n("terracotta.status.scanning"));
|
||||
progressProperty.set(-1);
|
||||
@@ -303,7 +334,11 @@ public class TerracottaControllerPage extends StackPane {
|
||||
nodesProperty.setAll(room);
|
||||
} else if (state instanceof TerracottaState.HostOK hostOK) {
|
||||
if (hostOK.isForkOf(legacyState)) {
|
||||
((PlayerProfileUI) nodesProperty.get(nodesProperty.size() - 1)).updateProfiles(hostOK.getProfiles());
|
||||
if (nodesProperty.get(nodesProperty.size() - 1) instanceof PlayerProfileUI profileUI) {
|
||||
profileUI.updateProfiles(hostOK.getProfiles());
|
||||
} else { // Should NOT happen
|
||||
nodesProperty.add(new PlayerProfileUI(hostOK.getProfiles()));
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
String cs = hostOK.getCode();
|
||||
@@ -357,7 +392,11 @@ public class TerracottaControllerPage extends StackPane {
|
||||
}
|
||||
});
|
||||
|
||||
nodesProperty.setAll(code, copy, back, new PlayerProfileUI(hostOK.getProfiles()));
|
||||
if (hostOK.getProfiles().isEmpty()) {
|
||||
nodesProperty.setAll(code, copy, back);
|
||||
} else {
|
||||
nodesProperty.setAll(code, copy, back, new PlayerProfileUI(hostOK.getProfiles()));
|
||||
}
|
||||
}
|
||||
} else if (state instanceof TerracottaState.GuestStarting) {
|
||||
statusProperty.set(i18n("terracotta.status.guest_starting"));
|
||||
@@ -377,7 +416,11 @@ public class TerracottaControllerPage extends StackPane {
|
||||
nodesProperty.setAll(room);
|
||||
} else if (state instanceof TerracottaState.GuestOK guestOK) {
|
||||
if (guestOK.isForkOf(legacyState)) {
|
||||
((PlayerProfileUI) nodesProperty.get(nodesProperty.size() - 1)).updateProfiles(guestOK.getProfiles());
|
||||
if (nodesProperty.get(nodesProperty.size() - 1) instanceof PlayerProfileUI profileUI) {
|
||||
profileUI.updateProfiles(guestOK.getProfiles());
|
||||
} else { // Should NOT happen
|
||||
nodesProperty.add(new PlayerProfileUI(guestOK.getProfiles()));
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
statusProperty.set(i18n("terracotta.status.guest_ok"));
|
||||
@@ -398,7 +441,11 @@ public class TerracottaControllerPage extends StackPane {
|
||||
}
|
||||
});
|
||||
|
||||
nodesProperty.setAll(tutorial, back, new PlayerProfileUI(guestOK.getProfiles()));
|
||||
if (guestOK.getProfiles().isEmpty()) {
|
||||
nodesProperty.setAll(tutorial, back);
|
||||
} else {
|
||||
nodesProperty.setAll(tutorial, back, new PlayerProfileUI(guestOK.getProfiles()));
|
||||
}
|
||||
}
|
||||
} else if (state instanceof TerracottaState.Exception exception) {
|
||||
statusProperty.set(i18n("terracotta.status.exception.desc." + exception.getType().name().toLowerCase(Locale.ROOT)));
|
||||
@@ -530,7 +577,7 @@ public class TerracottaControllerPage extends StackPane {
|
||||
header.setSubtitle(i18n("terracotta.from_local.desc"));
|
||||
locals.setHeaderLeft(header);
|
||||
|
||||
for (TerracottaMetadata.Link link : TerracottaMetadata.getPackageLinks()) {
|
||||
for (TerracottaMetadata.Link link : TerracottaMetadata.PACKAGE_LINKS) {
|
||||
HBox node = new HBox();
|
||||
node.setAlignment(Pos.CENTER_LEFT);
|
||||
node.setPadding(new Insets(10, 16, 10, 16));
|
||||
@@ -557,9 +604,10 @@ public class TerracottaControllerPage extends StackPane {
|
||||
private static final class LineButton extends RipplerContainer {
|
||||
private final WeakListenerHolder holder = new WeakListenerHolder();
|
||||
|
||||
private final TwoLineListItem middle = new TwoLineListItem();
|
||||
private final ObjectProperty<Node> left = new SimpleObjectProperty<>();
|
||||
private final ObjectProperty<Node> right = new SimpleObjectProperty<>();
|
||||
private final ObjectProperty<Node> left = new SimpleObjectProperty<>(this, "left");
|
||||
private final ObjectProperty<Node> right = new SimpleObjectProperty<>(this, "right");
|
||||
private final StringProperty title = new SimpleStringProperty(this, "title", "");
|
||||
private final StringProperty subTitle = new SimpleStringProperty(this, "subTitle", "");
|
||||
|
||||
public static LineButton of() {
|
||||
return of(true);
|
||||
@@ -584,7 +632,35 @@ public class TerracottaControllerPage extends StackPane {
|
||||
nodes.add(left);
|
||||
}
|
||||
|
||||
nodes.add(button.middle);
|
||||
{
|
||||
// FIXME: It's sucked to have the following TwoLineListItem-liked logic whose subtitle is a TextFlow.
|
||||
VBox middle = new VBox();
|
||||
middle.getStyleClass().add("two-line-list-item");
|
||||
middle.setMouseTransparent(true);
|
||||
{
|
||||
HBox firstLine = new HBox();
|
||||
firstLine.getStyleClass().add("first-line");
|
||||
{
|
||||
Label lblTitle = new Label(button.title.get());
|
||||
lblTitle.getStyleClass().add("title");
|
||||
firstLine.getChildren().setAll(lblTitle);
|
||||
}
|
||||
|
||||
HBox secondLine = new HBox();
|
||||
{
|
||||
Text text = new Text(button.subTitle.get());
|
||||
text.setFill(new Color(0, 0, 0, 0.5));
|
||||
|
||||
TextFlow lblSubtitle = new TextFlow(text);
|
||||
lblSubtitle.getStyleClass().add("subtitle");
|
||||
secondLine.getChildren().setAll(lblSubtitle);
|
||||
}
|
||||
|
||||
middle.getChildren().setAll(firstLine, secondLine);
|
||||
}
|
||||
nodes.add(middle);
|
||||
}
|
||||
|
||||
nodes.add(spacing);
|
||||
|
||||
Node right = button.right.get();
|
||||
@@ -593,7 +669,7 @@ public class TerracottaControllerPage extends StackPane {
|
||||
}
|
||||
|
||||
container.getChildren().setAll(nodes);
|
||||
}, button.middle.titleProperty(), button.middle.subtitleProperty(), button.left, button.right));
|
||||
}, button.title, button.subTitle, button.left, button.right));
|
||||
button.getProperties().put("ComponentList.noPadding", true);
|
||||
|
||||
return button;
|
||||
@@ -604,11 +680,11 @@ public class TerracottaControllerPage extends StackPane {
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.middle.setTitle(title);
|
||||
this.title.set(title);
|
||||
}
|
||||
|
||||
public void setSubtitle(String subtitle) {
|
||||
this.middle.setSubtitle(subtitle);
|
||||
this.subTitle.set(subtitle);
|
||||
}
|
||||
|
||||
public void setLeftImage(Image left) {
|
||||
@@ -648,10 +724,7 @@ public class TerracottaControllerPage extends StackPane {
|
||||
TwoLineListItem item = new TwoLineListItem();
|
||||
item.setTitle(profile.getName());
|
||||
item.setSubtitle(profile.getVendor());
|
||||
item.getTags().setAll(TwoLineListItem.createTagLabel(
|
||||
i18n("terracotta.player_kind." + profile.getType().name().toLowerCase(Locale.ROOT)))
|
||||
);
|
||||
|
||||
item.addTag(i18n("terracotta.player_kind." + profile.getType().name().toLowerCase(Locale.ROOT)));
|
||||
pane.getChildren().add(item);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.ui.terracotta;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.terracotta.TerracottaMetadata;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
@@ -52,6 +53,12 @@ public class TerracottaPage extends DecoratorAnimatedPage implements DecoratorPa
|
||||
transitionPane.setContent(newValue.getNode(), ContainerAnimations.FADE);
|
||||
});
|
||||
|
||||
AdvancedListItem feedback = new AdvancedListItem();
|
||||
feedback.setLeftGraphic(wrap(SVG.FEEDBACK));
|
||||
feedback.setActionButtonVisible(false);
|
||||
feedback.setTitle(i18n("terracotta.feedback.title"));
|
||||
feedback.setOnAction(e -> FXUtils.openLink(TerracottaMetadata.FEEDBACK_LINK));
|
||||
|
||||
AdvancedListItem chatItem = new AdvancedListItem();
|
||||
chatItem.setLeftGraphic(wrap(SVG.CHAT));
|
||||
chatItem.setActionButtonVisible(false);
|
||||
@@ -67,6 +74,7 @@ public class TerracottaPage extends DecoratorAnimatedPage implements DecoratorPa
|
||||
AdvancedListBox sideBar = new AdvancedListBox()
|
||||
.addNavigationDrawerTab(tab, statusPage, i18n("terracotta.status"), SVG.TUNE)
|
||||
.startCategory(i18n("help").toUpperCase(Locale.ROOT))
|
||||
.add(feedback)
|
||||
.add(chatItem)
|
||||
.add(easytierItem);
|
||||
FXUtils.setLimitWidth(sideBar, 200);
|
||||
|
||||
@@ -532,6 +532,9 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
container.setAlignment(Pos.CENTER_LEFT);
|
||||
pane.getChildren().add(container);
|
||||
|
||||
imageView.setFitWidth(40);
|
||||
imageView.setFitHeight(40);
|
||||
|
||||
container.getChildren().setAll(FXUtils.limitingSize(imageView, 40, 40), content);
|
||||
HBox.setHgrow(content, Priority.ALWAYS);
|
||||
}
|
||||
@@ -547,7 +550,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
.map(category -> getSkinnable().getLocalizedCategory(category))
|
||||
.forEach(content::addTag);
|
||||
if (StringUtils.isNotBlank(dataItem.getIconUrl())) {
|
||||
imageView.imageProperty().bind(FXUtils.newRemoteImage(dataItem.getIconUrl(), 40, 40, true, true));
|
||||
imageView.imageProperty().bind(FXUtils.newRemoteImage(dataItem.getIconUrl(), 80, 80, true, true));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -219,8 +219,10 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
BorderPane.setMargin(descriptionPane, new Insets(11, 11, 0, 11));
|
||||
{
|
||||
ImageView imageView = new ImageView();
|
||||
imageView.setFitWidth(40);
|
||||
imageView.setFitHeight(40);
|
||||
if (StringUtils.isNotBlank(getSkinnable().addon.getIconUrl())) {
|
||||
imageView.imageProperty().bind(FXUtils.newRemoteImage(getSkinnable().addon.getIconUrl(), 40, 40, true, true));
|
||||
imageView.imageProperty().bind(FXUtils.newRemoteImage(getSkinnable().addon.getIconUrl(), 80, 80, true, true));
|
||||
}
|
||||
descriptionPane.getChildren().add(FXUtils.limitingSize(imageView, 40, 40));
|
||||
|
||||
@@ -341,6 +343,8 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
TwoLineListItem content = new TwoLineListItem();
|
||||
HBox.setHgrow(content, Priority.ALWAYS);
|
||||
ImageView imageView = new ImageView();
|
||||
imageView.setFitWidth(40);
|
||||
imageView.setFitHeight(40);
|
||||
pane.getChildren().setAll(FXUtils.limitingSize(imageView, 40, 40), content);
|
||||
|
||||
RipplerContainer container = new RipplerContainer(pane);
|
||||
@@ -358,12 +362,12 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
.map(page::getLocalizedCategory)
|
||||
.forEach(content::addTag);
|
||||
if (StringUtils.isNotBlank(addon.getIconUrl())) {
|
||||
imageView.imageProperty().bind(FXUtils.newRemoteImage(addon.getIconUrl(), 40, 40, true, true));
|
||||
imageView.imageProperty().bind(FXUtils.newRemoteImage(addon.getIconUrl(), 80, 80, true, true));
|
||||
}
|
||||
} else {
|
||||
content.setTitle(i18n("mods.broken_dependency.title"));
|
||||
content.setSubtitle(i18n("mods.broken_dependency.desc"));
|
||||
imageView.setImage(FXUtils.newBuiltinImage("/assets/img/icon@8x.png", 40, 40, true, true));
|
||||
imageView.setImage(FXUtils.newBuiltinImage("/assets/img/icon@4x.png"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.collections.ObservableList;
|
||||
@@ -27,6 +26,7 @@ import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.mod.ModLoaderType;
|
||||
import org.jackhuang.hmcl.mod.ModManager;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
@@ -44,19 +44,22 @@ import java.io.UncheckedIOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObject> implements VersionPage.VersionLoadable, PageAware {
|
||||
private final BooleanProperty modded = new SimpleBooleanProperty(this, "modded", false);
|
||||
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
|
||||
private ModManager modManager;
|
||||
private LibraryAnalyzer libraryAnalyzer;
|
||||
private Profile profile;
|
||||
private String versionId;
|
||||
private String instanceId;
|
||||
private String gameVersion;
|
||||
|
||||
final EnumSet<ModLoaderType> supportedLoaders = EnumSet.noneOf(ModLoaderType.class);
|
||||
|
||||
public ModListPage() {
|
||||
FXUtils.applyDragListener(this, it -> Arrays.asList("jar", "zip", "litemod").contains(FileUtils.getExtension(it)), mods -> {
|
||||
@@ -83,34 +86,82 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
@Override
|
||||
public void loadVersion(Profile profile, String id) {
|
||||
this.profile = profile;
|
||||
this.versionId = id;
|
||||
this.instanceId = id;
|
||||
|
||||
HMCLGameRepository repository = profile.getRepository();
|
||||
Version resolved = repository.getResolvedPreservingPatchesVersion(id);
|
||||
libraryAnalyzer = LibraryAnalyzer.analyze(resolved, repository.getGameVersion(resolved).orElse(null));
|
||||
modded.set(libraryAnalyzer.hasModLoader());
|
||||
this.gameVersion = repository.getGameVersion(resolved).orElse(null);
|
||||
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(resolved, gameVersion);
|
||||
modded.set(analyzer.hasModLoader());
|
||||
loadMods(profile.getRepository().getModManager(id));
|
||||
}
|
||||
|
||||
private CompletableFuture<?> loadMods(ModManager modManager) {
|
||||
private void loadMods(ModManager modManager) {
|
||||
setLoading(true);
|
||||
|
||||
this.modManager = modManager;
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
CompletableFuture.supplyAsync(() -> {
|
||||
lock.lock();
|
||||
try {
|
||||
synchronized (ModListPage.this) {
|
||||
runInFX(() -> loadingProperty().set(true));
|
||||
modManager.refreshMods();
|
||||
return new ArrayList<>(modManager.getMods());
|
||||
}
|
||||
modManager.refreshMods();
|
||||
return modManager.getMods().stream().map(ModListPageSkin.ModInfoObject::new).toList();
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}, Schedulers.defaultScheduler()).whenCompleteAsync((list, exception) -> {
|
||||
loadingProperty().set(false);
|
||||
if (exception == null)
|
||||
itemsProperty().setAll(list.stream().map(ModListPageSkin.ModInfoObject::new).sorted().collect(Collectors.toList()));
|
||||
else
|
||||
getProperties().remove(ModListPage.class);
|
||||
}, Platform::runLater);
|
||||
}, Schedulers.io()).whenCompleteAsync((list, exception) -> {
|
||||
updateSupportedLoaders(modManager);
|
||||
|
||||
if (exception == null) {
|
||||
getItems().setAll(list);
|
||||
} else {
|
||||
LOG.warning("Failed to load mods", exception);
|
||||
getItems().clear();
|
||||
}
|
||||
setLoading(false);
|
||||
}, Schedulers.javafx());
|
||||
}
|
||||
|
||||
private void updateSupportedLoaders(ModManager modManager) {
|
||||
supportedLoaders.clear();
|
||||
|
||||
LibraryAnalyzer analyzer = modManager.getLibraryAnalyzer();
|
||||
if (analyzer == null) {
|
||||
Collections.addAll(supportedLoaders, ModLoaderType.values());
|
||||
return;
|
||||
}
|
||||
|
||||
for (LibraryAnalyzer.LibraryType type : LibraryAnalyzer.LibraryType.values()) {
|
||||
if (type.isModLoader() && analyzer.has(type)) {
|
||||
ModLoaderType modLoaderType = type.getModLoaderType();
|
||||
if (modLoaderType != null) {
|
||||
supportedLoaders.add(modLoaderType);
|
||||
|
||||
if (modLoaderType == ModLoaderType.CLEANROOM)
|
||||
supportedLoaders.add(ModLoaderType.FORGE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (analyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE) && "1.20.1".equals(gameVersion)) {
|
||||
supportedLoaders.add(ModLoaderType.FORGE);
|
||||
}
|
||||
|
||||
if (analyzer.has(LibraryAnalyzer.LibraryType.QUILT)) {
|
||||
supportedLoaders.add(ModLoaderType.FABRIC);
|
||||
}
|
||||
|
||||
if (analyzer.has(LibraryAnalyzer.LibraryType.FABRIC) && modManager.hasMod("kilt", ModLoaderType.FABRIC)) {
|
||||
supportedLoaders.add(ModLoaderType.FORGE);
|
||||
supportedLoaders.add(ModLoaderType.NEO_FORGED);
|
||||
}
|
||||
|
||||
// Sinytra Connector
|
||||
if (analyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE) && modManager.hasMod("connectormod", ModLoaderType.NEO_FORGED)
|
||||
|| "1.20.1".equals(gameVersion) && analyzer.has(LibraryAnalyzer.LibraryType.FORGE) && modManager.hasMod("connectormod", ModLoaderType.FORGE)) {
|
||||
supportedLoaders.add(ModLoaderType.FABRIC);
|
||||
}
|
||||
}
|
||||
|
||||
public void add() {
|
||||
@@ -148,7 +199,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
}).start();
|
||||
}
|
||||
|
||||
public void removeSelected(ObservableList<ModListPageSkin.ModInfoObject> selectedItems) {
|
||||
void removeSelected(ObservableList<ModListPageSkin.ModInfoObject> selectedItems) {
|
||||
try {
|
||||
modManager.removeMods(selectedItems.stream()
|
||||
.filter(Objects::nonNull)
|
||||
@@ -160,14 +211,14 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
}
|
||||
}
|
||||
|
||||
public void enableSelected(ObservableList<ModListPageSkin.ModInfoObject> selectedItems) {
|
||||
void enableSelected(ObservableList<ModListPageSkin.ModInfoObject> selectedItems) {
|
||||
selectedItems.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(ModListPageSkin.ModInfoObject::getModInfo)
|
||||
.forEach(info -> info.setActive(true));
|
||||
}
|
||||
|
||||
public void disableSelected(ObservableList<ModListPageSkin.ModInfoObject> selectedItems) {
|
||||
void disableSelected(ObservableList<ModListPageSkin.ModInfoObject> selectedItems) {
|
||||
selectedItems.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(ModListPageSkin.ModInfoObject::getModInfo)
|
||||
@@ -175,13 +226,13 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
}
|
||||
|
||||
public void openModFolder() {
|
||||
FXUtils.openFolder(profile.getRepository().getRunDirectory(versionId).resolve("mods"));
|
||||
FXUtils.openFolder(profile.getRepository().getRunDirectory(instanceId).resolve("mods"));
|
||||
}
|
||||
|
||||
public void checkUpdates() {
|
||||
Runnable action = () -> Controllers.taskDialog(Task
|
||||
.composeAsync(() -> {
|
||||
Optional<String> gameVersion = profile.getRepository().getGameVersion(versionId);
|
||||
Optional<String> gameVersion = profile.getRepository().getGameVersion(instanceId);
|
||||
if (gameVersion.isPresent()) {
|
||||
return new ModCheckUpdatesTask(gameVersion.get(), modManager.getMods());
|
||||
}
|
||||
@@ -199,7 +250,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
.withStagesHint(Collections.singletonList("mods.check_updates")),
|
||||
i18n("update.checking"), TaskCancellationAction.NORMAL);
|
||||
|
||||
if (profile.getRepository().isModpack(versionId)) {
|
||||
if (profile.getRepository().isModpack(instanceId)) {
|
||||
Controllers.confirm(
|
||||
i18n("mods.update_modpack_mod.warning"), null,
|
||||
MessageDialogPane.MessageType.WARNING,
|
||||
@@ -210,7 +261,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
}
|
||||
|
||||
public void download() {
|
||||
Controllers.getDownloadPage().showModDownloads().selectVersion(versionId);
|
||||
Controllers.getDownloadPage().showModDownloads().selectVersion(instanceId);
|
||||
Controllers.navigate(Controllers.getDownloadPage());
|
||||
}
|
||||
|
||||
@@ -239,7 +290,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
return this.profile;
|
||||
}
|
||||
|
||||
public String getVersionId() {
|
||||
return this.versionId;
|
||||
public String getInstanceId() {
|
||||
return this.instanceId;
|
||||
}
|
||||
}
|
||||
@@ -18,18 +18,17 @@
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import com.jfoenix.controls.*;
|
||||
import com.jfoenix.controls.datamodels.treetable.RecursiveTreeObject;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.SelectionMode;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.KeyCode;
|
||||
@@ -37,6 +36,7 @@ import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.mod.ModLoaderType;
|
||||
@@ -55,32 +55,28 @@ import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.util.Holder;
|
||||
import org.jackhuang.hmcl.util.Lazy;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.lang.ref.SoftReference;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.ignoreEvent;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2;
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.StringUtils.isNotBlank;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
@@ -97,6 +93,9 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
// FXThread
|
||||
private boolean isSearching = false;
|
||||
|
||||
@SuppressWarnings({"FieldCanBeLocal", "unused"})
|
||||
private final ChangeListener<Boolean> holder;
|
||||
|
||||
ModListPageSkin(ModListPage skinnable) {
|
||||
super(skinnable);
|
||||
|
||||
@@ -108,6 +107,12 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
root.getStyleClass().add("no-padding");
|
||||
listView = new JFXListView<>();
|
||||
|
||||
this.holder = FXUtils.onWeakChange(skinnable.loadingProperty(), loading -> {
|
||||
if (!loading) {
|
||||
listView.scrollTo(0);
|
||||
}
|
||||
});
|
||||
|
||||
{
|
||||
toolbarPane = new TransitionPane();
|
||||
|
||||
@@ -275,19 +280,41 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
|| predicate.test(modInfo.getGameVersion())
|
||||
|| predicate.test(modInfo.getId())
|
||||
|| predicate.test(Objects.toString(modInfo.getModLoaderType()))
|
||||
|| predicate.test((item.getMod() != null ? item.getMod().getDisplayName() : null))) {
|
||||
|| predicate.test((item.getModTranslations() != null ? item.getModTranslations().getDisplayName() : null))) {
|
||||
listView.getItems().add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Task<Image> loadModIcon(LocalModFile modFile, int size) {
|
||||
return Task.supplyAsync(() -> {
|
||||
static final class ModInfoObject {
|
||||
private final BooleanProperty active;
|
||||
private final LocalModFile localModFile;
|
||||
private final @Nullable ModTranslations.Mod modTranslations;
|
||||
|
||||
private SoftReference<CompletableFuture<Image>> iconCache;
|
||||
|
||||
ModInfoObject(LocalModFile localModFile) {
|
||||
this.localModFile = localModFile;
|
||||
this.active = localModFile.activeProperty();
|
||||
|
||||
this.modTranslations = ModTranslations.MOD.getMod(localModFile.getId(), localModFile.getName());
|
||||
}
|
||||
|
||||
LocalModFile getModInfo() {
|
||||
return localModFile;
|
||||
}
|
||||
|
||||
public @Nullable ModTranslations.Mod getModTranslations() {
|
||||
return modTranslations;
|
||||
}
|
||||
|
||||
@FXThread
|
||||
private Image loadIcon() {
|
||||
List<String> iconPaths = new ArrayList<>();
|
||||
|
||||
if (StringUtils.isNotBlank(modFile.getLogoPath())) {
|
||||
iconPaths.add(modFile.getLogoPath());
|
||||
if (StringUtils.isNotBlank(this.localModFile.getLogoPath())) {
|
||||
iconPaths.add(this.localModFile.getLogoPath());
|
||||
}
|
||||
|
||||
iconPaths.addAll(List.of(
|
||||
@@ -311,7 +338,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
"resources/mod_icon.png"
|
||||
));
|
||||
|
||||
String modId = modFile.getId();
|
||||
String modId = this.localModFile.getId();
|
||||
if (StringUtils.isNotBlank(modId)) {
|
||||
iconPaths.addAll(List.of(
|
||||
"assets/" + modId + "/icon.png",
|
||||
@@ -330,14 +357,12 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
));
|
||||
}
|
||||
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile.getFile())) {
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(this.localModFile.getFile())) {
|
||||
for (String path : iconPaths) {
|
||||
Path iconPath = fs.getPath(path);
|
||||
if (Files.exists(iconPath)) {
|
||||
Image image = FXUtils.loadImage(iconPath, size, size, true, true);
|
||||
if (!image.isError() &&
|
||||
image.getWidth() > 0 &&
|
||||
image.getHeight() > 0 &&
|
||||
Image image = FXUtils.loadImage(iconPath, 80, 80, true, true);
|
||||
if (!image.isError() && image.getWidth() > 0 && image.getHeight() > 0 &&
|
||||
Math.abs(image.getWidth() - image.getHeight()) < 1) {
|
||||
return image;
|
||||
}
|
||||
@@ -347,102 +372,68 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
LOG.warning("Failed to load mod icons", e);
|
||||
}
|
||||
|
||||
VersionIconType defaultIcon = switch (modFile.getModLoaderType()) {
|
||||
case FORGE -> VersionIconType.FORGE;
|
||||
case NEO_FORGED -> VersionIconType.NEO_FORGE;
|
||||
case FABRIC -> VersionIconType.FABRIC;
|
||||
case QUILT -> VersionIconType.QUILT;
|
||||
case LITE_LOADER -> VersionIconType.CHICKEN;
|
||||
case CLEANROOM -> VersionIconType.CLEANROOM;
|
||||
default -> VersionIconType.COMMAND;
|
||||
};
|
||||
return VersionIconType.getIconType(this.localModFile.getModLoaderType()).getIcon();
|
||||
}
|
||||
|
||||
return defaultIcon.getIcon(size);
|
||||
});
|
||||
}
|
||||
|
||||
static class ModInfoObject extends RecursiveTreeObject<ModInfoObject> implements Comparable<ModInfoObject> {
|
||||
private final BooleanProperty active;
|
||||
private final LocalModFile localModFile;
|
||||
private final String title;
|
||||
private final String message;
|
||||
private final ModTranslations.Mod mod;
|
||||
|
||||
private SoftReference<Image> iconCache;
|
||||
|
||||
ModInfoObject(LocalModFile localModFile) {
|
||||
this.localModFile = localModFile;
|
||||
this.active = localModFile.activeProperty();
|
||||
|
||||
this.title = localModFile.getName();
|
||||
|
||||
List<String> parts = new ArrayList<>();
|
||||
if (isNotBlank(localModFile.getId())) {
|
||||
parts.add(localModFile.getId());
|
||||
public void loadIcon(ImageView imageView, @Nullable WeakReference<ObjectProperty<ModInfoObject>> current) {
|
||||
SoftReference<CompletableFuture<Image>> iconCache = this.iconCache;
|
||||
CompletableFuture<Image> imageFuture;
|
||||
if (iconCache != null && (imageFuture = iconCache.get()) != null) {
|
||||
Image image = imageFuture.getNow(null);
|
||||
if (image != null) {
|
||||
imageView.setImage(image);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
imageFuture = CompletableFuture.supplyAsync(this::loadIcon, Schedulers.io());
|
||||
this.iconCache = new SoftReference<>(imageFuture);
|
||||
}
|
||||
if (isNotBlank(localModFile.getVersion())) {
|
||||
parts.add(localModFile.getVersion());
|
||||
}
|
||||
if (isNotBlank(localModFile.getGameVersion())) {
|
||||
parts.add(i18n("game.version") + ": " + localModFile.getGameVersion());
|
||||
}
|
||||
this.message = String.join(", ", parts);
|
||||
this.mod = ModTranslations.MOD.getModById(localModFile.getId());
|
||||
}
|
||||
imageView.setImage(VersionIconType.getIconType(localModFile.getModLoaderType()).getIcon());
|
||||
imageFuture.thenAcceptAsync(image -> {
|
||||
if (current != null) {
|
||||
ObjectProperty<ModInfoObject> infoObjectProperty = current.get();
|
||||
if (infoObjectProperty == null || infoObjectProperty.get() != this) {
|
||||
// The current ListCell has already switched to another object
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
String getSubtitle() {
|
||||
return message;
|
||||
}
|
||||
|
||||
LocalModFile getModInfo() {
|
||||
return localModFile;
|
||||
}
|
||||
|
||||
public ModTranslations.Mod getMod() {
|
||||
return mod;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(@NotNull ModListPageSkin.ModInfoObject o) {
|
||||
return localModFile.getFileName().toLowerCase().compareTo(o.localModFile.getFileName().toLowerCase());
|
||||
imageView.setImage(image);
|
||||
}, Schedulers.javafx());
|
||||
}
|
||||
}
|
||||
|
||||
class ModInfoDialog extends JFXDialogLayout {
|
||||
final class ModInfoDialog extends JFXDialogLayout {
|
||||
|
||||
ModInfoDialog(ModInfoObject modInfo) {
|
||||
HBox titleContainer = new HBox();
|
||||
titleContainer.setSpacing(8);
|
||||
|
||||
Stage stage = Controllers.getStage();
|
||||
maxWidthProperty().bind(stage.widthProperty().multiply(0.7));
|
||||
|
||||
ImageView imageView = new ImageView();
|
||||
FXUtils.limitSize(imageView, 40, 40);
|
||||
loadModIcon(modInfo.getModInfo(), 40)
|
||||
.whenComplete(Schedulers.javafx(), (image, exception) -> {
|
||||
imageView.setImage(image);
|
||||
}).start();
|
||||
modInfo.loadIcon(imageView, null);
|
||||
|
||||
TwoLineListItem title = new TwoLineListItem();
|
||||
title.setTitle(modInfo.getModInfo().getName());
|
||||
if (modInfo.getMod() != null) {
|
||||
title.addTag(modInfo.getMod().getDisplayName());
|
||||
}
|
||||
if (modInfo.getModTranslations() != null && I18n.isUseChinese())
|
||||
title.setTitle(modInfo.getModTranslations().getDisplayName());
|
||||
else
|
||||
title.setTitle(modInfo.getModInfo().getName());
|
||||
|
||||
List<String> subtitleParts = new ArrayList<>();
|
||||
subtitleParts.add(FileUtils.getName(modInfo.getModInfo().getFile()));
|
||||
StringJoiner subtitle = new StringJoiner(" | ");
|
||||
subtitle.add(FileUtils.getName(modInfo.getModInfo().getFile()));
|
||||
if (StringUtils.isNotBlank(modInfo.getModInfo().getGameVersion())) {
|
||||
subtitleParts.add(modInfo.getModInfo().getGameVersion());
|
||||
subtitle.add(modInfo.getModInfo().getGameVersion());
|
||||
}
|
||||
if (StringUtils.isNotBlank(modInfo.getModInfo().getVersion())) {
|
||||
subtitleParts.add(modInfo.getModInfo().getVersion());
|
||||
subtitle.add(modInfo.getModInfo().getVersion());
|
||||
}
|
||||
if (StringUtils.isNotBlank(modInfo.getModInfo().getAuthors())) {
|
||||
subtitleParts.add(i18n("archive.author") + ": " + modInfo.getModInfo().getAuthors());
|
||||
subtitle.add(i18n("archive.author") + ": " + modInfo.getModInfo().getAuthors());
|
||||
}
|
||||
title.setSubtitle(String.join(", ", subtitleParts));
|
||||
title.setSubtitle(subtitle.toString());
|
||||
|
||||
titleContainer.getChildren().setAll(FXUtils.limitingSize(imageView, 40, 40), title);
|
||||
setHeading(titleContainer);
|
||||
@@ -456,6 +447,11 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
descriptionPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
descriptionPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
|
||||
descriptionPane.setFitToWidth(true);
|
||||
description.heightProperty().addListener((obs, oldVal, newVal) -> {
|
||||
double maxHeight = stage.getHeight() * 0.5;
|
||||
double targetHeight = Math.min(newVal.doubleValue(), maxHeight);
|
||||
descriptionPane.setPrefViewportHeight(targetHeight);
|
||||
});
|
||||
|
||||
setBody(descriptionPane);
|
||||
|
||||
@@ -507,7 +503,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
Controllers.navigate(new DownloadPage(
|
||||
repository instanceof CurseForgeRemoteModRepository ? HMCLLocalizedDownloadListPage.ofCurseForgeMod(null, false) : HMCLLocalizedDownloadListPage.ofModrinthMod(null, false),
|
||||
remoteMod,
|
||||
new Profile.ProfileVersion(ModListPageSkin.this.getSkinnable().getProfile(), ModListPageSkin.this.getSkinnable().getVersionId()),
|
||||
new Profile.ProfileVersion(ModListPageSkin.this.getSkinnable().getProfile(), ModListPageSkin.this.getSkinnable().getInstanceId()),
|
||||
(profile, version, file) -> org.jackhuang.hmcl.ui.download.DownloadPage.download(profile, version, file, "mods")
|
||||
));
|
||||
});
|
||||
@@ -530,7 +526,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
getActions().add(officialPageButton);
|
||||
}
|
||||
|
||||
if (modInfo.getMod() == null || StringUtils.isBlank(modInfo.getMod().getMcmod())) {
|
||||
if (modInfo.getModTranslations() == null || StringUtils.isBlank(modInfo.getModTranslations().getMcmod())) {
|
||||
JFXHyperlink searchButton = new JFXHyperlink(i18n("mods.mcmod.search"));
|
||||
searchButton.setOnAction(e -> {
|
||||
fireEvent(new DialogCloseEvent());
|
||||
@@ -545,7 +541,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
JFXHyperlink mcmodButton = new JFXHyperlink(i18n("mods.mcmod.page"));
|
||||
mcmodButton.setOnAction(e -> {
|
||||
fireEvent(new DialogCloseEvent());
|
||||
FXUtils.openLink(ModTranslations.MOD.getMcmodUrl(modInfo.getMod()));
|
||||
FXUtils.openLink(ModTranslations.MOD.getMcmodUrl(modInfo.getModTranslations()));
|
||||
});
|
||||
getActions().add(mcmodButton);
|
||||
}
|
||||
@@ -564,6 +560,8 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
private static final Lazy<JFXPopup> popup = new Lazy<>(() -> new JFXPopup(menu.get()));
|
||||
|
||||
final class ModInfoListCell extends MDListCell<ModInfoObject> {
|
||||
private static final PseudoClass WARNING = PseudoClass.getPseudoClass("warning");
|
||||
|
||||
JFXCheckBox checkBox = new JFXCheckBox();
|
||||
ImageView imageView = new ImageView();
|
||||
TwoLineListItem content = new TwoLineListItem();
|
||||
@@ -572,9 +570,13 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
JFXButton revealButton = new JFXButton();
|
||||
BooleanProperty booleanProperty;
|
||||
|
||||
Tooltip warningTooltip;
|
||||
|
||||
ModInfoListCell(JFXListView<ModInfoObject> listView, Holder<Object> lastCell) {
|
||||
super(listView, lastCell);
|
||||
|
||||
this.getStyleClass().add("mod-info-list-cell");
|
||||
|
||||
HBox container = new HBox(8);
|
||||
container.setPickOnBounds(false);
|
||||
container.setAlignment(Pos.CENTER_LEFT);
|
||||
@@ -585,7 +587,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
imageView.setFitWidth(24);
|
||||
imageView.setFitHeight(24);
|
||||
imageView.setPreserveRatio(true);
|
||||
imageView.setImage(FXUtils.newBuiltinImage("/assets/img/command.png", 24, 24, true, true));
|
||||
imageView.setImage(VersionIconType.COMMAND.getIcon());
|
||||
|
||||
restoreButton.getStyleClass().add("toggle-icon4");
|
||||
restoreButton.setGraphic(FXUtils.limitingSize(SVG.RESTORE.createIcon(Theme.blackFill(), 24), 24, 24));
|
||||
@@ -606,68 +608,113 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
|
||||
@Override
|
||||
protected void updateControl(ModInfoObject dataItem, boolean empty) {
|
||||
pseudoClassStateChanged(WARNING, false);
|
||||
if (warningTooltip != null) {
|
||||
Tooltip.uninstall(this, warningTooltip);
|
||||
warningTooltip = null;
|
||||
}
|
||||
|
||||
if (empty) return;
|
||||
|
||||
SoftReference<Image> iconCache = dataItem.iconCache;
|
||||
Image icon;
|
||||
if (iconCache != null && (icon = iconCache.get()) != null) {
|
||||
imageView.setImage(icon);
|
||||
} else {
|
||||
loadModIcon(dataItem.getModInfo(), 24)
|
||||
.whenComplete(Schedulers.javafx(), (image, exception) -> {
|
||||
dataItem.iconCache = new SoftReference<>(image);
|
||||
imageView.setImage(image);
|
||||
}).start();
|
||||
List<String> warning = new ArrayList<>();
|
||||
|
||||
content.getTags().clear();
|
||||
|
||||
LocalModFile modInfo = dataItem.getModInfo();
|
||||
ModTranslations.Mod modTranslations = dataItem.getModTranslations();
|
||||
|
||||
ModLoaderType modLoaderType = modInfo.getModLoaderType();
|
||||
|
||||
dataItem.loadIcon(imageView, new WeakReference<>(this.itemProperty()));
|
||||
|
||||
String displayName = modInfo.getName();
|
||||
if (modTranslations != null && I18n.isUseChinese()) {
|
||||
String chineseName = modTranslations.getName();
|
||||
if (StringUtils.containsChinese(chineseName)) {
|
||||
if (StringUtils.containsEmoji(chineseName)) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
chineseName.codePoints().forEach(ch -> {
|
||||
if (ch < 0x1F300 || ch > 0x1FAFF)
|
||||
builder.appendCodePoint(ch);
|
||||
});
|
||||
|
||||
chineseName = builder.toString().trim();
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(chineseName) && !displayName.equalsIgnoreCase(chineseName)) {
|
||||
displayName = displayName + " (" + chineseName + ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
content.setTitle(displayName);
|
||||
|
||||
StringJoiner joiner = new StringJoiner(" | ");
|
||||
if (modLoaderType != ModLoaderType.UNKNOWN && StringUtils.isNotBlank(modInfo.getId()))
|
||||
joiner.add(modInfo.getId());
|
||||
|
||||
joiner.add(FileUtils.getName(modInfo.getFile()));
|
||||
|
||||
content.setSubtitle(joiner.toString());
|
||||
|
||||
if (modLoaderType == ModLoaderType.UNKNOWN) {
|
||||
content.addTagWarning(i18n("mods.unknown"));
|
||||
} else if (!ModListPageSkin.this.getSkinnable().supportedLoaders.contains(modLoaderType)) {
|
||||
warning.add(i18n("mods.warning.loader_mismatch"));
|
||||
switch (dataItem.getModInfo().getModLoaderType()) {
|
||||
case FORGE:
|
||||
content.addTagWarning(i18n("install.installer.forge"));
|
||||
break;
|
||||
case CLEANROOM:
|
||||
content.addTagWarning(i18n("install.installer.cleanroom"));
|
||||
break;
|
||||
case NEO_FORGED:
|
||||
content.addTagWarning(i18n("install.installer.neoforge"));
|
||||
break;
|
||||
case FABRIC:
|
||||
content.addTagWarning(i18n("install.installer.fabric"));
|
||||
break;
|
||||
case LITE_LOADER:
|
||||
content.addTagWarning(i18n("install.installer.liteloader"));
|
||||
break;
|
||||
case QUILT:
|
||||
content.addTagWarning(i18n("install.installer.quilt"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
content.setTitle(dataItem.getTitle());
|
||||
content.getTags().clear();
|
||||
switch (dataItem.getModInfo().getModLoaderType()) {
|
||||
case FORGE:
|
||||
content.addTag(i18n("install.installer.forge"));
|
||||
break;
|
||||
case CLEANROOM:
|
||||
content.addTag(i18n("install.installer.cleanroom"));
|
||||
break;
|
||||
case NEO_FORGED:
|
||||
content.addTag(i18n("install.installer.neoforge"));
|
||||
break;
|
||||
case FABRIC:
|
||||
content.addTag(i18n("install.installer.fabric"));
|
||||
break;
|
||||
case LITE_LOADER:
|
||||
content.addTag(i18n("install.installer.liteloader"));
|
||||
break;
|
||||
case QUILT:
|
||||
content.addTag(i18n("install.installer.quilt"));
|
||||
break;
|
||||
}
|
||||
if (dataItem.getMod() != null && I18n.isUseChinese()) {
|
||||
if (isNotBlank(dataItem.getSubtitle())) {
|
||||
content.setSubtitle(dataItem.getSubtitle() + ", " + dataItem.getMod().getDisplayName());
|
||||
} else {
|
||||
content.setSubtitle(dataItem.getMod().getDisplayName());
|
||||
}
|
||||
} else {
|
||||
content.setSubtitle(dataItem.getSubtitle());
|
||||
String modVersion = modInfo.getVersion();
|
||||
if (StringUtils.isNotBlank(modVersion) && !"${version}".equals(modVersion)) {
|
||||
content.addTag(modVersion);
|
||||
}
|
||||
|
||||
if (booleanProperty != null) {
|
||||
checkBox.selectedProperty().unbindBidirectional(booleanProperty);
|
||||
}
|
||||
checkBox.selectedProperty().bindBidirectional(booleanProperty = dataItem.active);
|
||||
restoreButton.setVisible(!dataItem.getModInfo().getMod().getOldFiles().isEmpty());
|
||||
restoreButton.setVisible(!modInfo.getMod().getOldFiles().isEmpty());
|
||||
restoreButton.setOnAction(e -> {
|
||||
menu.get().getContent().setAll(dataItem.getModInfo().getMod().getOldFiles().stream()
|
||||
menu.get().getContent().setAll(modInfo.getMod().getOldFiles().stream()
|
||||
.map(localModFile -> new IconedMenuItem(null, localModFile.getVersion(),
|
||||
() -> getSkinnable().rollback(dataItem.getModInfo(), localModFile),
|
||||
() -> getSkinnable().rollback(modInfo, localModFile),
|
||||
popup.get()))
|
||||
.collect(Collectors.toList())
|
||||
.toList()
|
||||
);
|
||||
|
||||
popup.get().show(restoreButton, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, 0, restoreButton.getHeight());
|
||||
});
|
||||
revealButton.setOnAction(e -> FXUtils.showFileInExplorer(dataItem.getModInfo().getFile()));
|
||||
revealButton.setOnAction(e -> FXUtils.showFileInExplorer(modInfo.getFile()));
|
||||
infoButton.setOnAction(e -> Controllers.dialog(new ModInfoDialog(dataItem)));
|
||||
|
||||
if (!warning.isEmpty()) {
|
||||
pseudoClassStateChanged(WARNING, true);
|
||||
|
||||
//noinspection ConstantValue
|
||||
this.warningTooltip = warning.size() == 1
|
||||
? new Tooltip(warning.get(0))
|
||||
: new Tooltip(String.join("\n", warning));
|
||||
FXUtils.installFastTooltip(this, warningTooltip);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,13 +20,13 @@ package org.jackhuang.hmcl.ui.versions;
|
||||
import org.jackhuang.hmcl.mod.RemoteModRepository;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
@@ -57,54 +57,220 @@ public enum ModTranslations {
|
||||
};
|
||||
|
||||
public static ModTranslations getTranslationsByRepositoryType(RemoteModRepository.Type type) {
|
||||
switch (type) {
|
||||
case MOD:
|
||||
return MOD;
|
||||
case MODPACK:
|
||||
return MODPACK;
|
||||
default:
|
||||
return EMPTY;
|
||||
return switch (type) {
|
||||
case MOD -> MOD;
|
||||
case MODPACK -> MODPACK;
|
||||
default -> EMPTY;
|
||||
};
|
||||
}
|
||||
|
||||
@SuppressWarnings("StatementWithEmptyBody")
|
||||
private static String cleanSubname(String subname) {
|
||||
if (StringUtils.isBlank(subname))
|
||||
return "";
|
||||
|
||||
StringBuilder builder = new StringBuilder(subname.length());
|
||||
for (int i = 0; i < subname.length(); ) {
|
||||
int ch = subname.codePointAt(i);
|
||||
if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9')
|
||||
|| ".+\\".indexOf(ch) >= 0) {
|
||||
builder.appendCodePoint(ch);
|
||||
} else if (Character.isWhitespace(ch)
|
||||
|| "':_-/&()[]{}|,!?~•".indexOf(ch) >= 0
|
||||
|| ch >= 0x1F300 && ch <= 0x1FAFF) {
|
||||
// Remove these unnecessary characters from subname
|
||||
} else {
|
||||
// The subname contains unsupported characters, so we do not use this subname to match the mod
|
||||
return "";
|
||||
}
|
||||
i += Character.charCount(ch);
|
||||
}
|
||||
return builder.length() == subname.length() ? subname : builder.toString();
|
||||
}
|
||||
|
||||
private final String resourceName;
|
||||
private List<Mod> mods;
|
||||
private Map<String, Mod> modIdMap; // mod id -> mod
|
||||
private Map<String, Mod> curseForgeMap; // curseforge id -> mod
|
||||
private List<Pair<String, Mod>> keywords;
|
||||
private int maxKeywordLength = -1;
|
||||
private volatile List<Mod> mods;
|
||||
private volatile Map<String, Mod> modIdMap; // mod id -> mod
|
||||
private volatile Map<String, Mod> subnameMap;
|
||||
private volatile Map<String, Mod> curseForgeMap; // curseforge id -> mod
|
||||
private volatile List<Pair<String, Mod>> keywords;
|
||||
private volatile int maxKeywordLength = -1;
|
||||
|
||||
ModTranslations(String resourceName) {
|
||||
this.resourceName = resourceName;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Mod getModByCurseForgeId(String id) {
|
||||
if (StringUtils.isBlank(id) || !loadCurseForgeMap()) return null;
|
||||
public @NotNull List<Mod> getMods() {
|
||||
List<Mod> mods = this.mods;
|
||||
if (mods != null)
|
||||
return mods;
|
||||
|
||||
return curseForgeMap.get(id);
|
||||
synchronized (this) {
|
||||
mods = this.mods;
|
||||
if (mods != null)
|
||||
return mods;
|
||||
|
||||
if (StringUtils.isBlank(resourceName)) {
|
||||
return this.mods = List.of();
|
||||
}
|
||||
|
||||
//noinspection DataFlowIssue
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(
|
||||
ModTranslations.class.getResourceAsStream(resourceName), StandardCharsets.UTF_8))) {
|
||||
return this.mods = reader.lines().filter(line -> !line.startsWith("#")).map(Mod::new).toList();
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to load " + resourceName, e);
|
||||
return this.mods = List.of();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private @NotNull Map<String, Mod> getModIdMap() {
|
||||
Map<String, Mod> modIdMap = this.modIdMap;
|
||||
if (modIdMap != null)
|
||||
return modIdMap;
|
||||
synchronized (this) {
|
||||
modIdMap = this.modIdMap;
|
||||
if (modIdMap != null)
|
||||
return modIdMap;
|
||||
|
||||
List<Mod> mods = getMods();
|
||||
modIdMap = new HashMap<>(mods.size());
|
||||
for (Mod mod : mods) {
|
||||
for (String id : mod.getModIds()) {
|
||||
if (StringUtils.isNotBlank(id)) {
|
||||
modIdMap.putIfAbsent(id, mod);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.modIdMap = modIdMap;
|
||||
}
|
||||
}
|
||||
|
||||
private @NotNull Map<String, Mod> getSubnameMap() {
|
||||
Map<String, Mod> subnameMap = this.subnameMap;
|
||||
if (subnameMap != null)
|
||||
return subnameMap;
|
||||
synchronized (this) {
|
||||
subnameMap = this.subnameMap;
|
||||
if (subnameMap != null)
|
||||
return subnameMap;
|
||||
|
||||
subnameMap = new HashMap<>();
|
||||
|
||||
List<Mod> mods = getMods();
|
||||
for (Mod mod : mods) {
|
||||
String subname = cleanSubname(mod.getSubname());
|
||||
if (StringUtils.isNotBlank(subname)) {
|
||||
subnameMap.putIfAbsent(subname, mod);
|
||||
}
|
||||
}
|
||||
|
||||
return this.subnameMap = subnameMap;
|
||||
}
|
||||
}
|
||||
|
||||
private @NotNull Map<String, Mod> getCurseForgeMap() {
|
||||
Map<String, Mod> curseForgeMap = this.curseForgeMap;
|
||||
if (curseForgeMap != null)
|
||||
return curseForgeMap;
|
||||
|
||||
synchronized (this) {
|
||||
curseForgeMap = this.curseForgeMap;
|
||||
if (curseForgeMap != null)
|
||||
return curseForgeMap;
|
||||
|
||||
List<Mod> mods = getMods();
|
||||
curseForgeMap = new HashMap<>(mods.size());
|
||||
for (Mod mod : mods) {
|
||||
if (StringUtils.isNotBlank(mod.getCurseforge())) {
|
||||
curseForgeMap.putIfAbsent(mod.getCurseforge(), mod);
|
||||
}
|
||||
}
|
||||
|
||||
return this.curseForgeMap = curseForgeMap;
|
||||
}
|
||||
}
|
||||
|
||||
private @NotNull List<Pair<String, Mod>> getKeywords() {
|
||||
List<Pair<String, Mod>> keywords = this.keywords;
|
||||
if (keywords != null)
|
||||
return keywords;
|
||||
|
||||
synchronized (this) {
|
||||
keywords = this.keywords;
|
||||
if (keywords != null)
|
||||
return keywords;
|
||||
|
||||
List<Mod> mods = getMods();
|
||||
|
||||
keywords = new ArrayList<>();
|
||||
int maxKeywordLength = -1;
|
||||
for (Mod mod : mods) {
|
||||
if (StringUtils.isNotBlank(mod.getName())) {
|
||||
keywords.add(pair(mod.getName(), mod));
|
||||
maxKeywordLength = Math.max(maxKeywordLength, mod.getName().length());
|
||||
}
|
||||
if (StringUtils.isNotBlank(mod.getSubname())) {
|
||||
keywords.add(pair(mod.getSubname(), mod));
|
||||
maxKeywordLength = Math.max(maxKeywordLength, mod.getSubname().length());
|
||||
}
|
||||
if (StringUtils.isNotBlank(mod.getAbbr())) {
|
||||
keywords.add(pair(mod.getAbbr(), mod));
|
||||
maxKeywordLength = Math.max(maxKeywordLength, mod.getAbbr().length());
|
||||
}
|
||||
}
|
||||
|
||||
this.maxKeywordLength = maxKeywordLength;
|
||||
return this.keywords = keywords;
|
||||
}
|
||||
}
|
||||
|
||||
private int getMaxKeywordLength() {
|
||||
int maxKeywordLength = this.maxKeywordLength;
|
||||
if (maxKeywordLength >= 0)
|
||||
return maxKeywordLength;
|
||||
|
||||
// Ensure maxKeywordLength is initialized
|
||||
getKeywords();
|
||||
return this.maxKeywordLength;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Mod getModById(String id) {
|
||||
if (StringUtils.isBlank(id) || !loadModIdMap()) return null;
|
||||
public Mod getModByCurseForgeId(String id) {
|
||||
if (StringUtils.isBlank(id)) return null;
|
||||
|
||||
return modIdMap.get(id);
|
||||
return getCurseForgeMap().get(id);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Mod getMod(String id, String subname) {
|
||||
subname = cleanSubname(subname);
|
||||
if (StringUtils.isNotBlank(subname)) {
|
||||
Mod mod = getSubnameMap().get(subname);
|
||||
if (mod != null && (StringUtils.isBlank(id) || mod.getModIds().contains(id)))
|
||||
return mod;
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(id))
|
||||
return getModIdMap().get(id);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public abstract String getMcmodUrl(Mod mod);
|
||||
|
||||
public List<Mod> searchMod(String query) {
|
||||
if (!loadKeywords()) return Collections.emptyList();
|
||||
|
||||
StringBuilder newQuery = ((CharSequence) query).chars()
|
||||
StringBuilder newQuery = query.chars()
|
||||
.filter(ch -> !Character.isSpaceChar(ch))
|
||||
.collect(StringBuilder::new, (sb, value) -> sb.append((char) value), StringBuilder::append);
|
||||
query = newQuery.toString();
|
||||
|
||||
StringUtils.LongestCommonSubsequence lcs = new StringUtils.LongestCommonSubsequence(query.length(), maxKeywordLength);
|
||||
StringUtils.LongestCommonSubsequence lcs = new StringUtils.LongestCommonSubsequence(query.length(), getMaxKeywordLength());
|
||||
List<Pair<Integer, Mod>> modList = new ArrayList<>();
|
||||
for (Pair<String, Mod> keyword : keywords) {
|
||||
for (Pair<String, Mod> keyword : getKeywords()) {
|
||||
int value = lcs.calc(query, keyword.getKey());
|
||||
if (value >= Math.max(1, query.length() - 3)) {
|
||||
modList.add(pair(value, keyword.getValue()));
|
||||
@@ -113,92 +279,7 @@ public enum ModTranslations {
|
||||
return modList.stream()
|
||||
.sorted((a, b) -> -a.getKey().compareTo(b.getKey()))
|
||||
.map(Pair::getValue)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private boolean loadFromResource() {
|
||||
if (mods != null) return true;
|
||||
if (StringUtils.isBlank(resourceName)) {
|
||||
mods = Collections.emptyList();
|
||||
return true;
|
||||
}
|
||||
|
||||
//noinspection DataFlowIssue
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(
|
||||
ModTranslations.class.getResourceAsStream(resourceName), StandardCharsets.UTF_8))) {
|
||||
mods = reader.lines().filter(line -> !line.startsWith("#")).map(Mod::new).collect(Collectors.toList());
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to load " + resourceName, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean loadCurseForgeMap() {
|
||||
if (curseForgeMap != null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mods == null) {
|
||||
if (!loadFromResource()) return false;
|
||||
}
|
||||
|
||||
curseForgeMap = new HashMap<>();
|
||||
for (Mod mod : mods) {
|
||||
if (StringUtils.isNotBlank(mod.getCurseforge())) {
|
||||
curseForgeMap.put(mod.getCurseforge(), mod);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean loadModIdMap() {
|
||||
if (modIdMap != null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mods == null) {
|
||||
if (!loadFromResource()) return false;
|
||||
}
|
||||
|
||||
modIdMap = new HashMap<>();
|
||||
for (Mod mod : mods) {
|
||||
for (String id : mod.getModIds()) {
|
||||
if (StringUtils.isNotBlank(id) && !"examplemod".equals(id)) {
|
||||
modIdMap.put(id, mod);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean loadKeywords() {
|
||||
if (keywords != null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mods == null) {
|
||||
if (!loadFromResource()) return false;
|
||||
}
|
||||
|
||||
keywords = new ArrayList<>();
|
||||
maxKeywordLength = -1;
|
||||
for (Mod mod : mods) {
|
||||
if (StringUtils.isNotBlank(mod.getName())) {
|
||||
keywords.add(pair(mod.getName(), mod));
|
||||
maxKeywordLength = Math.max(maxKeywordLength, mod.getName().length());
|
||||
}
|
||||
if (StringUtils.isNotBlank(mod.getSubname())) {
|
||||
keywords.add(pair(mod.getSubname(), mod));
|
||||
maxKeywordLength = Math.max(maxKeywordLength, mod.getSubname().length());
|
||||
}
|
||||
if (StringUtils.isNotBlank(mod.getAbbr())) {
|
||||
keywords.add(pair(mod.getAbbr(), mod));
|
||||
maxKeywordLength = Math.max(maxKeywordLength, mod.getAbbr().length());
|
||||
}
|
||||
}
|
||||
return true;
|
||||
.toList();
|
||||
}
|
||||
|
||||
public static final class Mod {
|
||||
@@ -217,7 +298,7 @@ public enum ModTranslations {
|
||||
|
||||
curseforge = items[0];
|
||||
mcmod = items[1];
|
||||
modIds = Collections.unmodifiableList(Arrays.asList(items[2].split(",")));
|
||||
modIds = List.of(items[2].split(","));
|
||||
name = items[3];
|
||||
subname = items[4];
|
||||
abbr = items[5];
|
||||
|
||||
@@ -45,8 +45,6 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
* @author Glavo
|
||||
*/
|
||||
public final class NativePatcher {
|
||||
private NativePatcher() {
|
||||
}
|
||||
|
||||
private static final Library NONEXISTENT_LIBRARY = new Library(null);
|
||||
|
||||
@@ -196,4 +194,84 @@ public final class NativePatcher {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static SupportStatus checkSupportedStatus(GameVersionNumber gameVersion, Platform platform,
|
||||
OSVersion systemVersion) {
|
||||
if (platform.equals(Platform.WINDOWS_X86_64)) {
|
||||
if (!systemVersion.isAtLeast(OSVersion.WINDOWS_10) && gameVersion.isAtLeast("1.20.5", "24w14a"))
|
||||
return SupportStatus.UNSUPPORTED;
|
||||
|
||||
return SupportStatus.OFFICIAL_SUPPORTED;
|
||||
}
|
||||
|
||||
if (platform.equals(Platform.MACOS_X86_64) || platform.equals(Platform.LINUX_X86_64))
|
||||
return SupportStatus.OFFICIAL_SUPPORTED;
|
||||
|
||||
if (platform.equals(Platform.WINDOWS_X86) || platform.equals(Platform.LINUX_X86)) {
|
||||
if (gameVersion.isAtLeast("1.20.5", "24w14a"))
|
||||
return SupportStatus.UNSUPPORTED;
|
||||
else
|
||||
return SupportStatus.OFFICIAL_SUPPORTED;
|
||||
}
|
||||
|
||||
if (platform.equals(Platform.WINDOWS_ARM64) || platform.equals(Platform.MACOS_ARM64)) {
|
||||
if (gameVersion.compareTo("1.19") >= 0)
|
||||
return SupportStatus.OFFICIAL_SUPPORTED;
|
||||
|
||||
String minVersion = platform.getOperatingSystem() == OperatingSystem.WINDOWS
|
||||
? "1.8"
|
||||
: "1.6";
|
||||
|
||||
return gameVersion.compareTo(minVersion) >= 0
|
||||
? SupportStatus.LAUNCHER_SUPPORTED
|
||||
: SupportStatus.TRANSLATION_SUPPORTED;
|
||||
}
|
||||
|
||||
String minVersion = null;
|
||||
String maxVersion = null;
|
||||
|
||||
if (platform.equals(Platform.FREEBSD_X86_64)) {
|
||||
minVersion = "1.13";
|
||||
} else if (platform.equals(Platform.LINUX_ARM64)) {
|
||||
minVersion = "1.6";
|
||||
} else if (platform.equals(Platform.LINUX_RISCV64)) {
|
||||
minVersion = "1.8";
|
||||
} else if (platform.equals(Platform.LINUX_LOONGARCH64)) {
|
||||
minVersion = "1.6";
|
||||
} else if (platform.equals(Platform.LINUX_LOONGARCH64_OW)) {
|
||||
minVersion = "1.6";
|
||||
maxVersion = "1.20.1";
|
||||
} else if (platform.equals(Platform.LINUX_MIPS64EL) || platform.equals(Platform.LINUX_ARM32)) {
|
||||
minVersion = "1.8";
|
||||
maxVersion = "1.20.1";
|
||||
}
|
||||
|
||||
if (minVersion != null) {
|
||||
if (gameVersion.compareTo(minVersion) >= 0) {
|
||||
if (maxVersion != null && gameVersion.compareTo(maxVersion) >= 0)
|
||||
return SupportStatus.UNSUPPORTED;
|
||||
|
||||
String[] defaultGameVersions = GameVersionNumber.getDefaultGameVersions();
|
||||
if (defaultGameVersions.length > 0 && gameVersion.compareTo(defaultGameVersions[0]) > 0) {
|
||||
return SupportStatus.UNTESTED;
|
||||
}
|
||||
return SupportStatus.LAUNCHER_SUPPORTED;
|
||||
} else {
|
||||
return SupportStatus.UNSUPPORTED;
|
||||
}
|
||||
}
|
||||
|
||||
return SupportStatus.UNTESTED;
|
||||
}
|
||||
|
||||
public enum SupportStatus {
|
||||
OFFICIAL_SUPPORTED,
|
||||
LAUNCHER_SUPPORTED,
|
||||
TRANSLATION_SUPPORTED,
|
||||
UNTESTED,
|
||||
UNSUPPORTED,
|
||||
}
|
||||
|
||||
private NativePatcher() {
|
||||
}
|
||||
}
|
||||
@@ -101,14 +101,14 @@ public final class SelfDependencyPatcher {
|
||||
|
||||
private static final class DependencyDescriptor {
|
||||
private static final String DEPENDENCIES_LIST_FILE = "/assets/openjfx-dependencies.json";
|
||||
private static final Path DEPENDENCIES_DIR_PATH = Metadata.DEPENDENCIES_DIRECTORY.resolve(Platform.getPlatform().toString()).resolve("openjfx");
|
||||
private static final Path DEPENDENCIES_DIR_PATH = Metadata.DEPENDENCIES_DIRECTORY.resolve(Platform.CURRENT_PLATFORM.toString()).resolve("openjfx");
|
||||
|
||||
static List<DependencyDescriptor> readDependencies() {
|
||||
//noinspection ConstantConditions
|
||||
try (Reader reader = new InputStreamReader(SelfDependencyPatcher.class.getResourceAsStream(DEPENDENCIES_LIST_FILE), UTF_8)) {
|
||||
Map<String, Map<String, List<DependencyDescriptor>>> allDependencies =
|
||||
JsonUtils.GSON.fromJson(reader, mapTypeOf(String.class, mapTypeOf(String.class, listTypeOf(DependencyDescriptor.class))));
|
||||
Map<String, List<DependencyDescriptor>> platform = allDependencies.get(Platform.getPlatform().toString());
|
||||
Map<String, List<DependencyDescriptor>> platform = allDependencies.get(Platform.CURRENT_PLATFORM.toString());
|
||||
if (platform == null)
|
||||
return null;
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ package org.jackhuang.hmcl.util.i18n;
|
||||
|
||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jackhuang.hmcl.util.i18n.translator.Translator;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -55,6 +55,10 @@ public final class I18n {
|
||||
return locale.getResourceBundle();
|
||||
}
|
||||
|
||||
public static Translator getTranslator() {
|
||||
return locale.getTranslator();
|
||||
}
|
||||
|
||||
public static String i18n(String key, Object... formatArgs) {
|
||||
return locale.i18n(key, formatArgs);
|
||||
}
|
||||
@@ -64,22 +68,11 @@ public final class I18n {
|
||||
}
|
||||
|
||||
public static String formatDateTime(TemporalAccessor time) {
|
||||
return locale.formatDateTime(time);
|
||||
return getTranslator().formatDateTime(time);
|
||||
}
|
||||
|
||||
public static String getDisplaySelfVersion(RemoteVersion version) {
|
||||
if (locale.getLocale().getLanguage().equals("lzh")) {
|
||||
if (version instanceof GameRemoteVersion)
|
||||
return WenyanUtils.translateGameVersion(GameVersionNumber.asGameVersion(version.getSelfVersion()));
|
||||
else
|
||||
return WenyanUtils.translateGenericVersion(version.getSelfVersion());
|
||||
}
|
||||
|
||||
if (LocaleUtils.isEnglish(locale.getLocale()) && "Qabs".equals(LocaleUtils.getScript(locale.getLocale()))) {
|
||||
return UpsideDownUtils.translate(version.getSelfVersion());
|
||||
}
|
||||
|
||||
return version.getSelfVersion();
|
||||
return getTranslator().getDisplayVersion(version);
|
||||
}
|
||||
|
||||
/// Find the builtin localized resource with given name and suffix.
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.util.i18n;
|
||||
|
||||
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
|
||||
import org.jackhuang.hmcl.util.i18n.translator.Translator_lzh;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
|
||||
import java.util.Locale;
|
||||
@@ -36,9 +37,9 @@ public final class MinecraftWiki {
|
||||
if (wikiVersion.startsWith("2.0"))
|
||||
translatedVersion = "二點〇";
|
||||
else if (wikiVersion.startsWith("1.0.0-rc2"))
|
||||
translatedVersion = WenyanUtils.translateGameVersion(GameVersionNumber.asGameVersion("1.0.0-rc2"));
|
||||
translatedVersion = Translator_lzh.translateGameVersion(GameVersionNumber.asGameVersion("1.0.0-rc2"));
|
||||
else
|
||||
translatedVersion = WenyanUtils.translateGameVersion(gameVersion);
|
||||
translatedVersion = Translator_lzh.translateGameVersion(gameVersion);
|
||||
|
||||
if (translatedVersion.equals(gameVersion.toString()) || gameVersion instanceof GameVersionNumber.Old) {
|
||||
return getWikiLink(SupportedLocale.getLocale(LocaleUtils.LOCALE_ZH_HANT), version);
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
* 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.i18n;
|
||||
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
@@ -24,12 +23,13 @@ import com.google.gson.stream.JsonToken;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.translator.Translator;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.TemporalAccessor;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
@@ -72,10 +72,12 @@ public final class SupportedLocale {
|
||||
private final boolean isDefault;
|
||||
private final String name;
|
||||
private final Locale locale;
|
||||
private final TextDirection textDirection;
|
||||
|
||||
private ResourceBundle resourceBundle;
|
||||
private ResourceBundle localeNamesBundle;
|
||||
private DateTimeFormatter dateTimeFormatter;
|
||||
private List<Locale> candidateLocales;
|
||||
private Translator translator;
|
||||
|
||||
SupportedLocale() {
|
||||
this.isDefault = true;
|
||||
@@ -85,12 +87,14 @@ public final class SupportedLocale {
|
||||
this.locale = StringUtils.isBlank(language)
|
||||
? LocaleUtils.SYSTEM_DEFAULT
|
||||
: Locale.forLanguageTag(language);
|
||||
this.textDirection = LocaleUtils.getTextDirection(locale);
|
||||
}
|
||||
|
||||
SupportedLocale(Locale locale) {
|
||||
this.isDefault = false;
|
||||
this.name = locale.toLanguageTag();
|
||||
this.locale = locale;
|
||||
this.textDirection = LocaleUtils.getTextDirection(locale);
|
||||
}
|
||||
|
||||
public boolean isDefault() {
|
||||
@@ -105,6 +109,10 @@ public final class SupportedLocale {
|
||||
return locale;
|
||||
}
|
||||
|
||||
public TextDirection getTextDirection() {
|
||||
return textDirection;
|
||||
}
|
||||
|
||||
public String getDisplayName(SupportedLocale inLocale) {
|
||||
if (isDefault()) {
|
||||
try {
|
||||
@@ -200,23 +208,6 @@ public final class SupportedLocale {
|
||||
}
|
||||
}
|
||||
|
||||
public String formatDateTime(TemporalAccessor time) {
|
||||
DateTimeFormatter formatter = dateTimeFormatter;
|
||||
if (formatter == null) {
|
||||
if (LocaleUtils.isEnglish(locale) && "Qabs".equals(locale.getScript())) {
|
||||
return UpsideDownUtils.formatDateTime(time);
|
||||
}
|
||||
|
||||
if (locale.getLanguage().equals("lzh")) {
|
||||
return WenyanUtils.formatDateTime(time);
|
||||
}
|
||||
|
||||
formatter = dateTimeFormatter = DateTimeFormatter.ofPattern(getResourceBundle().getString("datetime.format"))
|
||||
.withZone(ZoneId.systemDefault());
|
||||
}
|
||||
return formatter.format(time);
|
||||
}
|
||||
|
||||
public String getFcMatchPattern() {
|
||||
String language = locale.getLanguage();
|
||||
String region = locale.getCountry();
|
||||
@@ -252,6 +243,31 @@ public final class SupportedLocale {
|
||||
return region.isEmpty() ? language : language + "-" + region;
|
||||
}
|
||||
|
||||
public Translator getTranslator() {
|
||||
Translator translator = this.translator;
|
||||
if (translator != null)
|
||||
return translator;
|
||||
|
||||
List<Locale> candidateLocales = getCandidateLocales();
|
||||
|
||||
for (Locale candidateLocale : candidateLocales) {
|
||||
String className = DefaultResourceBundleControl.INSTANCE.toBundleName(Translator.class.getSimpleName(), candidateLocale);
|
||||
if (Translator.class.getResource(className + ".class") != null) {
|
||||
try {
|
||||
Class<?> clazz = Class.forName(Translator.class.getPackageName() + "." + className);
|
||||
|
||||
MethodHandle constructor = MethodHandles.publicLookup()
|
||||
.findConstructor(clazz, MethodType.methodType(void.class, SupportedLocale.class));
|
||||
|
||||
return this.translator = (Translator) constructor.invoke(this);
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to create instance for " + className, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.translator = new Translator(this);
|
||||
}
|
||||
|
||||
public boolean isSameLanguage(SupportedLocale other) {
|
||||
return LocaleUtils.getRootLanguage(this.getLocale())
|
||||
.equals(LocaleUtils.getRootLanguage(other.getLocale()));
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 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.i18n.translator;
|
||||
|
||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||
import org.jackhuang.hmcl.util.i18n.SupportedLocale;
|
||||
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.TemporalAccessor;
|
||||
import java.util.Locale;
|
||||
|
||||
/// @author Glavo
|
||||
public class Translator {
|
||||
protected final SupportedLocale supportedLocale;
|
||||
protected final Locale locale;
|
||||
|
||||
public Translator(SupportedLocale supportedLocale) {
|
||||
this.supportedLocale = supportedLocale;
|
||||
this.locale = supportedLocale.getLocale();
|
||||
}
|
||||
|
||||
public final SupportedLocale getSupportedLocale() {
|
||||
return supportedLocale;
|
||||
}
|
||||
|
||||
public final Locale getLocale() {
|
||||
return locale;
|
||||
}
|
||||
|
||||
public String getDisplayVersion(RemoteVersion remoteVersion) {
|
||||
return remoteVersion.getSelfVersion();
|
||||
}
|
||||
|
||||
/// @see [#formatDateTime(TemporalAccessor)]
|
||||
protected DateTimeFormatter dateTimeFormatter;
|
||||
|
||||
public String formatDateTime(TemporalAccessor time) {
|
||||
DateTimeFormatter formatter = dateTimeFormatter;
|
||||
if (formatter == null) {
|
||||
formatter = dateTimeFormatter = DateTimeFormatter.ofPattern(supportedLocale.getResourceBundle().getString("datetime.format"))
|
||||
.withZone(ZoneId.systemDefault());
|
||||
}
|
||||
return formatter.format(time);
|
||||
}
|
||||
|
||||
}
|
||||
+19
-9
@@ -15,7 +15,10 @@
|
||||
* 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.i18n;
|
||||
package org.jackhuang.hmcl.util.i18n.translator;
|
||||
|
||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||
import org.jackhuang.hmcl.util.i18n.SupportedLocale;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@@ -30,13 +33,16 @@ import java.util.Map;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/// @author Glavo
|
||||
public final class UpsideDownUtils {
|
||||
public class Translator_en_Qabs extends Translator {
|
||||
private static final DateTimeFormatter BASE_FORMATTER = DateTimeFormatter.ofPattern("MMM d, yyyy, h:mm:ss a")
|
||||
.withZone(ZoneId.systemDefault());
|
||||
|
||||
private static final Map<Integer, Integer> MAPPER = loadMap();
|
||||
|
||||
private static Map<Integer, Integer> loadMap() {
|
||||
var map = new LinkedHashMap<Integer, Integer>();
|
||||
|
||||
InputStream inputStream = UpsideDownUtils.class.getResourceAsStream("/assets/lang/upside_down.txt");
|
||||
InputStream inputStream = Translator_en_Qabs.class.getResourceAsStream("/assets/lang/upside_down.txt");
|
||||
if (inputStream != null) {
|
||||
try (inputStream) {
|
||||
new String(inputStream.readAllBytes(), StandardCharsets.UTF_8).lines().forEach(line -> {
|
||||
@@ -65,13 +71,17 @@ public final class UpsideDownUtils {
|
||||
return builder.reverse().toString();
|
||||
}
|
||||
|
||||
private static final DateTimeFormatter BASE_FORMATTER = DateTimeFormatter.ofPattern("MMM d, yyyy, h:mm:ss a")
|
||||
.withZone(ZoneId.systemDefault());
|
||||
public Translator_en_Qabs(SupportedLocale locale) {
|
||||
super(locale);
|
||||
}
|
||||
|
||||
public static String formatDateTime(TemporalAccessor time) {
|
||||
@Override
|
||||
public String getDisplayVersion(RemoteVersion remoteVersion) {
|
||||
return translate(remoteVersion.getSelfVersion());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String formatDateTime(TemporalAccessor time) {
|
||||
return translate(BASE_FORMATTER.format(time));
|
||||
}
|
||||
|
||||
private UpsideDownUtils() {
|
||||
}
|
||||
}
|
||||
+61
-68
@@ -15,8 +15,11 @@
|
||||
* 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.i18n;
|
||||
package org.jackhuang.hmcl.util.i18n.translator;
|
||||
|
||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
|
||||
import org.jackhuang.hmcl.util.i18n.SupportedLocale;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
|
||||
import java.time.Instant;
|
||||
@@ -27,10 +30,8 @@ import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public final class WenyanUtils {
|
||||
/// @author Glavo
|
||||
public class Translator_lzh extends Translator {
|
||||
private static final String DOT = "點";
|
||||
|
||||
private static final String[] NUMBERS = {
|
||||
@@ -79,37 +80,8 @@ public final class WenyanUtils {
|
||||
builder.append(hour % 2 == 0 ? '正' : '初');
|
||||
}
|
||||
|
||||
public static String formatDateTime(TemporalAccessor time) {
|
||||
LocalDateTime localDateTime;
|
||||
if (time instanceof Instant)
|
||||
localDateTime = ((Instant) time).atZone(ZoneId.systemDefault()).toLocalDateTime();
|
||||
else
|
||||
localDateTime = LocalDateTime.from(time);
|
||||
|
||||
StringBuilder builder = new StringBuilder(16);
|
||||
|
||||
appendYear(builder, localDateTime.getYear());
|
||||
builder.append('年');
|
||||
builder.append(numberToString(localDateTime.getMonthValue()));
|
||||
builder.append('月');
|
||||
builder.append(numberToString(localDateTime.getDayOfMonth()));
|
||||
builder.append('日');
|
||||
|
||||
builder.append(' ');
|
||||
|
||||
appendHour(builder, localDateTime.getHour());
|
||||
builder.append(numberToString(localDateTime.getMinute()));
|
||||
builder.append('分');
|
||||
builder.append(numberToString(localDateTime.getSecond()));
|
||||
builder.append('秒');
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
public static String translateGameVersion(GameVersionNumber gameVersion) {
|
||||
if (gameVersion instanceof GameVersionNumber.Release) {
|
||||
var release = (GameVersionNumber.Release) gameVersion;
|
||||
|
||||
if (gameVersion instanceof GameVersionNumber.Release release) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
appendDigitByDigit(builder, String.valueOf(release.getMajor()));
|
||||
builder.append(DOT);
|
||||
@@ -120,6 +92,7 @@ public final class WenyanUtils {
|
||||
appendDigitByDigit(builder, String.valueOf(release.getPatch()));
|
||||
}
|
||||
|
||||
//noinspection StatementWithEmptyBody
|
||||
if (release.getEaType() == GameVersionNumber.Release.TYPE_GA) {
|
||||
// do nothing
|
||||
} else if (release.getEaType() == GameVersionNumber.Release.TYPE_PRE) {
|
||||
@@ -134,9 +107,7 @@ public final class WenyanUtils {
|
||||
}
|
||||
|
||||
return builder.toString();
|
||||
} else if (gameVersion instanceof GameVersionNumber.Snapshot) {
|
||||
var snapshot = (GameVersionNumber.Snapshot) gameVersion;
|
||||
|
||||
} else if (gameVersion instanceof GameVersionNumber.Snapshot snapshot) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
appendDigitByDigit(builder, String.valueOf(snapshot.getYear()));
|
||||
@@ -152,35 +123,20 @@ public final class WenyanUtils {
|
||||
return builder.toString();
|
||||
} else if (gameVersion instanceof GameVersionNumber.Special) {
|
||||
String version = gameVersion.toString();
|
||||
switch (version.toLowerCase(Locale.ROOT)) {
|
||||
case "2.0":
|
||||
return "二點〇";
|
||||
case "2.0_blue":
|
||||
return "二點〇藍";
|
||||
case "2.0_red":
|
||||
return "二點〇赤";
|
||||
case "2.0_purple":
|
||||
return "二點〇紫";
|
||||
case "1.rv-pre1":
|
||||
return "一點真視之預一";
|
||||
case "3d shareware v1.34":
|
||||
return "躍然享件一點三四";
|
||||
case "20w14infinite":
|
||||
case "20w14~":
|
||||
case "20w14∞":
|
||||
return "二〇週一四宇";
|
||||
case "22w13oneblockatatime":
|
||||
return "二二週一三典";
|
||||
case "23w13a_or_b":
|
||||
return "二三週一三暨";
|
||||
case "24w14potato":
|
||||
return "二四週一四芋";
|
||||
case "25w14craftmine":
|
||||
return "二五週一四礦";
|
||||
default:
|
||||
return version;
|
||||
}
|
||||
|
||||
return switch (version.toLowerCase(Locale.ROOT)) {
|
||||
case "2.0" -> "二點〇";
|
||||
case "2.0_blue" -> "二點〇藍";
|
||||
case "2.0_red" -> "二點〇赤";
|
||||
case "2.0_purple" -> "二點〇紫";
|
||||
case "1.rv-pre1" -> "一點真視之預一";
|
||||
case "3d shareware v1.34" -> "躍然享件一點三四";
|
||||
case "20w14infinite", "20w14~", "20w14∞" -> "二〇週一四宇";
|
||||
case "22w13oneblockatatime" -> "二二週一三典";
|
||||
case "23w13a_or_b" -> "二三週一三暨";
|
||||
case "24w14potato" -> "二四週一四芋";
|
||||
case "25w14craftmine" -> "二五週一四礦";
|
||||
default -> version;
|
||||
};
|
||||
} else {
|
||||
return gameVersion.toString();
|
||||
}
|
||||
@@ -211,6 +167,43 @@ public final class WenyanUtils {
|
||||
return version;
|
||||
}
|
||||
|
||||
private WenyanUtils() {
|
||||
public Translator_lzh(SupportedLocale locale) {
|
||||
super(locale);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayVersion(RemoteVersion remoteVersion) {
|
||||
if (remoteVersion instanceof GameRemoteVersion)
|
||||
return translateGameVersion(GameVersionNumber.asGameVersion(remoteVersion.getSelfVersion()));
|
||||
else
|
||||
return translateGenericVersion(remoteVersion.getSelfVersion());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String formatDateTime(TemporalAccessor time) {
|
||||
LocalDateTime localDateTime;
|
||||
if (time instanceof Instant instant)
|
||||
localDateTime = instant.atZone(ZoneId.systemDefault()).toLocalDateTime();
|
||||
else
|
||||
localDateTime = LocalDateTime.from(time);
|
||||
|
||||
StringBuilder builder = new StringBuilder(16);
|
||||
|
||||
appendYear(builder, localDateTime.getYear());
|
||||
builder.append('年');
|
||||
builder.append(numberToString(localDateTime.getMonthValue()));
|
||||
builder.append('月');
|
||||
builder.append(numberToString(localDateTime.getDayOfMonth()));
|
||||
builder.append('日');
|
||||
|
||||
builder.append(' ');
|
||||
|
||||
appendHour(builder, localDateTime.getHour());
|
||||
builder.append(numberToString(localDateTime.getMinute()));
|
||||
builder.append('分');
|
||||
builder.append(numberToString(localDateTime.getSecond()));
|
||||
builder.append('秒');
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
@@ -293,6 +293,14 @@
|
||||
-fx-font-size: 12px;
|
||||
}
|
||||
|
||||
.two-line-list-item > .first-line > .tag-warning {
|
||||
-fx-text-fill: #d34336;;
|
||||
-fx-background-color: #f1aeb5;
|
||||
-fx-padding: 2;
|
||||
-fx-font-weight: normal;
|
||||
-fx-font-size: 12px;
|
||||
}
|
||||
|
||||
.two-line-item-second-large {
|
||||
|
||||
}
|
||||
@@ -918,6 +926,10 @@
|
||||
-fx-background-color: derive(-fx-base-color, 60%);
|
||||
}
|
||||
|
||||
.mod-info-list-cell:warning {
|
||||
-fx-background-color: #F8D7DA;
|
||||
}
|
||||
|
||||
.options-sublist {
|
||||
-fx-background-color: white;
|
||||
}
|
||||
|
||||
@@ -717,10 +717,10 @@ install.installer.game=Minecraft
|
||||
install.installer.incompatible=Incompatible with %s
|
||||
install.installer.install=Installing %s
|
||||
install.installer.install_offline=Install/Update from Local File
|
||||
install.installer.install_offline.extension=(Neo)Forge/OptiFine installer
|
||||
install.installer.install_offline.tooltip=We support using the local (Neo)Forge/OptiFine installer.
|
||||
install.installer.install_offline.extension=(Neo)Forge/Cleanroom/OptiFine installer
|
||||
install.installer.install_offline.tooltip=We support using the local (Neo)Forge, Cleanroom, and OptiFine installer.
|
||||
install.installer.install_online=Online Install
|
||||
install.installer.install_online.tooltip=We currently support Forge, NeoForge, OptiFine, Fabric, Quilt, and LiteLoader.
|
||||
install.installer.install_online.tooltip=We currently support Forge, NeoForge, Cleanroom, OptiFine, Fabric, Quilt, and LiteLoader.
|
||||
install.installer.liteloader=LiteLoader
|
||||
install.installer.not_installed=Not installed
|
||||
install.installer.optifine=OptiFine
|
||||
@@ -1090,8 +1090,10 @@ mods.not_modded=You must install a modloader (Forge, NeoForge, Fabric, Quilt, or
|
||||
mods.restore=Restore
|
||||
mods.url=Official Page
|
||||
mods.update_modpack_mod.warning=Updating mods in a modpack can lead to irreparable results, possibly corrupting the modpack so that it cannot launch. Are you sure you want to update?
|
||||
mods.warning.loader_mismatch=Mod loader mismatch
|
||||
mods.install=Install
|
||||
mods.save_as=Save As
|
||||
mods.unknown=Unknown Mod
|
||||
|
||||
nbt.entries=%s entries
|
||||
nbt.open.failed=Failed to open file
|
||||
@@ -1418,6 +1420,8 @@ terracotta.easytier=About EasyTier
|
||||
terracotta.terracotta=Terracotta | Multiplayer
|
||||
terracotta.status=Lobby
|
||||
terracotta.back=Exit
|
||||
terracotta.feedback.title=Fill Out Feedback Form
|
||||
terracotta.feedback.desc=As HMCL updates Multiplayer Core, we hope you can take 10 seconds to fill out the feedback form.
|
||||
terracotta.sudo_installing=HMCL must verify your password before installing Multiplayer Core
|
||||
terracotta.from_local.title=Third-party download channels for Multiplayer Core
|
||||
terracotta.from_local.desc=In some areas, the built-in default download channel may be unstable.
|
||||
@@ -1432,7 +1436,7 @@ terracotta.status.uninitialized.update=Multiplayer Core: Update Available
|
||||
terracotta.status.uninitialized.update.title=Update Multiplayer Core (~ 8MiB)
|
||||
terracotta.status.uninitialized.desc=You legally promise to strictly abide by all laws and regulations of your country or region during the multiplayer process.
|
||||
terracotta.confirm.title=User Notice
|
||||
terracotta.confirm.desc=Terracotta is a third-party open source free software, which has little relationship with HMCL.\n\
|
||||
terracotta.confirm.desc=Terracotta is a third-party open source free software. Please provide feedback on any issues you encounter through the relevant channels during use.\n\
|
||||
Terracotta is based on P2P, so that the final experience depends greatly on your network condition.\n\
|
||||
You promise to strictly abide by all laws and regulations in your country or region when playing online.
|
||||
terracotta.status.preparing=Multiplayer Core: Downloading (DO NOT exit HMCL)
|
||||
@@ -1513,7 +1517,8 @@ update.note=Beta and Nightly channels may have more features or fixes, but they
|
||||
update.latest=This is the latest version
|
||||
update.no_browser=Cannot open in system browser. But we copied the link to your clipboard, and you can open it manually.
|
||||
update.tooltip=Update
|
||||
update.preview=Try out the HMCL version
|
||||
update.preview=Preview HMCL releases early
|
||||
update.preview.tooltip=Enable this option to receive new versions of HMCL early for testing before their official release.
|
||||
|
||||
version=Games
|
||||
version.name=Instance Name
|
||||
@@ -1529,6 +1534,8 @@ version.game.release=Release
|
||||
version.game.releases=Releases
|
||||
version.game.snapshot=Snapshot
|
||||
version.game.snapshots=Snapshots
|
||||
version.game.support_status.unsupported=Unsupported
|
||||
version.game.support_status.untested=Untested
|
||||
version.game.type=Type
|
||||
version.launch=Launch Game
|
||||
version.launch.empty=Start Game
|
||||
|
||||
@@ -723,10 +723,10 @@ install.installer.game=Minecraft
|
||||
install.installer.incompatible=Incompatible con %s
|
||||
install.installer.install=Instalando %s
|
||||
install.installer.install_offline=Instalar/actualizar desde archivo local
|
||||
install.installer.install_offline.extension=Instalador de (Neo)Forge/OptiFine
|
||||
install.installer.install_offline.tooltip=Apoyamos el uso del instalador local de (Neo)Forge/OptiFine.
|
||||
install.installer.install_offline.extension=Instalador de (Neo)Forge/Cleanroom/OptiFine
|
||||
install.installer.install_offline.tooltip=Apoyamos el uso del instalador local de (Neo)Forge/Cleanroom/OptiFine.
|
||||
install.installer.install_online=Instalación en línea
|
||||
install.installer.install_online.tooltip=Actualmente soportamos Forge, NeoForge, OptiFine, Fabric, Quilt y LiteLoader.
|
||||
install.installer.install_online.tooltip=Actualmente soportamos Forge, NeoForge, Cleanroom, Fabric, Quilt, LiteLoader y OptiFine.
|
||||
install.installer.liteloader=LiteLoader
|
||||
install.installer.not_installed=No está instalado
|
||||
install.installer.optifine=OptiFine
|
||||
@@ -1444,6 +1444,8 @@ update.note=Advertencia: Las versiones beta y las versiones nocturnas pueden ten
|
||||
update.latest=Esta es la última versión.
|
||||
update.no_browser=No se puede abrir en el navegador del sistema. Pero, hemos copiado el enlace a su portapapeles y puede abrirlo manualmente.
|
||||
update.tooltip=Actualización
|
||||
update.preview=Vista previa de actualizaciones anticipadas
|
||||
update.preview.tooltip=Activa esta opción para recibir nuevas versiones del lanzador antes de su lanzamiento oficial para probarlas
|
||||
|
||||
version=Juegos
|
||||
version.name=Nombre de instancia
|
||||
|
||||
@@ -532,10 +532,10 @@ install.installer.game=礦藝
|
||||
install.installer.incompatible=與 %s 不兼也
|
||||
install.installer.install=裝 %s
|
||||
install.installer.install_offline=取私案而置
|
||||
install.installer.install_offline.extension= (新) 鍛/OptiFine 裝者
|
||||
install.installer.install_offline.tooltip=可錄 (新) 鍛/Optifine 裝者
|
||||
install.installer.install_offline.extension= (新) 鍛/Cleanroom/OptiFine 裝者
|
||||
install.installer.install_offline.tooltip=可錄 (新) 鍛/Cleanroom/OptiFine 裝者
|
||||
install.installer.install_online=網取而置
|
||||
install.installer.install_online.tooltip=可置鍛、新鍛、緞、褥、輕載與 Optifine
|
||||
install.installer.install_online.tooltip=可置鍛、新鍛、Cleanroom、緞、褥、輕載與 OptiFine
|
||||
install.installer.liteloader=輕載
|
||||
install.installer.not_installed=未裝
|
||||
install.installer.optifine=OptiFine
|
||||
|
||||
@@ -714,10 +714,10 @@ install.installer.game=Minecraft
|
||||
install.installer.incompatible=Несовместимо с %s
|
||||
install.installer.install=Установить %s
|
||||
install.installer.install_offline=Установить/обновить из локального файла
|
||||
install.installer.install_offline.extension=Установщик (Neo)Forge/OptiFine
|
||||
install.installer.install_offline.tooltip=Поддерживаются установщики (Neo)Forge/OptiFine.
|
||||
install.installer.install_offline.extension=Установщик (Neo)Forge/Cleanroom/OptiFine
|
||||
install.installer.install_offline.tooltip=Поддерживаются установщики (Neo)Forge/Cleanroom/OptiFine.
|
||||
install.installer.install_online=Установить с интернета
|
||||
install.installer.install_online.tooltip=Поддерживаются Forge, NeoForge, OptiFine, Fabric, Quilt и LiteLoader.
|
||||
install.installer.install_online.tooltip=Поддерживаются Forge, NeoForge, Cleanroom, Fabric, Quilt, LiteLoader и OptiFine.
|
||||
install.installer.liteloader=LiteLoader
|
||||
install.installer.not_installed=Не установлено
|
||||
install.installer.optifine=OptiFine
|
||||
|
||||
@@ -659,10 +659,10 @@ install.installer.game=Minecraft
|
||||
install.installer.incompatible=Несумісний з %s
|
||||
install.installer.install=Встановлення %s
|
||||
install.installer.install_offline=Встановити/Оновити з локального файлу
|
||||
install.installer.install_offline.extension=Інсталятор (Neo)Forge/OptiFine
|
||||
install.installer.install_offline.tooltip=Ми підтримуємо використання локального інсталятора (Neo)Forge/OptiFine.
|
||||
install.installer.install_offline.extension=Інсталятор (Neo)Forge/Cleanroom/OptiFine
|
||||
install.installer.install_offline.tooltip=Ми підтримуємо використання локального інсталятора (Neo)Forge/Cleanroom/OptiFine.
|
||||
install.installer.install_online=Онлайн встановлення
|
||||
install.installer.install_online.tooltip=Наразі ми підтримуємо Forge, NeoForge, OptiFine, Fabric, Quilt та LiteLoader.
|
||||
install.installer.install_online.tooltip=Наразі ми підтримуємо Forge, NeoForge, Cleanroom, OptiFine, Fabric, Quilt та LiteLoader.
|
||||
install.installer.liteloader=LiteLoader
|
||||
install.installer.not_installed=Не встановлено
|
||||
install.installer.optifine=OptiFine
|
||||
|
||||
@@ -524,10 +524,10 @@ install.installer.game=Minecraft
|
||||
install.installer.incompatible=與 %s 不相容
|
||||
install.installer.install=安裝 %s
|
||||
install.installer.install_offline=從本機檔案安裝或升級
|
||||
install.installer.install_offline.extension=(Neo)Forge/OptiFine 安裝器
|
||||
install.installer.install_offline.tooltip=支援匯入已經下載好的 (Neo)Forge/OptiFine 安裝器
|
||||
install.installer.install_offline.extension=(Neo)Forge/Cleanroom/OptiFine 安裝器
|
||||
install.installer.install_offline.tooltip=支援匯入已經下載好的 (Neo)Forge、Cleanroom 及 OptiFine 安裝器
|
||||
install.installer.install_online=線上安裝
|
||||
install.installer.install_online.tooltip=支援安裝 Forge、NeoForge、Fabric、Quilt、LiteLoader 和 OptiFine
|
||||
install.installer.install_online.tooltip=支援安裝 Forge、NeoForge、Cleanroom、Fabric、Quilt、LiteLoader 和 OptiFine
|
||||
install.installer.liteloader=LiteLoader
|
||||
install.installer.not_installed=未安裝
|
||||
install.installer.optifine=OptiFine
|
||||
@@ -538,7 +538,7 @@ install.installer.external_version=%s [由外部安裝的版本,無法解除
|
||||
install.installing=安裝
|
||||
install.modpack=安裝模組包
|
||||
install.modpack.installation=安裝模組包
|
||||
install.name.invalid=名稱中包含特殊字元(如 Emoji 表情或中文字元)。\n建議修改名稱。名稱建議僅包含英文字母、數字和底線,以防啟動遊戲時出現問題。是否繼續安裝?
|
||||
install.name.invalid=名稱中包含特殊字元 (如 Emoji 表情或中文字元)。\n建議修改名稱。名稱建議僅包含英文字母、數字和底線,以防啟動遊戲時出現問題。是否繼續安裝?
|
||||
install.new_game=安裝新實例
|
||||
install.new_game.already_exists=此實例已經存在,請重新命名
|
||||
install.new_game.current_game_version=目前遊戲實例
|
||||
@@ -748,7 +748,7 @@ modpack.wizard.step.3.title=選取模組包匯出類型
|
||||
modpack.wizard.step.initialization.exported_version=要匯出的遊戲實例
|
||||
modpack.wizard.step.initialization.force_update=強制升級模組包至最新版本 (需要自建伺服器)
|
||||
modpack.wizard.step.initialization.include_launcher=包含啟動器
|
||||
modpack.wizard.step.initialization.modrinth.info=在模組包建立過程中,啟動器將匹配 CurseForge/Modrinth 遠端資源替代本機檔案(包括模組、資源包和光影包)以縮減模組包大小,並將副檔名為「.disabled」的檔案標註為「安裝時可選項」。
|
||||
modpack.wizard.step.initialization.modrinth.info=在模組包建立過程中,啟動器將匹配 CurseForge/Modrinth 遠端資源替代本機檔案 (包括模組、資源包和光影包) 以縮減模組包大小,並將副檔名為「.disabled」的檔案標註為「安裝時可選項」。
|
||||
modpack.wizard.step.initialization.no_create_remote_files=不匹配遠端檔案
|
||||
modpack.wizard.step.initialization.save=選取要匯出到的遊戲模組包位置
|
||||
modpack.wizard.step.initialization.skip_curseforge_remote_files=不匹配 CurseForge 遠端資源
|
||||
@@ -887,8 +887,10 @@ mods.not_modded=你需要先在「自動安裝」頁面安裝 Forge、NeoForge
|
||||
mods.restore=回退
|
||||
mods.url=官方頁面
|
||||
mods.update_modpack_mod.warning=更新模組包中的模組可能導致模組包損壞,使模組包無法正常啟動。該操作不可逆,確定要更新嗎?
|
||||
mods.warning.loader_mismatch=模組載入器不匹配
|
||||
mods.install=安裝到目前實例
|
||||
mods.save_as=下載到本機目錄
|
||||
mods.unknown=未知模組
|
||||
|
||||
nbt.entries=%s 個條目
|
||||
nbt.open.failed=開啟檔案失敗
|
||||
@@ -1206,6 +1208,8 @@ terracotta.easytier=關於 EasyTier
|
||||
terracotta.terracotta=Terracotta | 陶瓦聯機
|
||||
terracotta.status=聯機大廳
|
||||
terracotta.back=退出
|
||||
terracotta.feedback.title=填寫回饋表
|
||||
terracotta.feedback.desc=在 HMCL 更新聯機核心時,我們歡迎您用 10 秒時間填寫聯機品質回饋收集表。
|
||||
terracotta.sudo_installing=HMCL 需要驗證您的密碼才能安裝線上核心
|
||||
terracotta.from_local.title=線上核心第三方下載管道
|
||||
terracotta.from_local.desc=在部分地區,內建的預設下載管道可能不穩定或連線緩慢
|
||||
@@ -1215,15 +1219,15 @@ terracotta.export_log=匯出線上核心日誌
|
||||
terracotta.export_log.desc=為分析錯誤提供更多信息
|
||||
terracotta.status.bootstrap=正在收集資訊
|
||||
terracotta.status.uninitialized.not_exist=未下載聯機核心
|
||||
terracotta.status.uninitialized.not_exist.title=下載聯機核心(約 8MiB)
|
||||
terracotta.status.uninitialized.not_exist.title=下載聯機核心 (約 8MiB)
|
||||
terracotta.status.uninitialized.update=需更新聯機核心
|
||||
terracotta.status.uninitialized.update.title=更新聯機核心(約 8MiB)
|
||||
terracotta.status.uninitialized.update.title=更新聯機核心 (約 8MiB)
|
||||
terracotta.status.uninitialized.desc=您承諾,在多人聯機全過程中,您將嚴格遵守您所在國家或地區的全部法律法規
|
||||
terracotta.confirm.title=使用者須知
|
||||
terracotta.confirm.desc=陶瓦聯機是第三方開源自由軟體,與 HMCL 無強關聯性。\n\
|
||||
terracotta.confirm.desc=陶瓦聯機是第三方開源自由軟體,您在使用過程中所遇到的問題請通過相關管道進行回饋。\n\
|
||||
多人連線基於 p2p,最終線上體驗和您的網路情況有較大關係。\n\
|
||||
您承諾,在多人連線全過程中,您將嚴格遵守您所在國家或地區的全部法律法規。
|
||||
terracotta.status.preparing=正在下載聯機核心(請勿退出啟動器)
|
||||
terracotta.status.preparing=正在下載聯機核心 (請勿退出啟動器)
|
||||
terracotta.status.launching=正在初始化聯機核心
|
||||
terracotta.status.unknown=正在初始化聯機核心
|
||||
terracotta.status.waiting=聯機核心已就緒
|
||||
@@ -1242,7 +1246,7 @@ terracotta.status.scanning.back=這將同時停止掃描區域網路世界。
|
||||
terracotta.status.host_starting=正在建立房間
|
||||
terracotta.status.host_starting.back=這將會取消建立房間。
|
||||
terracotta.status.host_ok=已建立房間
|
||||
terracotta.status.host_ok.code=邀請碼(已自動複製到剪貼簿)
|
||||
terracotta.status.host_ok.code=邀請碼 (已自動複製到剪貼簿)
|
||||
terracotta.status.host_ok.code.copy=複製邀請碼
|
||||
terracotta.status.host_ok.code.desc=請提醒您的朋友在 HMCL 或 PCL CE 多人遊戲功能中選擇房客模式,並輸入該邀請碼。
|
||||
terracotta.status.host_ok.back=這將同時徹底關閉房間,其他房客將退出並不再能重新加入該房間。
|
||||
@@ -1297,7 +1301,8 @@ update.note=開發版與預覽版包含更多的功能以及錯誤修復,但
|
||||
update.latest=目前版本為最新版本
|
||||
update.no_browser=無法開啟瀏覽器。網址已經複製到剪貼簿了,你可以手動複製網址開啟頁面。
|
||||
update.tooltip=更新
|
||||
update.preview=提前預覽 HMCL 版本
|
||||
update.preview=提前測試 HMCL 預覽版本
|
||||
update.preview.tooltip=啟用此選項,你將可以提前取得 HMCL 的新版本,以便在正式發布前進行測試。
|
||||
|
||||
version=遊戲
|
||||
version.name=遊戲實例名稱
|
||||
@@ -1313,6 +1318,8 @@ version.game.release=正式版
|
||||
version.game.releases=正式版
|
||||
version.game.snapshot=快照
|
||||
version.game.snapshots=快照
|
||||
version.game.support_status.unsupported=不支援
|
||||
version.game.support_status.untested=未經測試
|
||||
version.game.type=版本類型
|
||||
version.launch=啟動遊戲
|
||||
version.launch.empty=開始遊戲
|
||||
|
||||
@@ -534,10 +534,10 @@ install.installer.game=Minecraft
|
||||
install.installer.incompatible=与 %s 不兼容
|
||||
install.installer.install=安装 %s
|
||||
install.installer.install_offline=从本地文件安装/升级
|
||||
install.installer.install_offline.extension=(Neo)Forge/OptiFine 安装器
|
||||
install.installer.install_offline.tooltip=支持导入已经下载好的 (Neo)Forge/OptiFine 安装器
|
||||
install.installer.install_offline.extension=(Neo)Forge/Cleanroom/OptiFine 安装器
|
||||
install.installer.install_offline.tooltip=支持导入已经下载好的 (Neo)Forge、Cleanroom 和 OptiFine 安装器
|
||||
install.installer.install_online=在线安装
|
||||
install.installer.install_online.tooltip=支持安装 Forge、NeoForge、Fabric、Quilt、LiteLoader 和 OptiFine
|
||||
install.installer.install_online.tooltip=支持安装 Forge、NeoForge、Cleanroom、Fabric、Quilt、LiteLoader 和 OptiFine
|
||||
install.installer.liteloader=LiteLoader
|
||||
install.installer.not_installed=未安装
|
||||
install.installer.optifine=OptiFine
|
||||
@@ -548,7 +548,7 @@ install.installer.external_version=%s (由外部安装的版本,无法卸载
|
||||
install.installing=安装
|
||||
install.modpack=安装整合包
|
||||
install.modpack.installation=安装整合包
|
||||
install.name.invalid=名称中包含特殊字符(如 Emoji 表情或中文字符)。\n建议修改名称。名称建议仅包含英文字母、数字和下划线,以防启动游戏时出现问题。是否继续安装?
|
||||
install.name.invalid=名称中包含特殊字符 (如 Emoji 表情或中文字符)。\n建议修改名称。名称建议仅包含英文字母、数字和下划线,以防启动游戏时出现问题。是否继续安装?
|
||||
install.new_game=安装新游戏
|
||||
install.new_game.already_exists=此实例已经存在,请换一个名字
|
||||
install.new_game.current_game_version=当前游戏实例
|
||||
@@ -758,7 +758,7 @@ modpack.wizard.step.3.title=选择整合包导出类型
|
||||
modpack.wizard.step.initialization.exported_version=要导出的游戏实例
|
||||
modpack.wizard.step.initialization.force_update=强制升级整合包至最新版本 (需要自建服务器)
|
||||
modpack.wizard.step.initialization.include_launcher=包含启动器
|
||||
modpack.wizard.step.initialization.modrinth.info=在整合包创建过程中,启动器将匹配 CurseForge/Modrinth 远程资源替代本地文件(包括模组、资源包和光影包)以缩减整合包体积,并将扩展名为“.disabled”的文件标注为“安装时可选项”。
|
||||
modpack.wizard.step.initialization.modrinth.info=在整合包创建过程中,启动器将匹配 CurseForge/Modrinth 远程资源替代本地文件 (包括模组、资源包和光影包) 以缩减整合包体积,并将扩展名为“.disabled”的文件标注为“安装时可选项”。
|
||||
modpack.wizard.step.initialization.no_create_remote_files=不匹配远程文件
|
||||
modpack.wizard.step.initialization.save=选择要导出到的游戏整合包位置
|
||||
modpack.wizard.step.initialization.skip_curseforge_remote_files=不匹配 CurseForge 远程资源
|
||||
@@ -897,8 +897,10 @@ mods.not_modded=你需要先在“自动安装”页面安装 Forge、NeoForge
|
||||
mods.restore=回退
|
||||
mods.url=官方页面
|
||||
mods.update_modpack_mod.warning=更新整合包中的模组可能导致整合包损坏,使整合包无法正常启动。该操作不可逆,确定要更新吗?
|
||||
mods.warning.loader_mismatch=模组加载器不匹配
|
||||
mods.install=安装到当前实例
|
||||
mods.save_as=下载到本地文件夹
|
||||
mods.unknown=未知模组
|
||||
|
||||
nbt.entries=%s 个条目
|
||||
nbt.open.failed=打开文件失败
|
||||
@@ -1216,6 +1218,8 @@ terracotta.easytier=关于 EasyTier
|
||||
terracotta.terracotta=Terracotta | 陶瓦联机
|
||||
terracotta.status=联机大厅
|
||||
terracotta.back=退出
|
||||
terracotta.feedback.title=填写反馈表
|
||||
terracotta.feedback.desc=在 HMCL 更新联机核心时,我们欢迎您用 10 秒时间填写联机质量反馈收集表。
|
||||
terracotta.sudo_installing=HMCL 需要验证您的密码才能安装联机核心
|
||||
terracotta.from_local.title=联机核心第三方下载渠道
|
||||
terracotta.from_local.desc=在部分地区,HMCL 内置的默认下载渠道可能不稳定或连接缓慢
|
||||
@@ -1225,15 +1229,15 @@ terracotta.export_log=导出联机核心日志
|
||||
terracotta.export_log.desc=为分析错误提供更多信息
|
||||
terracotta.status.bootstrap=正在收集信息
|
||||
terracotta.status.uninitialized.not_exist=未下载联机核心
|
||||
terracotta.status.uninitialized.not_exist.title=下载联机核心(约 8MiB)
|
||||
terracotta.status.uninitialized.not_exist.title=下载联机核心 (约 8MiB)
|
||||
terracotta.status.uninitialized.update=需更新联机核心
|
||||
terracotta.status.uninitialized.update.title=更新联机核心(约 8MiB)
|
||||
terracotta.status.uninitialized.update.title=更新联机核心 (约 8MiB)
|
||||
terracotta.status.uninitialized.desc=您承诺,在多人联机全过程中,您将严格遵守您所在国家或地区的全部法律法规
|
||||
terracotta.confirm.title=用户须知
|
||||
terracotta.confirm.desc=陶瓦联机是第三方开源自由软件,与 HMCL 无强关联性。\n\
|
||||
terracotta.confirm.desc=陶瓦联机是第三方开源自由软件,您在使用过程中所遇到的问题请通过相关渠道进行反馈。\n\
|
||||
多人联机基于 p2p,最终联机体验和您的网络情况有较大关系。\n\
|
||||
在多人联机全过程中,您将严格遵守您所在国家或地区的全部法律法规。
|
||||
terracotta.status.preparing=正在下载联机核心(请勿退出启动器)
|
||||
terracotta.status.preparing=正在下载联机核心 (请勿退出启动器)
|
||||
terracotta.status.launching=正在初始化联机核心
|
||||
terracotta.status.unknown=正在初始化联机核心
|
||||
terracotta.status.waiting=联机核心已就绪
|
||||
@@ -1252,7 +1256,7 @@ terracotta.status.scanning.back=这将同时停止扫描局域网世界。
|
||||
terracotta.status.host_starting=正在启动房间
|
||||
terracotta.status.host_starting.back=这将会取消创建房间。
|
||||
terracotta.status.host_ok=已启动房间
|
||||
terracotta.status.host_ok.code=邀请码(已自动复制到剪贴板)
|
||||
terracotta.status.host_ok.code=邀请码 (已自动复制到剪贴板)
|
||||
terracotta.status.host_ok.code.copy=复制邀请码
|
||||
terracotta.status.host_ok.code.desc=请提醒您的朋友在 HMCL 或 PCL CE 多人联机功能中选择房客模式,并输入该邀请码。
|
||||
terracotta.status.host_ok.back=这将同时彻底关闭房间,其他房客将退出并不再能重新加入该房间。
|
||||
@@ -1308,6 +1312,7 @@ update.latest=当前版本为最新版本
|
||||
update.no_browser=无法打开浏览器。网址已经复制到剪贴板,你可以手动粘贴网址打开页面。
|
||||
update.tooltip=更新
|
||||
update.preview=提前预览 HMCL 版本
|
||||
update.preview.tooltip=启用此选项,你将可以提前获取 HMCL 的新版本,以便在正式发布前进行测试。
|
||||
|
||||
version=游戏
|
||||
version.name=游戏实例名称
|
||||
@@ -1323,6 +1328,8 @@ version.game.release=正式版
|
||||
version.game.releases=正式版
|
||||
version.game.snapshot=快照
|
||||
version.game.snapshots=快照
|
||||
version.game.support_status.unsupported=不支持
|
||||
version.game.support_status.untested=未经测试
|
||||
version.game.type=版本类型
|
||||
version.launch=启动游戏
|
||||
version.launch.empty=开始游戏
|
||||
|
||||
@@ -100,7 +100,7 @@ craftguide;106;craftguide;G键合成表;CraftGuide;
|
||||
;107;mobdictionary;生物图鉴;Mob Dictionary;
|
||||
biomes-o-plenty;108;biomesoplenty;超多生物群系;Biomes O' Plenty;BOP
|
||||
;109;BambooMod;竹;Bamboo;
|
||||
;110;tdmg;立体机动装置(Ibemu版);立体機動装置MOD / Three-Dimensional Maneuver Gear;TDMG
|
||||
;110;tdmg;立体机动装置 (Ibemu版);立体機動装置MOD / Three-Dimensional Maneuver Gear;TDMG
|
||||
enhanced-portals-3;111;enhancedportals;增强传送门3;Enhanced Portals 3;
|
||||
compact-solars;112;compactsolars,CompactSolars;压缩太阳能阵列;CompactSolars / Compact Solar Arrays;
|
||||
bibliocraft;113;BiblioCraft,bibliocraft;收藏馆;BiblioCraft;
|
||||
@@ -662,7 +662,7 @@ compact-machines;712;compactmachines3,cm2,compactmachines,CompactMachines;更多
|
||||
ex-nihilo-omnia;714;exnihiloomnia;无中生有-世间万物;Ex Nihilo Omnia;
|
||||
last-sword-you-will-ever-need-mo;715;theonlychaosmodforyou;最终之剑;The Last Sword You Will Ever Need Mod;
|
||||
the-last-sword-you-will-ever-need-reboot;716;tlsywen;最终之剑:重生;The Last Sword You Will Ever Need Reboot;
|
||||
ftb-library-legacy-forge;718;ftblib,FTBL,ftbl;FTB Library(旧版);FTB Library (Forge) (Legacy);FTBL
|
||||
ftb-library-legacy-forge;718;ftblib,FTBL,ftbl;FTB Library (旧版);FTB Library (Forge) (Legacy);FTBL
|
||||
;719;InventoryScan;库存扫描;InventoryScan;IS
|
||||
;720;godzillamod;哥斯拉;Godzilla;
|
||||
thaumic-potatoes-2;721;thaumicpotatoes;神秘土豆2;Thaumic Potatoes 2;TP2
|
||||
@@ -684,7 +684,7 @@ aether-aspects;737;AetherAspects;天境要素扫描;Aether Aspects;
|
||||
arcane-arteries;738;arcanearteries;奥法献祭学;Arcane Arteries;
|
||||
thaumic-palmistry;739;thaumicpalmistry;Thaumic Palmistry;;
|
||||
one-click-crafting;740;oneclickcrafting;一键合成;One click crafting;OCC
|
||||
essential-thaumaturgy-unofficial;741;;源质神秘学(非官方版);Essential Thaumaturgy Unofficial;
|
||||
essential-thaumaturgy-unofficial;741;;源质神秘学 (非官方版);Essential Thaumaturgy Unofficial;
|
||||
wireless-crafting-terminal;742;wct,ae2wct;无线合成终端;Wireless Crafting Terminal;WCT
|
||||
waila-plugins;743;wailaplugins;Waila 插件;Waila Plugins;WP
|
||||
appleskin;744;appleskin;苹果皮;AppleSkin;AS
|
||||
@@ -704,7 +704,7 @@ hammer-lib;756;hammercore,hammerlib;锤子核心;Hammer Core/Hammer Lib;
|
||||
thermal-casting;758;ThermalCasting;热力铸造室;Thermal Casting;
|
||||
thermal-smeltery-redux;759;;热力冶炼厂重置版;Thermal Smeltery Redux;
|
||||
brcore;760;BRCore;BRCore;;
|
||||
industrial-expansion-te-addon;761;industrialexpansion;工业扩充(TE附属);Industrial Expansion [TE Addon];
|
||||
industrial-expansion-te-addon;761;industrialexpansion;工业扩充 (TE附属);Industrial Expansion [TE Addon];
|
||||
;762;gtveinlocator,detravscannermod;格雷科技矿脉定位器;GTVeinLocator;
|
||||
;763;gtneioreplugin;格雷矿物NEI插件;GTNEIOrePlugin;GTNOP
|
||||
spmodapi;764;spmodapi;SpmodAPI;;
|
||||
@@ -2219,7 +2219,7 @@ lollipop;2328;lollipop;棒棒糖;Lollipop;
|
||||
enemyz;2329;enemyz;敌意显示;Enemyz;
|
||||
thaumic-restoration;2330;thaumicrestoration;神秘复辟;Thaumic Restoration;
|
||||
;2331;vimi;连锁矿工数据包;Vein Miner Datapack;
|
||||
;2332;;快速落叶(伪);Fast Leaf Decay;
|
||||
;2332;;快速落叶 (伪);Fast Leaf Decay;
|
||||
primal-boat;2333;primalboat;原始船舶;Primal Boat;
|
||||
eyes-in-the-darkness;2334;eyesinthedarkness;Eyes in the Darkness;;
|
||||
thaumic-augmentation;2335;thaumicaugmentation;神秘进阶;Thaumic Augmentation;TA
|
||||
@@ -2910,7 +2910,7 @@ riddle-chests;3041;riddlechests;谜题箱;Riddle Chests;
|
||||
;3042;cinfinitelib;CinfiniteLib;;CiL
|
||||
totem-plus;3043;;图腾增强;Totem Plus;
|
||||
blockmeterfabric;3044;blockmeter;方块计量表;Block Meter Fabric;
|
||||
;3045;kamenrider3dmod;假面骑士(3D版);KamenRider3D;KR3D
|
||||
;3045;kamenrider3dmod;假面骑士 (3D版);KamenRider3D;KR3D
|
||||
fluid-drawers;3046;fluiddrawers;储液抽屉;Fluid Drawers;
|
||||
creepypastacraft-reborn;3047;creepypastacraft;蠕动意面重置版;CreepyPastaCraft Reborn;
|
||||
itemzoom;3048;itemzoom;物品缩放;ItemZoom;
|
||||
@@ -3324,7 +3324,7 @@ eidolon;3469;eidolon;幻梦;Eidolon;
|
||||
rats-ratlantis;3470;ratlantis;老鼠:鼠西洲;Rats: Ratlantis;
|
||||
wthit;3471;wthit;What The Hell Is That;;WTHIT
|
||||
modern-industrialization;3472;modern_industrialization;现代工业化;Modern Industrialization;MI
|
||||
chunk-pregenerator-fabric;3473;chunkpregen;区块预生成器(Fabric);Fabric Chunk Pregenerator;
|
||||
chunk-pregenerator-fabric;3473;chunkpregen;区块预生成器 (Fabric);Fabric Chunk Pregenerator;
|
||||
jumploader;3474;jumploader;Jumploader;;
|
||||
unloader;3475;unloader;Unloader;;
|
||||
better-animal-models;3476;betteranimals;更真实的动物;Better animal models;
|
||||
@@ -3920,7 +3920,7 @@ gender;4082;gender;Gender & Age Mod;;
|
||||
;4083;mtm;Magical Talismans Mod;;
|
||||
ticktock;4084;servertick;Server Tick;;
|
||||
;4085;synthesis_formula_added;行尸走肉弹夹合成配方添加;;
|
||||
fps-reducre-for-fabric;4086;;FPS减速器(Fabric);FPS Reducer (For Fabric);
|
||||
fps-reducre-for-fabric;4086;;FPS减速器 (Fabric);FPS Reducer (For Fabric);
|
||||
art-artillery;4087;art_artillery,aa2;艺术?火炮!;Art? Artillery!;AA
|
||||
minemenufabric;4088;modmenu;我的菜单Fabric版;MineMenuFabric;
|
||||
;4089;tabtps-fabric;TabTPS;;
|
||||
@@ -4061,7 +4061,7 @@ firmalife;4226;firmalife;FirmaLife;;
|
||||
supermartijn642s-core-lib;4227;supermartijn642corelib;SuperMartijn642's Core Lib;;
|
||||
cherished-worlds;4228;cherishedworlds;存档置顶;Cherished Worlds;
|
||||
ironfist;4229;IronFist;铁拳;Ironfist;
|
||||
;4230;mining_dimension_cave_version;挖矿维度(矿洞版);Mining dimension(cave version);
|
||||
;4230;mining_dimension_cave_version;挖矿维度 (矿洞版);Mining dimension(cave version);
|
||||
simply-acceleration;4231;simplyacceleration;简单加速;Simply Acceleration;
|
||||
creeper-habitats;4232;creeperhabitats;Creeper Habitats;;
|
||||
husbandry;4233;husbandry;Husbandry;;
|
||||
@@ -4269,9 +4269,9 @@ dungeons-mobs;4436;dungeons_mobs;地下城生物;Dungeons Mobs;
|
||||
;4439;meteorrandomidea;流年年的随想作;MeteorRandomIdea;
|
||||
;4440;fortyyears;四十年;Miyazaki Hayao Forty Years;
|
||||
;4441;flycycle;Flycycle;;
|
||||
;4442;civilizationswars;三体工业(TeaCon预览版);Civilizations Wars;
|
||||
;4442;civilizationswars;三体工业 (TeaCon预览版);Civilizations Wars;
|
||||
;4443;king_and_knight;明日方舟;King and knight;
|
||||
;4444;ashihara;苇原(芝之章~百里乡野行纪);Ashihara;ASHR
|
||||
;4444;ashihara;苇原 (芝之章~百里乡野行纪);Ashihara;ASHR
|
||||
uusi-aurinko;4445;uusi-aurinko;新日;Uusi Aurinko;
|
||||
;4446;serializableconsciousness;Serializable Consciousness;;SeriCons
|
||||
;4448;elytra_booster;推进鞘翅;Elytra Booster;
|
||||
@@ -4332,7 +4332,7 @@ capsule;4503;capsule;Capsule;;
|
||||
isometric-renders;4504;isometric-renders;等轴渲染;Isometric Renders;IR
|
||||
;4505;;Abstract's Recraft;;
|
||||
enchanting-secretaire;4506;enchantnote;附魔记录;Enchanting Secretaire;
|
||||
sync-fabric;4507;sync;克隆(Fabric);Sync (Fabric);
|
||||
sync-fabric;4507;sync;克隆 (Fabric);Sync (Fabric);
|
||||
tale-of-kingdoms-a-new-conquest;4508;taleofkingdoms;王国:新征程;Tale of Kingdoms: A new Conquest;
|
||||
;4509;networktitle;网络窗口标题;NetworkTitle;
|
||||
schwarz;4510;schwarz;黑;Schwarz;
|
||||
@@ -6502,7 +6502,7 @@ uncle-js;6698;;Uncle J's;;
|
||||
wither-bones;6699;wither_bones;凋灵之骨;Wither Bones;
|
||||
sleep-warp;6700;sleepwarp;SleepWarp;;
|
||||
diagonal-fences;6701;diagonalfences;Diagonal Fences;;DF
|
||||
jumpy-boats-fabric;6702;jumpyboat;跳跃的船(Fabric/Quilt);Jumpy Boats(Fabric/Quilt);
|
||||
jumpy-boats-fabric;6702;jumpyboat;跳跃的船 (Fabric/Quilt);Jumpy Boats(Fabric/Quilt);
|
||||
dyeable-redstone-signal;6703;dyeable_redstone_signal;染色红石信号;Dyeable Redstone Signal;DRS
|
||||
velocity-based-damage;6704;veldmg;Velocity Based Damage;;
|
||||
no-telemetry;6705;no-telemetry;No Telemetry;;
|
||||
@@ -7925,8 +7925,8 @@ fallout-wastelands;8133;fallout_wastelands;Fallout Wastelands;;
|
||||
;8136;letitsnow;Let It Snow;;
|
||||
dons-lightning-rod-mod;8137;dlr;Don's Lightning Rod;;
|
||||
rainglow;8138;rainglow;Rainglow;;
|
||||
nethers-delight-fabric;8139;nethersdelight;下界乐事(Fabric 版);Nether's Delight (Fabric);
|
||||
quicksand-neoforge;8140;quicksand;流沙(NeoForge);Quicksand (NeoForge);
|
||||
nethers-delight-fabric;8139;nethersdelight;下界乐事 (Fabric 版);Nether's Delight (Fabric);
|
||||
quicksand-neoforge;8140;quicksand;流沙 (NeoForge);Quicksand (NeoForge);
|
||||
axolotl-bucket-fix-forge;8141;axolotlbucketfix;美西螈桶修复 Forge 版;Axolotl Bucket Fix (Forge);
|
||||
scape-and-run-parasites-survival-addon-by-nocube;8142;nocubessrpsurvival;Scape and Run: Parasites Survival Addon;;
|
||||
scape-and-run-parasites-nests-addon-by-nocube;8143;nocubessrpnests;Scape and Run: Parasites Nests Addon;;
|
||||
@@ -8587,7 +8587,7 @@ vanity-metallurgical;8803;vanity_metallurgical;Vanity: Metallurgical;;
|
||||
minespaceex;8804;minespaceex;MineSpaceEX;;MSEX
|
||||
gregious-maximus;8805;gregiousmaximus;Gregious Maximus;;
|
||||
shut-up-model-loader;8806;shutupmodelloader;Shut Up, Model Loader!;;SUML
|
||||
cultural-delights-fabric;8807;culturaldelights;多元乐事(Fabric 版);Cultural Delights [Fabric];
|
||||
cultural-delights-fabric;8807;culturaldelights;多元乐事 (Fabric 版);Cultural Delights [Fabric];
|
||||
collectors-reap;8808;collectorsreap;Collector's Reap;;
|
||||
bones-and-swords;8809;bonesandswords;Bones And Swords;;
|
||||
vanity-shedds-mystical-toolsets-pack;8810;sheddmersopal;Vanity: Shedd's Mystical Toolsets Pack;;
|
||||
@@ -8669,7 +8669,7 @@ waymaker;8888;waymaker;Waystone Waypoint Maker;;
|
||||
;8889;mixingasm;Mixingasm;;
|
||||
dmod;8890;dmod;D-Mod;;
|
||||
extra-redstone-craft;8891;extracraft;更多红石合成;Extra Redstone Craft;EXRS
|
||||
farmers-respite-fabric;8892;farmersrespite;农夫暇事(Fabric 版);Farmer's Respite (Fabric);
|
||||
farmers-respite-fabric;8892;farmersrespite;农夫暇事 (Fabric 版);Farmer's Respite (Fabric);
|
||||
wizards;8893;wizards;魔法师;Wizards;
|
||||
basketcase;8894;basketcase;Basketcase;;
|
||||
primal-lib;8895;primallib;Primal Lib;;
|
||||
@@ -9027,7 +9027,7 @@ universal-enchants-forge;9252;universalenchants;通用附魔;Universal Enchants;
|
||||
aa-farmer-compat;9253;farmeradditions;AA Farmer Compat;;
|
||||
;9254;sctz_ywsj;只要听话就不会死!;;
|
||||
;9255;configurable-firework-explosion;可配置的烟花爆炸;Configurable Firework Explosion;CFE
|
||||
;9256;;内修炼体(属性加点);;
|
||||
;9256;;内修炼体 (属性加点);;
|
||||
better-crates;9257;bettercrates;Better Crates;;
|
||||
vanillaicecreamfix;9259;vanillaicecreamfix;VanillaIcecreamFix;;
|
||||
actually-unbreaking-forge;9260;actuallyunbreaking;Actually Unbreaking;;
|
||||
@@ -9235,7 +9235,7 @@ cute-hermit-crabs;9466;cutehermitcrabs;Cute Hermit Crabs;;
|
||||
;9467;ulib;uLib;;
|
||||
receiver-mod;9468;rm;Receiver Gun Mod;;
|
||||
chargepads;9469;ChargePads;充电座;ChargePads;
|
||||
snow-real-magic-potato-edition;9470;;雪!真实的魔法!(土豆版);Snow Real Magic Potato Edition;
|
||||
snow-real-magic-potato-edition;9470;;雪!真实的魔法!(土豆版);Snow Real Magic Potato Edition;
|
||||
;9471;depthstrider;Depth Strider;;
|
||||
ravenous-void;9472;rv;Ravenous Void;;
|
||||
immersive-engineering-blueprint-tweaker;9473;iebpt;Immersive Engineering Blueprint Tweaker;;IEBPT
|
||||
@@ -9434,7 +9434,7 @@ splatcraft;9665;splatcraft;斯普拉遁;Splatcraft;
|
||||
beneath-the-snow;9666;beneath_thesnow_mr;Beneath The Snow;;
|
||||
sea-creeps;9667;aquacreepermod;Sea Creeps;;
|
||||
skills-v;9668;skillsvanilla;技能:香草;Skills: Vanilla;
|
||||
;9669;teleportation;瞬身术(Fabric);Teleportation (Fabric);
|
||||
;9669;teleportation;瞬身术 (Fabric);Teleportation (Fabric);
|
||||
flower-foxes;9670;flower_foxes;Flower Foxes!;;
|
||||
pack-it-up;9671;pack_it_up;Pack It Up!;;
|
||||
mine-trading-cards-open-source-edition;9672;is_mtc;Mine Trading Cards Open Source Edition;;
|
||||
@@ -11746,7 +11746,7 @@ stairdoors-extended;11999;stairdoors;StairDoors Extended;;
|
||||
into-the-rend;12000;therenddimension;Into The Rendv / The Rend Dimension;;
|
||||
buff-frog-mod;12001;buff_frog_mod;Buff Frogs;;
|
||||
asphere;12002;asphere;Asphere;;
|
||||
autoclick-machine;12003;clickmachine;自动连点器(Fabric);Click Machine (Fabric);
|
||||
autoclick-machine;12003;clickmachine;自动连点器 (Fabric);Click Machine (Fabric);
|
||||
harvestcraft-tweaker;12004;harvestcrafttweaker;HarvestCraft Tweaker;;
|
||||
the-legend-of-the-mudman;12005;legend_of_the_mudman;The Legend of the Mudman;;
|
||||
mutagenesis;12006;mutagenesis;Mutagenesis;;
|
||||
@@ -11842,7 +11842,7 @@ the-digimod-beta;12095;thedigimod;方块数码宝贝;The Digimod;
|
||||
ex-deorum;12096;exdeorum;无中生有:神赐;Ex Deorum;
|
||||
entity-detectors;12097;entitydetectors;Entity Detectors;;
|
||||
;12098;horsearmorstandmod;Horse Armor Stand;;
|
||||
decorative-blocks-fork;12099;decorative_blocks;装饰方块(分支版);Decorative Blocks (Fork);
|
||||
decorative-blocks-fork;12099;decorative_blocks;装饰方块 (分支版);Decorative Blocks (Fork);
|
||||
above-and-below;12100;above_and_below;Above and Below;;
|
||||
;12101;ppcore;PPcore;;
|
||||
freeze-it-and-heat-it;12102;fiahi;冷汗:食物冷冻与加热;Freeze It, and Heat It!;FIAHI
|
||||
@@ -11974,10 +11974,10 @@ rustic-furniture-compat;12227;rusticcompat;Rustic Furniture Compat;;
|
||||
;12228;norealmsbutton;Realms按钮移除;No Realms Button;
|
||||
;12229;benchantments;BEnchantments;;
|
||||
epic-fight-indestructible;12230;indestructible;坚不可摧;Epic Fight - Indestructible;
|
||||
;12231;set_bonus;Set Bonus(TeamPancake);;
|
||||
;12232;chat_bar_time_display;聊天栏时间显示(TeamPancake 版);When Was That Again(TeamPancke);
|
||||
;12231;set_bonus;Set Bonus (TeamPancake);;
|
||||
;12232;chat_bar_time_display;聊天栏时间显示 (TeamPancake 版);When Was That Again(TeamPancke);
|
||||
;12233;;逃离塔块夫-重铸;Escape From Tarcraft Reforged;EFTR
|
||||
;12234;;右键文本清除(TeamPancake 版);Right Click Clear(TeamPancake);
|
||||
;12234;;右键文本清除 (TeamPancake 版);Right Click Clear(TeamPancake);
|
||||
catwalks-llc;12235;catwalksinc;Catwalks LLC.;;
|
||||
divine-quest-rpg;12236;tales_of_sunnrya,divine_quest;Tales of Sunnrya / Divine Quest RPG;;
|
||||
;12237;carpet-org-addition;Carpet Org Addition;;
|
||||
@@ -12021,7 +12021,7 @@ rusticwitchcraft;12274;rusticwitchcraft;Rustic Witchcraft;;
|
||||
;12275;unif.logger;UniF-Logger;;
|
||||
sterling-and-black;12276;sterlingblack;Sterling and Black;;
|
||||
the-devourer-of-skulls-datapack;12277;;The Devourer of Skulls 『𝔹𝕠𝕤𝕤 𝔽𝕚𝕘𝕙𝕥』;;
|
||||
;12278;birds_nests;鸟巢(TeamPancake 版);Birds Nests(Team Pancake);
|
||||
;12278;birds_nests;鸟巢 (TeamPancake 版);Birds Nests(Team Pancake);
|
||||
boss-and-magic-raid-compat;12279;;Boss And Magic: Raid Compat;;
|
||||
animated-mobs;12280;animatedmobsmod;Boss And Magic / AME's Mobs / Animated Mobs;;
|
||||
;12281;GTQTCore;量子跃迁:纠缠核心;GTQTcore;GTC
|
||||
@@ -13701,12 +13701,12 @@ the-super-god-road;13962;TheSuperGodRoad;超神之路;TheSuperGodRoad;SGR
|
||||
;13965;exblades;ExBlades;;
|
||||
storage-delight-forge;13966;storagedelight;存储乐事;Storage Delight;
|
||||
lightshield;13967;lightshield;光之护盾;LightShield;LShield
|
||||
sophisticated-core-unofficial-fabric-port;13968;;精妙核心(非官方 Fabric 移植);Sophisticated Core (Unofficial Fabric port);
|
||||
sophisticated-backpacks-unofficial-fabric-port;13969;;精妙背包(非官方 Fabric 分支);Sophisticated Backpacks (Unofficial Fabric port);
|
||||
sophisticated-core-unofficial-fabric-port;13968;;精妙核心 (非官方 Fabric 移植);Sophisticated Core (Unofficial Fabric port);
|
||||
sophisticated-backpacks-unofficial-fabric-port;13969;;精妙背包 (非官方 Fabric 分支);Sophisticated Backpacks (Unofficial Fabric port);
|
||||
red-power;13970;red_power;Red Power;;
|
||||
;13971;hdykwd;你怎么知道我做了什么;How do you know what I done;HDYKWD
|
||||
;13972;minihudextra;MiniHUD Extra;;MHEx
|
||||
sophisticated-storage-unofficial-fabric-port;13973;;精妙存储(非官方 Fabric 分支);Sophisticated Storage (Unofficial Fabric port);
|
||||
sophisticated-storage-unofficial-fabric-port;13973;;精妙存储 (非官方 Fabric 分支);Sophisticated Storage (Unofficial Fabric port);
|
||||
;13974;oldblockdrops,oldoredropsforge;Old Ore Drops;;
|
||||
rage-mod;13975;rage;兴怒;Rage Mod;
|
||||
;13976;disableallstructers;Disable All Structures;;
|
||||
@@ -14500,7 +14500,7 @@ the-rings-of-power;14766;trop;力量之戒/魔戒众戒;The Rings of Power;TRoP
|
||||
suspicious-zombification;14770;suszombification;Suspicious Zombification;;
|
||||
;14771;wmis;Where Is My Stuff-Forge;;WIMSF
|
||||
ivtoolkit-arcana-rpg-continuation-edition;14772;IvToolkit;IvToolkit:延续;IvToolkit Arcana Rpg Continuation Edition;
|
||||
farmer-s-delight-bedrock-unofficial;14773;FarmersDelightBedrockUnofficial;农夫乐事基岩版(Unofficial);Farmer's Delight Bedrock (Unofficial);
|
||||
farmer-s-delight-bedrock-unofficial;14773;FarmersDelightBedrockUnofficial;农夫乐事基岩版 (Unofficial);Farmer's Delight Bedrock (Unofficial);
|
||||
;14774;byzh-picgen;byzh's 麦块画图;BYZH's Picgen;
|
||||
tropicraft-continuation;14775;tropicraft;热带世界:延续;Tropicraft Continuation;
|
||||
battle-tower-continuation;14776;AS_BattleTowers;战斗高塔:延续;Battle Tower Continuation;
|
||||
@@ -14676,9 +14676,9 @@ skeletalband;14947;skeletalband;骷髅乐队乐谱;SkeletalBand;
|
||||
alexs-caves-entropy;14948;entropy;Alex 的洞穴:熵;Alex's Caves:Entropy;ACE
|
||||
mekanism-explosives;14949;mekanismexplosives;通用机械:炸药;Mekanism Explosives;
|
||||
;14950;minimalcoordshud;Minimal Coords HUD;;
|
||||
;14951;;存储乐事基岩版(Unofficial);Storage Delight Bedrock (Unofficial);
|
||||
;14952;;蟹农乐事基岩版(Unofficial);Crabber's Delight Bedrock (Unofficial);
|
||||
;14953;;末地乐事基岩版(Unofficial);End's Delight Bedrock (Unofficial);
|
||||
;14951;;存储乐事基岩版 (Unofficial);Storage Delight Bedrock (Unofficial);
|
||||
;14952;;蟹农乐事基岩版 (Unofficial);Crabber's Delight Bedrock (Unofficial);
|
||||
;14953;;末地乐事基岩版 (Unofficial);End's Delight Bedrock (Unofficial);
|
||||
avaritiareborn;14954;avaritia;无尽贪婪:复兴;AvaritiaReborn;
|
||||
cherry-shrimp;14955;cherry_shrimp;Cherry Shrimp;;
|
||||
pick-command;14956;pickcommand;俺拾的指令;Pick Command;PiC
|
||||
@@ -18132,7 +18132,7 @@ deimos-fabric-forge-neoforge;18436;deimos;Deimos;;
|
||||
excavation;18438;excavation;挖掘;Excavation;
|
||||
basic-end-ores;18439;beo;基础末地矿石;Basic End Ores;BEO
|
||||
;18440;portable_crafting;Portable Crafting;;
|
||||
;18441;gofish;一起钓鱼吧!(Forge 版);Go Fish Forge;
|
||||
;18441;gofish;一起钓鱼吧!(Forge 版);Go Fish Forge;
|
||||
simple-bbq-unofficial;18442;simplebbq;简易烧烤非官方版;Simple BBQ Unofficial;
|
||||
;18443;carystanley_extremefarming;Extreme Farming;;
|
||||
eggy-goodness;18444;EggyGoodness;Eggy Goodness;;
|
||||
@@ -18332,7 +18332,7 @@ auto-pillar-generator;18637;apg;一键柱子生成;Auto Pillar Generator;APG
|
||||
vanilla-delight;18638;vanilladelight;Vanilla Delight;;
|
||||
jade-vs;18639;jade_vs;Jade VS;;
|
||||
;18640;custom_ender_dragon;Draconic Ascension - Custom Ender Dragon Fight;;
|
||||
vics-modern-warfare-patched;18641;;维克的现代战争(修复版);Vic's Modern Warfare (Patched);VMW
|
||||
vics-modern-warfare-patched;18641;;维克的现代战争 (修复版);Vic's Modern Warfare (Patched);VMW
|
||||
waterframesbackported;18642;waterframes;WaterFramesBackported;;
|
||||
;18643;sihywtcamc-unofficial-port;So I heard you were talking crap about Minecraft's combat? - 非官方移植;sihywtcamc unofficial port;sihywtcamc
|
||||
tweakeroo-update-port;18644;tweakeroo;Tweakeroo Update Port;;
|
||||
@@ -18777,8 +18777,8 @@ exporbrecall;19084;exporbrecall;经验球召回;ExpOrbRecall;
|
||||
functional-economy;19085;functional_economy;功能性钱币;FunctionalEconomy;FE
|
||||
ranckors-rings-of-power;19086;the_rings_of_power;Ranckor的力量之戒;Ranckor's Rings of Power;
|
||||
lotr-improvements-lite;19087;lotrimprovementslite;魔戒改进:轻量版;LOTR Improvements Lite;
|
||||
;19088;;海洋乐事基岩版(Unofficial);Ocean's Delight Bedrock (Unofficial);
|
||||
;19089;;幽匿乐事基岩版(unofficial);Silent's Delight Bedrock (Unofficial);
|
||||
;19088;;海洋乐事基岩版 (Unofficial);Ocean's Delight Bedrock (Unofficial);
|
||||
;19089;;幽匿乐事基岩版 (unofficial);Silent's Delight Bedrock (Unofficial);
|
||||
astounding;19090;astounding;Astounding;;AUD
|
||||
carpets-core;19091;carpetscore;Carpets Core;;
|
||||
carpet-variants;19092;carpetvariants;Carpet Variants;;
|
||||
@@ -19206,7 +19206,7 @@ baublesreforked;19516;baublesreforked;饰品重铸;BaublesReforked;
|
||||
tmrv;19518;tmrv;TooManyRecipeViewers;;TMRV
|
||||
;19519;codmodern;COD击杀信息显示;COD Modern Warfare Information;CMW
|
||||
twilight-treehouses;19520;twilight_treehouses;Twilight Treehouses;;
|
||||
improved-rails;19521;irails;更好的铁轨(可转弯の动力铁轨!);Improved Rail (TURNABLE POWERED RAILS);IRail
|
||||
improved-rails;19521;irails;更好的铁轨 (可转弯の动力铁轨!);Improved Rail (TURNABLE POWERED RAILS);IRail
|
||||
redstone-signal-bind-wand;19522;wlrt;非Create版无线红石;Redstone Signal Bind Wand;RSBW
|
||||
;19523;mr_custom_packetaggregates;定制数据包集合体;Custom packet aggregates;CPA
|
||||
;19524;trator;游戏内聊天翻译;In game Translator;IGTL
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"version_legacy": "0\\.3\\.[89]-rc.[0-9]",
|
||||
"version_legacy": "0\\.3\\.[89]-rc\\.([0-9]|10)",
|
||||
"version_recent": [
|
||||
"0.3.9-rc.9"
|
||||
"0.3.9-rc.10"
|
||||
],
|
||||
"version_latest": "0.3.9-rc.10",
|
||||
"version_latest": "0.3.10",
|
||||
"classifiers": {
|
||||
"linux-arm64": "sha256:d5d386e9936d99dd8c31cdab2098a9db04bf55263faf5cd27d388139a42dee0b",
|
||||
"linux-x86_64": "sha256:c43f111460c777315f12081d934729c08cc67b1a2048fdf3ae451e2682157dd8",
|
||||
"macos-arm64": "sha256:49cbf5880aa94551951abda0e225bbbd9c43b9078ead8a738f5b995085d6022c",
|
||||
"macos-arm64.pkg": "sha256:756831f50d67ed2382f8507dfcc94fcc32d6082e087cd4f6a2fc40ae39f25cdf",
|
||||
"macos-x86_64": "sha256:fdf519f018ce4b3d5b4be06b86dcf9990e94b4010db9dac391175d917214cc01",
|
||||
"macos-x86_64.pkg": "sha256:fc030f0205aa3667033c79ac514045bb7c94ae0175dc5946ea920eefee7ad997",
|
||||
"windows-arm64.exe": "sha256:4b395093b0acaea85a36ce1e569c206d9dfa7e11bfe70409f00bc6c6100df88e",
|
||||
"windows-x86_64.exe": "sha256:2b8add8f6330452d29faaa944ac63c35452e61c5a4ff009069d2c3017c7186c6"
|
||||
"linux-arm64": "sha256:001b6b1e220e98dffd84198a3e16855644cad27900562c78d610b0bf50138d17",
|
||||
"linux-x86_64": "sha256:b133ddb796ff52879fb0c38e68ef7ea968d3eaec77f27ea274c701888e795782",
|
||||
"macos-arm64": "sha256:4bb2eb432ea0ee3e3dca6b1019d8864aa14bedfe20b3d804865e216e36e856df",
|
||||
"macos-arm64.pkg": "sha256:e4bf3d2014e417565e3ab0df579f4280e0af42bc28516f640067a52dca77098f",
|
||||
"macos-x86_64": "sha256:17a2cc46d884951a88da6f3f0725e38ffca6fdfd1da021807121c29b944bd830",
|
||||
"macos-x86_64.pkg": "sha256:a4f08a706605262485c1889dfae00554fba753141f6d6ada76e29bfc3e0338aa",
|
||||
"windows-arm64.exe": "sha256:245e0faf2ee1cdc5e430b5c18fbab4c4e5e3f333f0de51a621ae772db3a16924",
|
||||
"windows-x86_64.exe": "sha256:ab99ddb729860c939b2085d8852fd62007b86d97568b687cf2a7147718b64eea"
|
||||
},
|
||||
"downloads": [
|
||||
"https://github.com/burningtnt/Terracotta/releases/download/v${version}/terracotta-${version}-${classifier}",
|
||||
|
||||
+15
-12
@@ -15,7 +15,7 @@
|
||||
* 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.i18n;
|
||||
package org.jackhuang.hmcl.util.i18n.translator;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -26,33 +26,36 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public final class WenyanUtilsTest {
|
||||
public final class TranslatorTest {
|
||||
|
||||
private static void assertYearToString(String value, int year) {
|
||||
//region lzh
|
||||
|
||||
private static void assertYearToLZH(String value, int year) {
|
||||
StringBuilder builder = new StringBuilder(2);
|
||||
WenyanUtils.appendYear(builder, year);
|
||||
Translator_lzh.appendYear(builder, year);
|
||||
assertEquals(value, builder.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testYearToString() {
|
||||
assertYearToString("甲子", 1984);
|
||||
assertYearToString("乙巳", 2025);
|
||||
assertYearToString("甲子", -2996);
|
||||
assertYearToString("庚子", 1000);
|
||||
public void testYearToLZH() {
|
||||
assertYearToLZH("甲子", 1984);
|
||||
assertYearToLZH("乙巳", 2025);
|
||||
assertYearToLZH("甲子", -2996);
|
||||
assertYearToLZH("庚子", 1000);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHourToString() {
|
||||
|
||||
public void testHourToLZH() {
|
||||
List<String> list = List.of(
|
||||
"子正", "丑初", "丑正", "寅初", "寅正", "卯初", "卯正", "辰初", "辰正", "巳初", "巳正", "午初",
|
||||
"午正", "未初", "未正", "申初", "申正", "酉初", "酉正", "戌初", "戌正", "亥初", "亥正", "子初"
|
||||
);
|
||||
for (int hour = 0; hour < list.size(); hour++) {
|
||||
StringBuilder builder = new StringBuilder(2);
|
||||
WenyanUtils.appendHour(builder, hour);
|
||||
Translator_lzh.appendHour(builder, hour);
|
||||
assertEquals(list.get(hour), builder.toString());
|
||||
}
|
||||
}
|
||||
|
||||
//endregion
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.download;
|
||||
|
||||
import org.jackhuang.hmcl.download.cleanroom.CleanroomInstallTask;
|
||||
import org.jackhuang.hmcl.download.forge.ForgeInstallTask;
|
||||
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
|
||||
import org.jackhuang.hmcl.download.game.GameDownloadTask;
|
||||
@@ -182,6 +183,11 @@ public class DefaultDependencyManager extends AbstractDependencyManager {
|
||||
|
||||
return Task
|
||||
.composeAsync(() -> {
|
||||
try {
|
||||
return CleanroomInstallTask.install(this, oldVersion, installer);
|
||||
} catch (IOException ignore) {
|
||||
}
|
||||
|
||||
try {
|
||||
return NeoForgeInstallTask.install(this, oldVersion, installer);
|
||||
} catch (IOException ignore) {
|
||||
|
||||
@@ -20,7 +20,6 @@ package org.jackhuang.hmcl.download;
|
||||
import org.intellij.lang.annotations.Language;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.mod.ModLoaderType;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionRange;
|
||||
@@ -172,6 +171,7 @@ public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
|
||||
String mainClass = resolvedVersion.getMainClass();
|
||||
return mainClass != null && (LAUNCH_WRAPPER_MAIN.equals(mainClass)
|
||||
|| mainClass.startsWith("net.minecraftforge")
|
||||
|| mainClass.startsWith("net.neoforged")
|
||||
|| mainClass.startsWith("top.outlands") //Cleanroom
|
||||
|| mainClass.startsWith("net.fabricmc")
|
||||
|| mainClass.startsWith("org.quiltmc")
|
||||
@@ -361,14 +361,16 @@ public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
|
||||
public static final String MOD_LAUNCHER_MAIN = "cpw.mods.modlauncher.Launcher";
|
||||
public static final String BOOTSTRAP_LAUNCHER_MAIN = "cpw.mods.bootstraplauncher.BootstrapLauncher";
|
||||
public static final String FORGE_BOOTSTRAP_MAIN = "net.minecraftforge.bootstrap.ForgeBootstrap";
|
||||
public static final String NEO_FORGED_BOOTSTRAP_MAIN = "net.neoforged.fml.startup.Client";
|
||||
|
||||
public static final Set<String> FORGE_OPTIFINE_MAIN = new HashSet<>(Lang.immutableListOf(
|
||||
public static final Set<String> FORGE_OPTIFINE_MAIN = Set.of(
|
||||
LibraryAnalyzer.VANILLA_MAIN,
|
||||
LibraryAnalyzer.LAUNCH_WRAPPER_MAIN,
|
||||
LibraryAnalyzer.MOD_LAUNCHER_MAIN,
|
||||
LibraryAnalyzer.BOOTSTRAP_LAUNCHER_MAIN,
|
||||
LibraryAnalyzer.FORGE_BOOTSTRAP_MAIN
|
||||
));
|
||||
LibraryAnalyzer.FORGE_BOOTSTRAP_MAIN,
|
||||
LibraryAnalyzer.NEO_FORGED_BOOTSTRAP_MAIN
|
||||
);
|
||||
|
||||
public static final VersionRange<VersionNumber> FORGE_OPTIFINE_BROKEN_RANGE = VersionNumber.between("48.0.0", "49.0.50");
|
||||
|
||||
|
||||
+55
-10
@@ -21,30 +21,48 @@ import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.download.UnsupportedInstallationException;
|
||||
import org.jackhuang.hmcl.download.VersionMismatchException;
|
||||
import org.jackhuang.hmcl.download.forge.ForgeNewInstallProfile;
|
||||
import org.jackhuang.hmcl.download.forge.ForgeNewInstallTask;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
public final class CleanroomInstallTask extends Task<Version> {
|
||||
|
||||
private final DefaultDependencyManager dependencyManager;
|
||||
private final Version version;
|
||||
private Path installer;
|
||||
private final CleanroomRemoteVersion remote;
|
||||
private Path installer;
|
||||
private FileDownloadTask dependent;
|
||||
private Task<Version> task;
|
||||
private String selfVersion;
|
||||
|
||||
public CleanroomInstallTask(DefaultDependencyManager dependencyManager, Version version, CleanroomRemoteVersion remoteVersion) {
|
||||
this.dependencyManager = dependencyManager;
|
||||
this.version = version;
|
||||
this.remote = remoteVersion;
|
||||
|
||||
setSignificance(TaskSignificance.MODERATE);
|
||||
}
|
||||
|
||||
public CleanroomInstallTask(DefaultDependencyManager dependencyManager, Version version, String selfVersion, Path installer) {
|
||||
this.dependencyManager = dependencyManager;
|
||||
this.version = version;
|
||||
this.selfVersion = selfVersion;
|
||||
this.remote = null;
|
||||
this.installer = installer;
|
||||
|
||||
setSignificance(TaskSignificance.MODERATE);
|
||||
}
|
||||
|
||||
@@ -55,14 +73,16 @@ public final class CleanroomInstallTask extends Task<Version> {
|
||||
|
||||
@Override
|
||||
public void preExecute() throws Exception {
|
||||
installer = Files.createTempFile("cleanroom-installer", ".jar");
|
||||
if (installer == null) {
|
||||
installer = Files.createTempFile("cleanroom-installer", ".jar");
|
||||
|
||||
dependent = new FileDownloadTask(
|
||||
dependencyManager.getDownloadProvider().injectURLsWithCandidates(remote.getUrls()),
|
||||
installer, null);
|
||||
dependent.setCacheRepository(dependencyManager.getCacheRepository());
|
||||
dependent.setCaching(true);
|
||||
dependent.addIntegrityCheckHandler(FileDownloadTask.ZIP_INTEGRITY_CHECK_HANDLER);
|
||||
dependent = new FileDownloadTask(
|
||||
dependencyManager.getDownloadProvider().injectURLsWithCandidates(remote.getUrls()),
|
||||
installer, null);
|
||||
dependent.setCacheRepository(dependencyManager.getCacheRepository());
|
||||
dependent.setCaching(true);
|
||||
dependent.addIntegrityCheckHandler(FileDownloadTask.ZIP_INTEGRITY_CHECK_HANDLER);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -78,7 +98,7 @@ public final class CleanroomInstallTask extends Task<Version> {
|
||||
|
||||
@Override
|
||||
public Collection<Task<?>> getDependents() {
|
||||
return Collections.singleton(dependent);
|
||||
return dependent == null ? Collections.emptySet() : Collections.singleton(dependent);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -88,6 +108,31 @@ public final class CleanroomInstallTask extends Task<Version> {
|
||||
|
||||
@Override
|
||||
public void execute() throws IOException, VersionMismatchException, UnsupportedInstallationException {
|
||||
task = new ForgeNewInstallTask(dependencyManager, version, remote.getSelfVersion(), installer).thenApplyAsync((version) -> version.setId(LibraryAnalyzer.LibraryType.CLEANROOM.getPatchId()));
|
||||
if (selfVersion == null) {
|
||||
task = new ForgeNewInstallTask(dependencyManager, version, remote.getSelfVersion(), installer).thenApplyAsync((version) -> version.setId(LibraryAnalyzer.LibraryType.CLEANROOM.getPatchId()));
|
||||
} else {
|
||||
task = new ForgeNewInstallTask(dependencyManager, version, selfVersion, installer).thenApplyAsync((version) -> version.setId(LibraryAnalyzer.LibraryType.CLEANROOM.getPatchId()));
|
||||
}
|
||||
}
|
||||
|
||||
public static Task<Version> install(DefaultDependencyManager dependencyManager, Version version, Path installer) throws IOException, VersionMismatchException {
|
||||
Optional<String> gameVersion = dependencyManager.getGameRepository().getGameVersion(version);
|
||||
if (gameVersion.isEmpty()) throw new IOException();
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(installer)) {
|
||||
String installProfileText = Files.readString(fs.getPath("install_profile.json"));
|
||||
Map<?, ?> installProfile = JsonUtils.fromNonNullJson(installProfileText, Map.class);
|
||||
if (LibraryAnalyzer.LibraryType.CLEANROOM.getPatchId().equals(installProfile.get("profile"))) {
|
||||
ForgeNewInstallProfile profile = JsonUtils.fromNonNullJson(installProfileText, ForgeNewInstallProfile.class);
|
||||
if (!gameVersion.get().equals(profile.getMinecraft()))
|
||||
throw new VersionMismatchException(profile.getMinecraft(), gameVersion.get());
|
||||
return new CleanroomInstallTask(dependencyManager, version, modifyVersion(profile.getVersion()), installer);
|
||||
} else {
|
||||
throw new IOException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String modifyVersion(String version) {
|
||||
return version.replace("cleanroom-", "");
|
||||
}
|
||||
}
|
||||
@@ -149,6 +149,11 @@ public class DefaultGameRepository implements GameRepository {
|
||||
return getVersionRoot(id).resolve("natives-" + platform);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path getModsDirectory(String id) {
|
||||
return getRunDirectory(id).resolve("mods");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path getVersionRoot(String id) {
|
||||
return getBaseDirectory().resolve("versions/" + id);
|
||||
|
||||
@@ -131,6 +131,12 @@ public interface GameRepository extends VersionProvider {
|
||||
*/
|
||||
Path getNativeDirectory(String id, Platform platform);
|
||||
|
||||
/// Get the directory for placing mod files.
|
||||
///
|
||||
/// @param id instance id
|
||||
/// @return the mods directory
|
||||
Path getModsDirectory(String id);
|
||||
|
||||
/**
|
||||
* Get minecraft jar
|
||||
*
|
||||
|
||||
@@ -189,7 +189,7 @@ public final class LocalModFile implements Comparable<LocalModFile> {
|
||||
|
||||
@Override
|
||||
public int compareTo(LocalModFile o) {
|
||||
return getFileName().compareTo(o.getFileName());
|
||||
return getFileName().compareToIgnoreCase(o.getFileName());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.mod;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.game.GameRepository;
|
||||
import org.jackhuang.hmcl.mod.modinfo.*;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
@@ -25,39 +26,46 @@ import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class ModManager {
|
||||
@FunctionalInterface
|
||||
private interface ModMetadataReader {
|
||||
LocalModFile fromFile(ModManager modManager, Path modFile, FileSystem fs) throws IOException, JsonParseException;
|
||||
}
|
||||
|
||||
private static final Map<String, Pair<ModMetadataReader[], String>> READERS;
|
||||
private static final Map<String, List<Pair<ModMetadataReader, ModLoaderType>>> READERS;
|
||||
|
||||
static {
|
||||
TreeMap<String, Pair<ModMetadataReader[], String>> readers = new TreeMap<>();
|
||||
readers.put("zip", Pair.pair(new ModMetadataReader[]{
|
||||
ForgeOldModMetadata::fromFile,
|
||||
ForgeNewModMetadata::fromFile,
|
||||
FabricModMetadata::fromFile,
|
||||
QuiltModMetadata::fromFile,
|
||||
PackMcMeta::fromFile,
|
||||
}, ""));
|
||||
readers.put("jar", readers.get("zip"));
|
||||
readers.put("litemod", Pair.pair(new ModMetadataReader[]{
|
||||
LiteModMetadata::fromFile
|
||||
}, "LiteLoader Mod"));
|
||||
READERS = Collections.unmodifiableMap(readers);
|
||||
var map = new HashMap<String, List<Pair<ModMetadataReader, ModLoaderType>>>();
|
||||
var zipReaders = List.<Pair<ModMetadataReader, ModLoaderType>>of(
|
||||
pair(ForgeNewModMetadata::fromForgeFile, ModLoaderType.FORGE),
|
||||
pair(ForgeNewModMetadata::fromNeoForgeFile, ModLoaderType.NEO_FORGED),
|
||||
pair(ForgeOldModMetadata::fromFile, ModLoaderType.FORGE),
|
||||
pair(FabricModMetadata::fromFile, ModLoaderType.FABRIC),
|
||||
pair(QuiltModMetadata::fromFile, ModLoaderType.QUILT),
|
||||
pair(PackMcMeta::fromFile, ModLoaderType.PACK)
|
||||
);
|
||||
|
||||
map.put("zip", zipReaders);
|
||||
map.put("jar", zipReaders);
|
||||
map.put("litemod", List.of(pair(LiteModMetadata::fromFile, ModLoaderType.LITE_LOADER)));
|
||||
|
||||
READERS = map;
|
||||
}
|
||||
|
||||
private final GameRepository repository;
|
||||
private final String id;
|
||||
private final TreeSet<LocalModFile> localModFiles = new TreeSet<>();
|
||||
private final HashMap<LocalMod, LocalMod> localMods = new HashMap<>();
|
||||
private final HashMap<Pair<String, ModLoaderType>, LocalMod> localMods = new HashMap<>();
|
||||
private LibraryAnalyzer analyzer;
|
||||
|
||||
private boolean loaded = false;
|
||||
|
||||
@@ -70,57 +78,104 @@ public final class ModManager {
|
||||
return repository;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
public String getInstanceId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public Path getModsDirectory() {
|
||||
return repository.getRunDirectory(id).resolve("mods");
|
||||
return repository.getModsDirectory(id);
|
||||
}
|
||||
|
||||
public LocalMod getLocalMod(String id, ModLoaderType modLoaderType) {
|
||||
return localMods.computeIfAbsent(new LocalMod(id, modLoaderType), x -> x);
|
||||
public LibraryAnalyzer getLibraryAnalyzer() {
|
||||
return analyzer;
|
||||
}
|
||||
|
||||
public LocalMod getLocalMod(String modId, ModLoaderType modLoaderType) {
|
||||
return localMods.computeIfAbsent(pair(modId, modLoaderType),
|
||||
x -> new LocalMod(x.getKey(), x.getValue()));
|
||||
}
|
||||
|
||||
public boolean hasMod(String modId, ModLoaderType modLoaderType) {
|
||||
return localMods.containsKey(pair(modId, modLoaderType));
|
||||
}
|
||||
|
||||
private void addModInfo(Path file) {
|
||||
try {
|
||||
LocalModFile localModFile = getModInfo(file);
|
||||
if (!localModFile.isOld()) {
|
||||
localModFiles.add(localModFile);
|
||||
}
|
||||
} catch (IllegalArgumentException ignore) {
|
||||
}
|
||||
}
|
||||
|
||||
public LocalModFile getModInfo(Path modFile) {
|
||||
String fileName = StringUtils.removeSuffix(FileUtils.getName(modFile), DISABLED_EXTENSION, OLD_EXTENSION);
|
||||
String fileName = StringUtils.removeSuffix(FileUtils.getName(file), DISABLED_EXTENSION, OLD_EXTENSION);
|
||||
String extension = fileName.substring(fileName.lastIndexOf(".") + 1);
|
||||
Pair<ModMetadataReader[], String> currentReader = READERS.get(extension);
|
||||
if (currentReader == null) {
|
||||
throw new IllegalArgumentException("File " + modFile + " is not a mod file.");
|
||||
|
||||
List<Pair<ModMetadataReader, ModLoaderType>> readersMap = READERS.get(extension);
|
||||
if (readersMap == null) {
|
||||
// Is not a mod file.
|
||||
return;
|
||||
}
|
||||
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile)) {
|
||||
for (ModMetadataReader reader : currentReader.getKey()) {
|
||||
Set<ModLoaderType> modLoaderTypes = analyzer.getModLoaders();
|
||||
|
||||
var supportedReaders = new ArrayList<ModMetadataReader>();
|
||||
var unsupportedReaders = new ArrayList<ModMetadataReader>();
|
||||
|
||||
for (Pair<ModMetadataReader, ModLoaderType> reader : readersMap) {
|
||||
if (modLoaderTypes.contains(reader.getValue())) {
|
||||
supportedReaders.add(reader.getKey());
|
||||
} else {
|
||||
unsupportedReaders.add(reader.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
LocalModFile modInfo = null;
|
||||
|
||||
List<Exception> exceptions = new ArrayList<>();
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(file)) {
|
||||
for (ModMetadataReader reader : supportedReaders) {
|
||||
try {
|
||||
return reader.fromFile(this, modFile, fs);
|
||||
} catch (Exception ignore) {
|
||||
modInfo = reader.fromFile(this, file, fs);
|
||||
break;
|
||||
} catch (Exception e) {
|
||||
exceptions.add(e);
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
|
||||
if (modInfo == null) {
|
||||
for (ModMetadataReader reader : unsupportedReaders) {
|
||||
try {
|
||||
modInfo = reader.fromFile(this, file, fs);
|
||||
break;
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to open mod file " + file, e);
|
||||
}
|
||||
|
||||
return new LocalModFile(this,
|
||||
getLocalMod(FileUtils.getNameWithoutExtension(modFile), ModLoaderType.UNKNOWN),
|
||||
modFile,
|
||||
FileUtils.getNameWithoutExtension(modFile),
|
||||
new LocalModFile.Description(currentReader.getValue())
|
||||
);
|
||||
if (modInfo == null) {
|
||||
Exception exception = new Exception("Failed to read mod metadata");
|
||||
for (Exception e : exceptions) {
|
||||
exception.addSuppressed(e);
|
||||
}
|
||||
LOG.warning("Failed to read mod metadata", exception);
|
||||
|
||||
String fileNameWithoutExtension = FileUtils.getNameWithoutExtension(file);
|
||||
|
||||
modInfo = new LocalModFile(this,
|
||||
getLocalMod(fileNameWithoutExtension, ModLoaderType.UNKNOWN),
|
||||
file,
|
||||
fileNameWithoutExtension,
|
||||
new LocalModFile.Description("litemod".equals(extension) ? "LiteLoader Mod" : "")
|
||||
);
|
||||
}
|
||||
|
||||
if (!modInfo.isOld()) {
|
||||
localModFiles.add(modInfo);
|
||||
}
|
||||
}
|
||||
|
||||
public void refreshMods() throws IOException {
|
||||
localModFiles.clear();
|
||||
localMods.clear();
|
||||
|
||||
analyzer = LibraryAnalyzer.analyze(getRepository().getResolvedPreservingPatchesVersion(id), null);
|
||||
|
||||
if (Files.isDirectory(getModsDirectory())) {
|
||||
try (DirectoryStream<Path> modsDirectoryStream = Files.newDirectoryStream(getModsDirectory())) {
|
||||
for (Path subitem : modsDirectoryStream) {
|
||||
@@ -140,10 +195,10 @@ public final class ModManager {
|
||||
loaded = true;
|
||||
}
|
||||
|
||||
public Collection<LocalModFile> getMods() throws IOException {
|
||||
public @Unmodifiable List<LocalModFile> getMods() throws IOException {
|
||||
if (!loaded)
|
||||
refreshMods();
|
||||
return localModFiles;
|
||||
return List.copyOf(localModFiles);
|
||||
}
|
||||
|
||||
public void addMod(Path file) throws IOException {
|
||||
|
||||
@@ -138,7 +138,7 @@ public final class CurseCompletionTask extends Task<Void> {
|
||||
.collect(Collectors.toList()));
|
||||
JsonUtils.writeToJsonFile(root.resolve("manifest.json"), newManifest);
|
||||
|
||||
Path versionRoot = repository.getVersionRoot(modManager.getVersion());
|
||||
Path versionRoot = repository.getVersionRoot(modManager.getInstanceId());
|
||||
Path resourcePacksRoot = versionRoot.resolve("resourcepacks");
|
||||
Path shaderPacksRoot = versionRoot.resolve("shaderpacks");
|
||||
finished.set(0);
|
||||
|
||||
+1
-1
@@ -139,7 +139,7 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
|
||||
StringUtils.LevCalculator levCalculator = new StringUtils.LevCalculator();
|
||||
|
||||
return new SearchResult(response.getData().stream().map(CurseAddon::toMod).map(remoteMod -> {
|
||||
String lowerCaseResult = remoteMod.getTitle().toLowerCase();
|
||||
String lowerCaseResult = remoteMod.getTitle().toLowerCase(Locale.ROOT);
|
||||
int diff = levCalculator.calc(lowerCaseSearchFilter, lowerCaseResult);
|
||||
|
||||
for (String s : StringUtils.tokenize(lowerCaseResult)) {
|
||||
|
||||
@@ -273,7 +273,7 @@ public class McbbsModpackCompletionTask extends CompletableFutureTask<Void> {
|
||||
@Nullable
|
||||
private Path getFilePath(McbbsModpackManifest.File file) {
|
||||
if (file instanceof McbbsModpackManifest.AddonFile) {
|
||||
return modManager.getRepository().getRunDirectory(modManager.getVersion()).resolve(((McbbsModpackManifest.AddonFile) file).getPath());
|
||||
return modManager.getRepository().getRunDirectory(modManager.getInstanceId()).resolve(((McbbsModpackManifest.AddonFile) file).getPath());
|
||||
} else if (file instanceof McbbsModpackManifest.CurseFile) {
|
||||
String fileName = ((McbbsModpackManifest.CurseFile) file).getFileName();
|
||||
if (fileName == null) return null;
|
||||
|
||||
@@ -6,12 +6,15 @@ import org.jackhuang.hmcl.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.mod.ModLoaderType;
|
||||
import org.jackhuang.hmcl.mod.ModManager;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.gson.JsonSerializable;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.jar.Attributes;
|
||||
@@ -111,25 +114,45 @@ public final class ForgeNewModMetadata {
|
||||
}
|
||||
}
|
||||
|
||||
private static final int ACC_FORGE = 0x01;
|
||||
public static LocalModFile fromForgeFile(ModManager modManager, Path modFile, FileSystem fs) throws IOException {
|
||||
return fromFile(modManager, modFile, fs, ModLoaderType.FORGE);
|
||||
}
|
||||
|
||||
private static final int ACC_NEO_FORGED = 0x02;
|
||||
public static LocalModFile fromNeoForgeFile(ModManager modManager, Path modFile, FileSystem fs) throws IOException {
|
||||
return fromFile(modManager, modFile, fs, ModLoaderType.NEO_FORGED);
|
||||
}
|
||||
|
||||
private static LocalModFile fromFile(ModManager modManager, Path modFile, FileSystem fs, ModLoaderType modLoaderType) throws IOException {
|
||||
if (modLoaderType != ModLoaderType.FORGE && modLoaderType != ModLoaderType.NEO_FORGED) {
|
||||
throw new IOException("Invalid mod loader: " + modLoaderType);
|
||||
}
|
||||
|
||||
if (modLoaderType == ModLoaderType.NEO_FORGED) {
|
||||
try {
|
||||
return fromFile0("META-INF/neoforge.mods.toml", modLoaderType, modManager, modFile, fs);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
public static LocalModFile fromFile(ModManager modManager, Path modFile, FileSystem fs) throws IOException, JsonParseException {
|
||||
try {
|
||||
return fromFile0("META-INF/mods.toml", ACC_FORGE | ACC_NEO_FORGED, ModLoaderType.FORGE, modManager, modFile, fs);
|
||||
return fromFile0("META-INF/mods.toml", modLoaderType, modManager, modFile, fs);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
try {
|
||||
return fromFile0("META-INF/neoforge.mods.toml", ACC_NEO_FORGED, ModLoaderType.NEO_FORGED, modManager, modFile, fs);
|
||||
return fromEmbeddedMod(modManager, modFile, fs, modLoaderType);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
throw new IOException("File " + modFile + " is not a Forge 1.13+ or NeoForge mod.");
|
||||
}
|
||||
|
||||
private static LocalModFile fromFile0(String tomlPath, int loaderACC, ModLoaderType defaultLoader, ModManager modManager, Path modFile, FileSystem fs) throws IOException, JsonParseException {
|
||||
private static LocalModFile fromFile0(
|
||||
String tomlPath,
|
||||
ModLoaderType modLoaderType,
|
||||
ModManager modManager,
|
||||
Path modFile,
|
||||
FileSystem fs) throws IOException, JsonParseException {
|
||||
Path modToml = fs.getPath(tomlPath);
|
||||
if (Files.notExists(modToml))
|
||||
throw new IOException("File " + modFile + " is not a Forge 1.13+ or NeoForge mod.");
|
||||
@@ -149,7 +172,7 @@ public final class ForgeNewModMetadata {
|
||||
}
|
||||
}
|
||||
|
||||
ModLoaderType type = analyzeLoader(toml, mod.getModId(), loaderACC, defaultLoader);
|
||||
ModLoaderType type = analyzeLoader(toml, mod.getModId(), modLoaderType);
|
||||
|
||||
return new LocalModFile(modManager, modManager.getLocalMod(mod.getModId(), type), modFile, mod.getDisplayName(), new LocalModFile.Description(mod.getDescription()),
|
||||
mod.getAuthors(), jarVersion == null ? mod.getVersion() : mod.getVersion().replace("${file.jarVersion}", jarVersion), "",
|
||||
@@ -157,31 +180,131 @@ public final class ForgeNewModMetadata {
|
||||
metadata.getLogoFile());
|
||||
}
|
||||
|
||||
private static ModLoaderType analyzeLoader(Toml toml, String modID, int loaderACC, ModLoaderType defaultLoader) throws IOException {
|
||||
private static LocalModFile fromEmbeddedMod(ModManager modManager, Path modFile, FileSystem fs, ModLoaderType modLoaderType) throws IOException {
|
||||
Path manifestFile = fs.getPath("META-INF/MANIFEST.MF");
|
||||
if (!Files.isRegularFile(manifestFile))
|
||||
throw new IOException("Missing MANIFEST.MF in file " + manifestFile);
|
||||
|
||||
Manifest manifest;
|
||||
try (InputStream input = Files.newInputStream(manifestFile)) {
|
||||
manifest = new Manifest(input);
|
||||
}
|
||||
|
||||
List<Path> embeddedModFiles = List.of();
|
||||
|
||||
String embeddedDependenciesMod = manifest.getMainAttributes().getValue("Embedded-Dependencies-Mod");
|
||||
if (embeddedDependenciesMod != null) {
|
||||
Path embeddedModFile = fs.getPath(embeddedDependenciesMod);
|
||||
if (!Files.isRegularFile(embeddedModFile)) {
|
||||
LOG.warning("Missing embedded-dependencies-mod: " + embeddedModFile);
|
||||
throw new IOException();
|
||||
}
|
||||
embeddedModFiles = List.of(embeddedModFile);
|
||||
} else {
|
||||
Path jarInJarMetadata = fs.getPath("META-INF/jarjar/metadata.json");
|
||||
if (Files.isRegularFile(jarInJarMetadata)) {
|
||||
JarInJarMetadata metadata = JsonUtils.fromJsonFile(jarInJarMetadata, JarInJarMetadata.class);
|
||||
if (metadata == null)
|
||||
throw new IOException("Invalid metadata file: " + jarInJarMetadata);
|
||||
|
||||
metadata.validate();
|
||||
|
||||
embeddedModFiles = new ArrayList<>();
|
||||
for (EmbeddedJarMetadata jar : metadata.jars) {
|
||||
Path path = fs.getPath(jar.path);
|
||||
if (Files.isRegularFile(path)) {
|
||||
embeddedModFiles.add(path);
|
||||
} else {
|
||||
LOG.warning("Missing embedded-dependencies-mod: " + path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (embeddedModFiles.isEmpty()) {
|
||||
throw new IOException("Missing embedded mods");
|
||||
}
|
||||
|
||||
Path tempFile = Files.createTempFile("hmcl-", ".zip");
|
||||
try {
|
||||
for (Path embeddedModFile : embeddedModFiles) {
|
||||
Files.copy(embeddedModFile, tempFile, StandardCopyOption.REPLACE_EXISTING);
|
||||
try (FileSystem embeddedFs = CompressingUtils.createReadOnlyZipFileSystem(tempFile)) {
|
||||
return fromFile(modManager, modFile, embeddedFs, modLoaderType);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
Files.deleteIfExists(tempFile);
|
||||
}
|
||||
|
||||
throw new IOException();
|
||||
}
|
||||
|
||||
private static ModLoaderType analyzeLoader(Toml toml, String modID, ModLoaderType loader) throws IOException {
|
||||
List<HashMap<String, Object>> dependencies = toml.getList("dependencies." + modID);
|
||||
if (dependencies == null) {
|
||||
dependencies = toml.getList("dependencies"); // ??? I have no idea why some of the Forge mods use [[dependencies]]
|
||||
try {
|
||||
dependencies = toml.getList("dependencies"); // ??? I have no idea why some of the Forge mods use [[dependencies]]
|
||||
} catch (ClassCastException e) {
|
||||
try {
|
||||
Toml table = toml.getTable("dependencies");
|
||||
if (table == null)
|
||||
return loader;
|
||||
|
||||
dependencies = table.getList(modID);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
if (dependencies == null) {
|
||||
return defaultLoader;
|
||||
return loader;
|
||||
}
|
||||
}
|
||||
|
||||
ModLoaderType result = null;
|
||||
loop:
|
||||
for (HashMap<String, Object> dependency : dependencies) {
|
||||
switch ((String) dependency.get("modId")) {
|
||||
case "forge": return checkLoaderACC(loaderACC, ACC_FORGE, ModLoaderType.FORGE);
|
||||
case "neoforge": return checkLoaderACC(loaderACC, ACC_NEO_FORGED, ModLoaderType.NEO_FORGED);
|
||||
case "forge":
|
||||
result = ModLoaderType.FORGE;
|
||||
break loop;
|
||||
case "neoforge":
|
||||
result = ModLoaderType.NEO_FORGED;
|
||||
break loop;
|
||||
}
|
||||
}
|
||||
|
||||
// ??? I have no idea why some of the Forge mods doesn't provide this key.
|
||||
return defaultLoader;
|
||||
}
|
||||
|
||||
private static ModLoaderType checkLoaderACC(int current, int target, ModLoaderType res) throws IOException {
|
||||
if ((target & current) != 0) {
|
||||
return res;
|
||||
} else {
|
||||
throw new IOException("Mismatched loader.");
|
||||
if (result == loader)
|
||||
return result;
|
||||
else if (result != null)
|
||||
throw new IOException("Loader mismatch");
|
||||
else {
|
||||
LOG.warning("Cannot determine the mod loader for mod " + modID + ", expected " + loader);
|
||||
return loader;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonSerializable
|
||||
private record JarInJarMetadata(List<EmbeddedJarMetadata> jars) implements Validation {
|
||||
@Override
|
||||
public void validate() throws JsonParseException {
|
||||
Validation.requireNonNull(jars, "jars");
|
||||
for (EmbeddedJarMetadata jar : jars) {
|
||||
jar.validate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JsonSerializable
|
||||
private record EmbeddedJarMetadata(
|
||||
String path,
|
||||
boolean isObfuscated
|
||||
) implements Validation {
|
||||
@Override
|
||||
public void validate() throws JsonParseException {
|
||||
Validation.requireNonNull(path, "path");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -19,6 +19,8 @@ package org.jackhuang.hmcl.mod.modinfo;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonToken;
|
||||
import org.jackhuang.hmcl.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.mod.ModLoaderType;
|
||||
import org.jackhuang.hmcl.mod.ModManager;
|
||||
@@ -125,9 +127,27 @@ public final class ForgeOldModMetadata {
|
||||
Path mcmod = fs.getPath("mcmod.info");
|
||||
if (Files.notExists(mcmod))
|
||||
throw new IOException("File " + modFile + " is not a Forge mod.");
|
||||
List<ForgeOldModMetadata> modList = JsonUtils.fromJsonFile(mcmod, listTypeOf(ForgeOldModMetadata.class));
|
||||
|
||||
List<ForgeOldModMetadata> modList;
|
||||
|
||||
try (var reader = Files.newBufferedReader(mcmod);
|
||||
var jsonReader = new JsonReader(reader)) {
|
||||
JsonToken firstToken = jsonReader.peek();
|
||||
|
||||
if (firstToken == JsonToken.BEGIN_ARRAY)
|
||||
modList = JsonUtils.GSON.fromJson(jsonReader, listTypeOf(ForgeOldModMetadata.class));
|
||||
else if (firstToken == JsonToken.BEGIN_OBJECT) {
|
||||
ForgeOldModMetadataLst list = JsonUtils.GSON.fromJson(jsonReader, ForgeOldModMetadataLst.class);
|
||||
if (list == null)
|
||||
throw new IOException("Mod " + modFile + " `mcmod.info` is malformed");
|
||||
modList = list.modList();
|
||||
} else {
|
||||
throw new JsonParseException("Unexpected first token: " + firstToken);
|
||||
}
|
||||
}
|
||||
|
||||
if (modList == null || modList.isEmpty())
|
||||
throw new IOException("Mod " + modFile + " `mcmod.info` is malformed..");
|
||||
throw new IOException("Mod " + modFile + " `mcmod.info` is malformed");
|
||||
ForgeOldModMetadata metadata = modList.get(0);
|
||||
String authors = metadata.getAuthor();
|
||||
if (StringUtils.isBlank(authors) && metadata.getAuthors().length > 0)
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.mod.modinfo;
|
||||
|
||||
import org.jackhuang.hmcl.util.gson.JsonSerializable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/// @author Glavo
|
||||
@JsonSerializable
|
||||
public record ForgeOldModMetadataLst(
|
||||
int modListVersion,
|
||||
List<ForgeOldModMetadata> modList) {
|
||||
}
|
||||
@@ -45,16 +45,6 @@ import static org.jackhuang.hmcl.util.Lang.threadPool;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public abstract class FetchTask<T> extends Task<T> {
|
||||
private static final HttpClient HTTP_CLIENT;
|
||||
|
||||
static {
|
||||
boolean useHttp2 = !"false".equalsIgnoreCase(System.getProperty("hmcl.http2"));
|
||||
|
||||
HTTP_CLIENT = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofMillis(NetworkUtils.TIME_OUT))
|
||||
.version(useHttp2 ? HttpClient.Version.HTTP_2 : HttpClient.Version.HTTP_1_1)
|
||||
.build();
|
||||
}
|
||||
|
||||
protected static final int DEFAULT_RETRY = 3;
|
||||
|
||||
@@ -221,7 +211,7 @@ public abstract class FetchTask<T> extends Task<T> {
|
||||
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(currentURI);
|
||||
requestBuilder.timeout(Duration.ofMillis(NetworkUtils.TIME_OUT));
|
||||
headers.forEach(requestBuilder::header);
|
||||
response = HTTP_CLIENT.send(requestBuilder.build(), BODY_HANDLER);
|
||||
response = Holder.HTTP_CLIENT.send(requestBuilder.build(), BODY_HANDLER);
|
||||
|
||||
bmclapiHash = response.headers().firstValue("x-bmclapi-hash").orElse(null);
|
||||
if (DigestUtils.isSha1Digest(bmclapiHash)) {
|
||||
@@ -509,4 +499,21 @@ public abstract class FetchTask<T> extends Task<T> {
|
||||
public static int getDownloadExecutorConcurrency() {
|
||||
return downloadExecutorConcurrency;
|
||||
}
|
||||
|
||||
/// Ensure that [#HTTP_CLIENT] is initialized after ProxyManager has been initialized.
|
||||
private static final class Holder {
|
||||
private static final HttpClient HTTP_CLIENT;
|
||||
|
||||
static {
|
||||
boolean useHttp2 = !"false".equalsIgnoreCase(System.getProperty("hmcl.http2"));
|
||||
|
||||
HTTP_CLIENT = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofMillis(NetworkUtils.TIME_OUT))
|
||||
.version(useHttp2 ? HttpClient.Version.HTTP_2 : HttpClient.Version.HTTP_1_1)
|
||||
.build();
|
||||
}
|
||||
|
||||
private Holder() {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,7 @@ public final class StringUtils {
|
||||
builder.append(str, start, i);
|
||||
}
|
||||
builder.append(' ');
|
||||
i = whitespaceEnd ;
|
||||
i = whitespaceEnd;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -261,6 +261,19 @@ public final class StringUtils {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean containsEmoji(String str) {
|
||||
for (int i = 0; i < str.length(); ) {
|
||||
int ch = str.codePointAt(i);
|
||||
|
||||
if (ch >= 0x1F300 && ch <= 0x1FAFF)
|
||||
return true;
|
||||
|
||||
i += Character.charCount(ch);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isVarNameStart(char ch) {
|
||||
return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_';
|
||||
}
|
||||
|
||||
@@ -27,14 +27,7 @@ import org.jetbrains.annotations.Unmodifiable;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.stream.Stream;
|
||||
@@ -55,6 +48,7 @@ public final class LocaleUtils {
|
||||
|
||||
private static final Map<String, String> subLanguageToParent = new HashMap<>();
|
||||
private static final Map<String, String> iso3To2 = new HashMap<>();
|
||||
private static final Set<String> rtl = new HashSet<>();
|
||||
|
||||
static {
|
||||
try {
|
||||
@@ -96,6 +90,17 @@ public final class LocaleUtils {
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to load iso_languages.csv", e);
|
||||
}
|
||||
|
||||
try {
|
||||
for (String line : Lang.toIterable(IOUtils.readFullyAsString(LocaleUtils.class.getResourceAsStream("/assets/lang/rtl.txt")).lines())) {
|
||||
if (line.startsWith("#") || line.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
rtl.add(line.trim());
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to load rtl.txt", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Locale getInstance(String language, String script, String region,
|
||||
@@ -163,6 +168,21 @@ public final class LocaleUtils {
|
||||
return locale.getScript();
|
||||
}
|
||||
|
||||
public static @NotNull TextDirection getTextDirection(Locale locale) {
|
||||
TextDirection direction = rtl.contains(getRootLanguage(locale))
|
||||
? TextDirection.RIGHT_TO_LEFT
|
||||
: TextDirection.LEFT_TO_RIGHT;
|
||||
|
||||
if ("Qabs".equals(getScript(locale))) {
|
||||
direction = switch (direction) {
|
||||
case RIGHT_TO_LEFT -> TextDirection.LEFT_TO_RIGHT;
|
||||
case LEFT_TO_RIGHT -> TextDirection.RIGHT_TO_LEFT;
|
||||
};
|
||||
}
|
||||
|
||||
return direction;
|
||||
}
|
||||
|
||||
private static final ConcurrentMap<Locale, List<Locale>> CANDIDATE_LOCALES = new ConcurrentHashMap<>();
|
||||
|
||||
public static @NotNull List<Locale> getCandidateLocales(Locale locale) {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 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.i18n;
|
||||
|
||||
/// @author Glavo
|
||||
public enum TextDirection {
|
||||
LEFT_TO_RIGHT,
|
||||
RIGHT_TO_LEFT;
|
||||
}
|
||||
@@ -1,37 +1,31 @@
|
||||
package org.jackhuang.hmcl.util.platform;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public final class Platform {
|
||||
public record Platform(OperatingSystem os, Architecture arch) {
|
||||
public static final Platform UNKNOWN = new Platform(OperatingSystem.UNKNOWN, Architecture.UNKNOWN);
|
||||
|
||||
public static final Platform WINDOWS_X86 = new Platform(OperatingSystem.WINDOWS, Architecture.X86);
|
||||
public static final Platform WINDOWS_X86_64 = new Platform(OperatingSystem.WINDOWS, Architecture.X86_64);
|
||||
public static final Platform WINDOWS_ARM64 = new Platform(OperatingSystem.WINDOWS, Architecture.ARM64);
|
||||
|
||||
public static final Platform LINUX_X86 = new Platform(OperatingSystem.LINUX, Architecture.X86);
|
||||
public static final Platform LINUX_X86_64 = new Platform(OperatingSystem.LINUX, Architecture.X86_64);
|
||||
public static final Platform LINUX_ARM64 = new Platform(OperatingSystem.LINUX, Architecture.ARM64);
|
||||
public static final Platform LINUX_ARM32 = new Platform(OperatingSystem.LINUX, Architecture.ARM32);
|
||||
public static final Platform LINUX_RISCV64 = new Platform(OperatingSystem.LINUX, Architecture.RISCV64);
|
||||
public static final Platform LINUX_LOONGARCH64 = new Platform(OperatingSystem.LINUX, Architecture.LOONGARCH64);
|
||||
public static final Platform LINUX_LOONGARCH64_OW = new Platform(OperatingSystem.LINUX, Architecture.LOONGARCH64_OW);
|
||||
public static final Platform LINUX_MIPS64EL = new Platform(OperatingSystem.LINUX, Architecture.MIPS64EL);
|
||||
|
||||
public static final Platform MACOS_X86_64 = new Platform(OperatingSystem.MACOS, Architecture.X86_64);
|
||||
public static final Platform MACOS_ARM64 = new Platform(OperatingSystem.MACOS, Architecture.ARM64);
|
||||
|
||||
public static final Platform FREEBSD_X86_64 = new Platform(OperatingSystem.FREEBSD, Architecture.X86);
|
||||
|
||||
public static final Platform CURRENT_PLATFORM = Platform.getPlatform(OperatingSystem.CURRENT_OS, Architecture.CURRENT_ARCH);
|
||||
public static final Platform SYSTEM_PLATFORM = Platform.getPlatform(OperatingSystem.CURRENT_OS, Architecture.SYSTEM_ARCH);
|
||||
|
||||
public static boolean isCompatibleWithX86Java() {
|
||||
return Architecture.SYSTEM_ARCH.isX86() || SYSTEM_PLATFORM == MACOS_ARM64 || SYSTEM_PLATFORM == WINDOWS_ARM64;
|
||||
}
|
||||
|
||||
private final OperatingSystem os;
|
||||
private final Architecture arch;
|
||||
|
||||
private Platform(OperatingSystem os, Architecture arch) {
|
||||
this.os = os;
|
||||
this.arch = arch;
|
||||
}
|
||||
|
||||
public static Platform getPlatform() {
|
||||
return CURRENT_PLATFORM;
|
||||
return Architecture.SYSTEM_ARCH.isX86() || SYSTEM_PLATFORM.equals(MACOS_ARM64) || SYSTEM_PLATFORM.equals(WINDOWS_ARM64);
|
||||
}
|
||||
|
||||
public static Platform getPlatform(OperatingSystem os, Architecture arch) {
|
||||
@@ -74,23 +68,10 @@ public final class Platform {
|
||||
return arch.getBits();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(os, arch);
|
||||
}
|
||||
|
||||
public boolean equals(OperatingSystem os, Architecture arch) {
|
||||
return this.os == os && this.arch == arch;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof Platform)) return false;
|
||||
Platform platform = (Platform) o;
|
||||
return os == platform.os && arch == platform.arch;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return os.getCheckedName() + "-" + arch.getCheckedName();
|
||||
|
||||
@@ -846,4 +846,6 @@
|
||||
1.21.9-pre3
|
||||
1.21.9-pre4
|
||||
1.21.9-rc1
|
||||
1.21.9
|
||||
1.21.9
|
||||
1.21.10-rc1
|
||||
1.21.10
|
||||
@@ -0,0 +1,6 @@
|
||||
ar
|
||||
fa
|
||||
he
|
||||
ps
|
||||
ur
|
||||
yi
|
||||
@@ -68,4 +68,10 @@ _‾
|
||||
!¡
|
||||
//
|
||||
\\
|
||||
',
|
||||
',
|
||||
()
|
||||
)(
|
||||
[]
|
||||
][
|
||||
{}
|
||||
}{
|
||||
@@ -220,4 +220,18 @@ public final class LocaleUtilsTest {
|
||||
assertNull(LocaleUtils.getParentLanguage("zh"));
|
||||
assertNull(LocaleUtils.getParentLanguage("zho"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetTextDirection() {
|
||||
assertEquals(TextDirection.LEFT_TO_RIGHT, LocaleUtils.getTextDirection(Locale.forLanguageTag("en")));
|
||||
assertEquals(TextDirection.LEFT_TO_RIGHT, LocaleUtils.getTextDirection(Locale.forLanguageTag("en-US")));
|
||||
assertEquals(TextDirection.LEFT_TO_RIGHT, LocaleUtils.getTextDirection(Locale.forLanguageTag("zh")));
|
||||
assertEquals(TextDirection.LEFT_TO_RIGHT, LocaleUtils.getTextDirection(Locale.forLanguageTag("zh-Hans")));
|
||||
assertEquals(TextDirection.LEFT_TO_RIGHT, LocaleUtils.getTextDirection(Locale.forLanguageTag("zh-CN")));
|
||||
assertEquals(TextDirection.LEFT_TO_RIGHT, LocaleUtils.getTextDirection(Locale.forLanguageTag("ar-Qabs")));
|
||||
|
||||
assertEquals(TextDirection.RIGHT_TO_LEFT, LocaleUtils.getTextDirection(Locale.forLanguageTag("en-Qabs")));
|
||||
assertEquals(TextDirection.RIGHT_TO_LEFT, LocaleUtils.getTextDirection(Locale.forLanguageTag("ar")));
|
||||
assertEquals(TextDirection.RIGHT_TO_LEFT, LocaleUtils.getTextDirection(Locale.forLanguageTag("ara")));
|
||||
}
|
||||
}
|
||||
@@ -23,11 +23,22 @@ import java.util.Locale;
|
||||
/// @author Glavo
|
||||
public enum DocumentLocale {
|
||||
ENGLISH(Locale.ENGLISH, "") {
|
||||
@Override
|
||||
public String getSubLanguageDisplayName() {
|
||||
return "Standard";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DocumentLocale> getCandidates() {
|
||||
return List.of(ENGLISH);
|
||||
}
|
||||
},
|
||||
ENGLISH_UPSIDE_DOWN(Locale.forLanguageTag("en-Qabs"), "en-Qabs") {
|
||||
@Override
|
||||
public String getSubLanguageDisplayName() {
|
||||
return "uʍoᗡ ǝpᴉsd∩";
|
||||
}
|
||||
},
|
||||
SIMPLIFIED_CHINESE(Locale.forLanguageTag("zh-Hans"), "zh"),
|
||||
TRADITIONAL_CHINESE("zh-Hant") {
|
||||
@Override
|
||||
|
||||
@@ -63,6 +63,14 @@ public abstract class CheckTranslations extends DefaultTask {
|
||||
|
||||
checker.checkMisspelled(simplifiedChinese, key, value, "账户", "帐户");
|
||||
checker.checkMisspelled(simplifiedChinese, key, value, "其他", "其它");
|
||||
|
||||
checker.checkMisspelled(simplifiedChinese, key, value, "(", "(");
|
||||
checker.checkMisspelled(simplifiedChinese, key, value, ")", ")");
|
||||
});
|
||||
|
||||
traditionalChinese.forEach((key, value) -> {
|
||||
checker.checkMisspelled(traditionalChinese, key, value, "(", "(");
|
||||
checker.checkMisspelled(traditionalChinese, key, value, ")", ")");
|
||||
});
|
||||
|
||||
classicalChinese.forEach((key, value) -> {
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.gradle.api.file.RegularFileProperty;
|
||||
import org.gradle.api.tasks.InputFile;
|
||||
import org.gradle.api.tasks.OutputFile;
|
||||
import org.gradle.api.tasks.TaskAction;
|
||||
import org.jackhuang.hmcl.gradle.utils.PropertiesUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@@ -53,10 +54,7 @@ public abstract class UpsideDownTranslate extends DefaultTask {
|
||||
Path inputFile = getInputFile().get().getAsFile().toPath();
|
||||
Path outputFile = getOutputFile().get().getAsFile().toPath();
|
||||
|
||||
Properties english = new Properties();
|
||||
try (var reader = Files.newBufferedReader(inputFile)) {
|
||||
english.load(reader);
|
||||
}
|
||||
Properties english = PropertiesUtils.load(inputFile);
|
||||
|
||||
Properties output = new Properties();
|
||||
Translator translator = new Translator();
|
||||
|
||||
@@ -52,8 +52,6 @@ public abstract class ParseModDataTask extends DefaultTask {
|
||||
@OutputFile
|
||||
public abstract RegularFileProperty getOutputFile();
|
||||
|
||||
// ---
|
||||
|
||||
private static final Logger LOGGER = Logging.getLogger(ParseModDataTask.class);
|
||||
|
||||
private static final String S = ";";
|
||||
@@ -94,7 +92,33 @@ public abstract class ParseModDataTask extends DefaultTask {
|
||||
return "";
|
||||
}
|
||||
|
||||
private static final Set<String> skip = Set.of(
|
||||
private static String cleanChineseName(String chineseName) {
|
||||
if (chineseName == null || chineseName.isBlank())
|
||||
return "";
|
||||
|
||||
chineseName = chineseName.trim();
|
||||
|
||||
StringBuilder builder = new StringBuilder(chineseName.length());
|
||||
int[] codePoints = chineseName.codePoints().toArray();
|
||||
for (int i = 0; i < codePoints.length; i++) {
|
||||
int ch = codePoints[i];
|
||||
int prev = i > 0 ? codePoints[i - 1] : 0;
|
||||
|
||||
switch (ch) {
|
||||
case '(' -> {
|
||||
if (Character.isWhitespace(prev) || prev == '!' || prev == '。')
|
||||
builder.append('(');
|
||||
else
|
||||
builder.append(" (");
|
||||
}
|
||||
case ')' -> builder.append(')');
|
||||
default -> builder.appendCodePoint(ch);
|
||||
}
|
||||
}
|
||||
return builder.toString().trim();
|
||||
}
|
||||
|
||||
private static final Set<String> SKIP = Set.of(
|
||||
"Minecraft",
|
||||
"The Building Game"
|
||||
);
|
||||
@@ -106,7 +130,6 @@ public abstract class ParseModDataTask extends DefaultTask {
|
||||
|
||||
Files.createDirectories(outputFile.getParent());
|
||||
|
||||
|
||||
List<ModData> modDatas;
|
||||
try (BufferedReader reader = Files.newBufferedReader(inputFile)) {
|
||||
modDatas = new Gson().fromJson(reader, TypeToken.getParameterized(List.class, ModData.class).getType());
|
||||
@@ -125,14 +148,13 @@ public abstract class ParseModDataTask extends DefaultTask {
|
||||
String subName = mod.name.sub;
|
||||
String abbr = mod.name.abbr;
|
||||
|
||||
if (chineseName == null)
|
||||
chineseName = "";
|
||||
chineseName = chineseName == null ? "" : cleanChineseName(chineseName);
|
||||
if (subName == null)
|
||||
subName = "";
|
||||
if (abbr == null)
|
||||
abbr = "";
|
||||
|
||||
if (skip.contains(subName)) {
|
||||
if (SKIP.contains(subName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -208,7 +230,6 @@ public abstract class ParseModDataTask extends DefaultTask {
|
||||
public Map<String, List<Link>> list;
|
||||
}
|
||||
|
||||
|
||||
public static final class ModIdDeserializer implements JsonDeserializer<List<String>> {
|
||||
private static final Type STRING_LIST = TypeToken.getParameterized(List.class, String.class).getType();
|
||||
|
||||
|
||||
@@ -104,5 +104,17 @@
|
||||
<module name="RedundantImport"/> <!-- e.g. double import statements: http://checkstyle.sourceforge.net/config_imports.html#RedundantImport -->
|
||||
<module name="UnusedImports"/> <!-- http://checkstyle.sourceforge.net/config_imports.html#UnusedImports -->
|
||||
<module name="IllegalImport"/> <!-- checks if import sun.* is used http://checkstyle.sourceforge.net/config_imports.html#IllegalImport -->
|
||||
|
||||
<!-- Custom -->
|
||||
<module name="RegexpSinglelineJava">
|
||||
<property name="format" value="\.toLowerCase\(\)"/>
|
||||
<property name="ignoreComments" value="true"/>
|
||||
<property name="message" value="Should not use String#toLowerCase(), use String#toLowerCase(Locale.ROOT) instead."/>
|
||||
</module>
|
||||
<module name="RegexpSinglelineJava">
|
||||
<property name="format" value="\.toUpperCase\(\)"/>
|
||||
<property name="ignoreComments" value="true"/>
|
||||
<property name="message" value="Should not use String#toUpperCase(), use String#toUpperCase(Locale.ROOT) instead."/>
|
||||
</module>
|
||||
</module>
|
||||
</module>
|
||||
+3
-4
@@ -2,20 +2,19 @@
|
||||
|
||||
<!-- #BEGIN BLOCK -->
|
||||
<!-- #PROPERTY NAME=BADGES -->
|
||||
[](https://ci.huangyuhui.net/job/HMCL)
|
||||

|
||||
[](https://github.com/HMCL-dev/HMCL/releases)
|
||||

|
||||
[](https://discord.gg/jVvC7HfM6U)
|
||||
[](https://docs.hmcl.net/groups.html)
|
||||
<!-- #END BLOCK -->
|
||||
|
||||
<!-- #BEGIN LANGUAGE_SWITCHER -->
|
||||
**English** | 中文 ([简体](README_zh.md), [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | [日本語](README_ja.md) | [español](README_es.md) | [русский](README_ru.md) | [українська](README_uk.md)
|
||||
**English** (**Standard**, [uʍoᗡ ǝpᴉsd∩](README_en_Qabs.md)) | 中文 ([简体](README_zh.md), [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | [日本語](README_ja.md) | [español](README_es.md) | [русский](README_ru.md) | [українська](README_uk.md)
|
||||
<!-- #END LANGUAGE_SWITCHER -->
|
||||
|
||||
## Introduction
|
||||
|
||||
HMCL is an open-source, cross-platform Minecraft launcher that supports Mod Management, Game Customizing, ModLoader Installing (Forge, NeoForge, Fabric, Quilt, LiteLoader, and OptiFine), Modpack Creating, UI Customization, and more.
|
||||
HMCL is an open-source, cross-platform Minecraft launcher that supports Mod Management, Game Customizing, ModLoader Installing (Forge, NeoForge, Cleanroom, Fabric, Quilt, LiteLoader, and OptiFine), Modpack Creating, UI Customization, and more.
|
||||
|
||||
HMCL has amazing cross-platform capabilities. Not only does it run on different operating systems like Windows, Linux, macOS, and FreeBSD, but it also supports various CPU architectures such as x86, ARM, RISC-V, MIPS, and LoongArch. You can easily enjoy Minecraft across different platforms through HMCL.
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# ɹǝɥɔunɐ˥ ¡ʇɟɐɹɔǝuᴉW ollǝH
|
||||
|
||||
[](https://github.com/HMCL-dev/HMCL/releases)
|
||||
|
||||
<!-- #BEGIN BLOCK -->
|
||||
<!-- #PROPERTY PROCESS_LINK=false -->
|
||||
**ɥsᴉlƃuƎ** (**uʍoᗡ ǝpᴉsd∩** '[pɹɐpuɐʇS](README.md))
|
||||
<!-- #END BLOCK -->
|
||||
|
||||
## uoᴉʇɔnpoɹʇuI
|
||||
|
||||
˙[ǝlqɐʇ sᴉɥʇ](PLATFORM.md) oʇ ɹǝɟǝɹ ǝsɐǝld '˥ƆWH ʎq pǝʇɹoddns sǝɹnʇɔǝʇᴉɥɔɹɐ ∩ԀƆ puɐ sɯǝʇsʎs ɹoℲ
|
||||
|
||||
˙˥ƆWH ɥƃnoɹɥʇ sɯɹoɟʇɐld ʇuǝɹǝɟɟᴉp ssoɹɔɐ ʇɟɐɹɔǝuᴉW ʎoɾuǝ ʎlᴉsɐǝ uɐɔ no⅄ ˙ɥɔɹ∀ƃuoo˥ puɐ 'SԀIW 'Λ-ƆSIɹ 'Wɹ∀ '98x sɐ ɥɔns sǝɹnʇɔǝʇᴉɥɔɹɐ ∩ԀƆ snoᴉɹɐʌ sʇɹoddns oslɐ ʇᴉ ʇnq 'pSqǝǝɹℲ puɐ 'SOɔɐɯ 'xnuᴉ˥ 'sʍopuᴉM ǝʞᴉl sɯǝʇsʎs ƃuᴉʇɐɹǝdo ʇuǝɹǝɟɟᴉp uo unɹ ʇᴉ sǝop ʎluo ʇoN ˙sǝᴉʇᴉlᴉqɐdɐɔ ɯɹoɟʇɐld-ssoɹɔ ƃuᴉzɐɯɐ sɐɥ ˥ƆWH
|
||||
|
||||
˙ǝɹoɯ puɐ 'uoᴉʇɐzᴉɯoʇsnƆ I∩ 'ƃuᴉʇɐǝɹƆ ʞɔɐdpoW '(ǝuᴉℲᴉʇdO puɐ 'ɹǝpɐo˥ǝʇᴉ˥ 'ʇlᴉnQ 'ɔᴉɹqɐℲ 'ɯooɹuɐǝlƆ 'ǝƃɹoℲoǝN 'ǝƃɹoℲ) ƃuᴉllɐʇsuI ɹǝpɐo˥poW 'ƃuᴉzᴉɯoʇsnƆ ǝɯɐפ 'ʇuǝɯǝƃɐuɐW poW sʇɹoddns ʇɐɥʇ ɹǝɥɔunɐl ʇɟɐɹɔǝuᴉW ɯɹoɟʇɐld-ssoɹɔ 'ǝɔɹnos-uǝdo uɐ sᴉ ˥ƆWH
|
||||
|
||||
## pɐoluʍoᗡ
|
||||
|
||||
˙ǝʌoqɐ pǝʇsᴉl sǝʇᴉsqǝʍ lɐᴉɔᴉɟɟo ǝɥʇ ɯoɹɟ sǝsɐǝlǝɹ pɐoluʍop oʇ ʎluo pǝpuǝɯɯoɔǝɹ sᴉ ʇᴉ 'ʎɹɐssǝɔǝu ʇou ɥƃnoɥʇl∀
|
||||
|
||||
˙[sǝsɐǝlǝɹ qnHʇᴉפ](https://github.com/HMCL-dev/HMCL/releases) uᴉ ˥ƆWH ɟo uoᴉsɹǝʌ ʇsǝʇɐl ǝɥʇ puᴉɟ oslɐ uɐɔ no⅄
|
||||
|
||||
˙[ǝʇᴉsqǝʍ lɐᴉɔᴉɟɟo](https://hmcl.huangyuhui.net/download) ǝɥʇ ɯoɹɟ uoᴉsɹǝʌ ʇsǝʇɐl ǝɥʇ pɐoluʍop
|
||||
+3
-4
@@ -2,8 +2,7 @@
|
||||
|
||||
<!-- #BEGIN COPY -->
|
||||
<!-- #PROPERTY NAME=BADGES -->
|
||||
[](https://ci.huangyuhui.net/job/HMCL)
|
||||

|
||||
[](https://github.com/HMCL-dev/HMCL/releases)
|
||||

|
||||
[](https://discord.gg/jVvC7HfM6U)
|
||||
[](https://docs.hmcl.net/groups.html)
|
||||
@@ -11,12 +10,12 @@
|
||||
|
||||
|
||||
<!-- #BEGIN LANGUAGE_SWITCHER -->
|
||||
[English](README.md) | 中文 ([简体](README_zh.md), [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | [日本語](README_ja.md) | **español** | [русский](README_ru.md) | [українська](README_uk.md)
|
||||
English ([Standard](README.md), [uʍoᗡ ǝpᴉsd∩](README_en_Qabs.md)) | 中文 ([简体](README_zh.md), [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | [日本語](README_ja.md) | **español** | [русский](README_ru.md) | [українська](README_uk.md)
|
||||
<!-- #END LANGUAGE_SWITCHER -->
|
||||
|
||||
## Introducción
|
||||
|
||||
HMCL es un lanzador de Minecraft de código abierto y multiplataforma que admite la gestión de mods, personalización del juego, instalación de ModLoaders (Forge, NeoForge, Fabric, Quilt, LiteLoader y OptiFine), creación de modpacks, personalización de la interfaz de usuario y más.
|
||||
HMCL es un lanzador de Minecraft de código abierto y multiplataforma que admite la gestión de mods, personalización del juego, instalación de ModLoaders (Forge, NeoForge, Cleanroom, Fabric, Quilt, LiteLoader y OptiFine), creación de modpacks, personalización de la interfaz de usuario y más.
|
||||
|
||||
HMCL tiene increíbles capacidades multiplataforma. No solo funciona en diferentes sistemas operativos como Windows, Linux, macOS y FreeBSD, sino que también es compatible con varias arquitecturas de CPU como x86, ARM, RISC-V, MIPS y LoongArch. Puedes disfrutar fácilmente de Minecraft en diferentes plataformas a través de HMCL.
|
||||
|
||||
|
||||
+3
-4
@@ -2,20 +2,19 @@
|
||||
|
||||
<!-- #BEGIN COPY -->
|
||||
<!-- #PROPERTY NAME=BADGES -->
|
||||
[](https://ci.huangyuhui.net/job/HMCL)
|
||||

|
||||
[](https://github.com/HMCL-dev/HMCL/releases)
|
||||

|
||||
[](https://discord.gg/jVvC7HfM6U)
|
||||
[](https://docs.hmcl.net/groups.html)
|
||||
<!-- #END COPY -->
|
||||
|
||||
<!-- #BEGIN LANGUAGE_SWITCHER -->
|
||||
[English](README.md) | 中文 ([简体](README_zh.md), [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | **日本語** | [español](README_es.md) | [русский](README_ru.md) | [українська](README_uk.md)
|
||||
English ([Standard](README.md), [uʍoᗡ ǝpᴉsd∩](README_en_Qabs.md)) | 中文 ([简体](README_zh.md), [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | **日本語** | [español](README_es.md) | [русский](README_ru.md) | [українська](README_uk.md)
|
||||
<!-- #END LANGUAGE_SWITCHER -->
|
||||
|
||||
## 紹介
|
||||
|
||||
HMCLはオープンソースでクロスプラットフォーム対応のMinecraftランチャーです。Mod管理、ゲームカスタマイズ、Modローダーのインストール(Forge、NeoForge、Fabric、Quilt、LiteLoader、OptiFine)、Modパック作成、UIカスタマイズなど、さまざまな機能をサポートしています。
|
||||
HMCLはオープンソースでクロスプラットフォーム対応のMinecraftランチャーです。Mod管理、ゲームカスタマイズ、Modローダーのインストール(Forge、NeoForge、Cleanroom、Fabric、Quilt、LiteLoader、OptiFine)、Modパック作成、UIカスタマイズなど、さまざまな機能をサポートしています。
|
||||
|
||||
HMCLは優れたクロスプラットフォーム性能を持っています。Windows、Linux、macOS、FreeBSDなどの異なるオペレーティングシステムだけでなく、x86、ARM、RISC-V、MIPS、LoongArchなどのさまざまなCPUアーキテクチャにも対応しています。HMCLを使えば、さまざまなプラットフォームでMinecraftを簡単に楽しむことができます。
|
||||
|
||||
|
||||
+3
-4
@@ -2,20 +2,19 @@
|
||||
|
||||
<!-- #BEGIN COPY -->
|
||||
<!-- #PROPERTY NAME=BADGES -->
|
||||
[](https://ci.huangyuhui.net/job/HMCL)
|
||||

|
||||
[](https://github.com/HMCL-dev/HMCL/releases)
|
||||

|
||||
[](https://discord.gg/jVvC7HfM6U)
|
||||
[](https://docs.hmcl.net/groups.html)
|
||||
<!-- #END COPY -->
|
||||
|
||||
<!-- #BEGIN LANGUAGE_SWITCHER -->
|
||||
[English](README.md) | **中文** ([简体](README_zh.md), [繁體](README_zh_Hant.md), **文言**) | [日本語](README_ja.md) | [español](README_es.md) | [русский](README_ru.md) | [українська](README_uk.md)
|
||||
English ([Standard](README.md), [uʍoᗡ ǝpᴉsd∩](README_en_Qabs.md)) | **中文** ([简体](README_zh.md), [繁體](README_zh_Hant.md), **文言**) | [日本語](README_ja.md) | [español](README_es.md) | [русский](README_ru.md) | [українська](README_uk.md)
|
||||
<!-- #END LANGUAGE_SWITCHER -->
|
||||
|
||||
### 概說
|
||||
|
||||
HMCL 者,開源之礦藝啟者也。能理改囊,善於遊戲之自定,且自動安裝諸如 Forge、NeoForge、Fabric、Quilt、LiteLoader、OptiFine 諸改囊,亦可製作改囊集,界面亦可隨意更易。
|
||||
HMCL 者,開源之礦藝啟者也。能理改囊,善於遊戲之自定,且自動安裝諸如 Forge、NeoForge、Cleanroom、Fabric、Quilt、LiteLoader、OptiFine 諸改囊,亦可製作改囊集,界面亦可隨意更易。
|
||||
|
||||
HMCL 跨域甚廣。無論 Windows、Linux、macOS、FreeBSD 諸常見械綱,抑或 x86、ARM、RISC-V、MIPS、LoongArch 諸大構處理器,皆可運行。
|
||||
君可憑此,於諸算機間自如遊戲。
|
||||
|
||||
+3
-4
@@ -2,20 +2,19 @@
|
||||
|
||||
<!-- #BEGIN COPY -->
|
||||
<!-- #PROPERTY NAME=BADGES -->
|
||||
[](https://ci.huangyuhui.net/job/HMCL)
|
||||

|
||||
[](https://github.com/HMCL-dev/HMCL/releases)
|
||||

|
||||
[](https://discord.gg/jVvC7HfM6U)
|
||||
[](https://docs.hmcl.net/groups.html)
|
||||
<!-- #END COPY -->
|
||||
|
||||
<!-- #BEGIN LANGUAGE_SWITCHER -->
|
||||
[English](README.md) | 中文 ([简体](README_zh.md), [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | [日本語](README_ja.md) | [español](README_es.md) | **русский** | [українська](README_uk.md)
|
||||
English ([Standard](README.md), [uʍoᗡ ǝpᴉsd∩](README_en_Qabs.md)) | 中文 ([简体](README_zh.md), [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | [日本語](README_ja.md) | [español](README_es.md) | **русский** | [українська](README_uk.md)
|
||||
<!-- #END LANGUAGE_SWITCHER -->
|
||||
|
||||
## Введение
|
||||
|
||||
HMCL — это открытый, кроссплатформенный лаунчер для Minecraft с поддержкой управления модами, настройки игры, установки загрузчиков модов (Forge, NeoForge, Fabric, Quilt, LiteLoader и OptiFine), создания модпаков, настройки интерфейса и многого другого.
|
||||
HMCL — это открытый, кроссплатформенный лаунчер для Minecraft с поддержкой управления модами, настройки игры, установки загрузчиков модов (Forge, NeoForge, Cleanroom, Fabric, Quilt, LiteLoader и OptiFine), создания модпаков, настройки интерфейса и многого другого.
|
||||
|
||||
HMCL обладает отличной кроссплатформенностью. Он работает не только на различных операционных системах, таких как Windows, Linux, macOS и FreeBSD, но и поддерживает различные архитектуры процессоров: x86, ARM, RISC-V, MIPS и LoongArch. Благодаря HMCL вы можете легко наслаждаться Minecraft на разных платформах.
|
||||
|
||||
|
||||
+3
-4
@@ -2,20 +2,19 @@
|
||||
|
||||
<!-- #BEGIN COPY -->
|
||||
<!-- #PROPERTY NAME=BADGES -->
|
||||
[](https://ci.huangyuhui.net/job/HMCL)
|
||||

|
||||
[](https://github.com/HMCL-dev/HMCL/releases)
|
||||

|
||||
[](https://discord.gg/jVvC7HfM6U)
|
||||
[](https://docs.hmcl.net/groups.html)
|
||||
<!-- #END COPY -->
|
||||
|
||||
<!-- #BEGIN LANGUAGE_SWITCHER -->
|
||||
[English](README.md) | 中文 ([简体](README_zh.md), [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | [日本語](README_ja.md) | [español](README_es.md) | [русский](README_ru.md) | **українська**
|
||||
English ([Standard](README.md), [uʍoᗡ ǝpᴉsd∩](README_en_Qabs.md)) | 中文 ([简体](README_zh.md), [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | [日本語](README_ja.md) | [español](README_es.md) | [русский](README_ru.md) | **українська**
|
||||
<!-- #END LANGUAGE_SWITCHER -->
|
||||
|
||||
## Вступ
|
||||
|
||||
HMCL — це відкритий, кросплатформний лаунчер для Minecraft, який підтримує керування модами, налаштування гри, встановлення модлоадерів (Forge, NeoForge, Fabric, Quilt, LiteLoader та OptiFine), створення модпаків, налаштування інтерфейсу та багато іншого.
|
||||
HMCL — це відкритий, кросплатформний лаунчер для Minecraft, який підтримує керування модами, налаштування гри, встановлення модлоадерів (Forge, NeoForge, Cleanroom, Fabric, Quilt, LiteLoader та OptiFine), створення модпаків, налаштування інтерфейсу та багато іншого.
|
||||
|
||||
HMCL має чудові кросплатформні можливості. Він працює не лише на різних операційних системах, таких як Windows, Linux, macOS і FreeBSD, а й підтримує різні архітектури процесорів, такі як x86, ARM, RISC-V, MIPS і LoongArch. Ви можете легко насолоджуватися Minecraft на різних платформах за допомогою HMCL.
|
||||
|
||||
|
||||
+3
-4
@@ -2,20 +2,19 @@
|
||||
|
||||
<!-- #BEGIN COPY -->
|
||||
<!-- #PROPERTY NAME=BADGES -->
|
||||
[](https://ci.huangyuhui.net/job/HMCL)
|
||||

|
||||
[](https://github.com/HMCL-dev/HMCL/releases)
|
||||

|
||||
[](https://discord.gg/jVvC7HfM6U)
|
||||
[](https://docs.hmcl.net/groups.html)
|
||||
<!-- #END COPY -->
|
||||
|
||||
<!-- #BEGIN LANGUAGE_SWITCHER -->
|
||||
[English](README.md) | **中文** (**简体**, [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | [日本語](README_ja.md) | [español](README_es.md) | [русский](README_ru.md) | [українська](README_uk.md)
|
||||
English ([Standard](README.md), [uʍoᗡ ǝpᴉsd∩](README_en_Qabs.md)) | **中文** (**简体**, [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | [日本語](README_ja.md) | [español](README_es.md) | [русский](README_ru.md) | [українська](README_uk.md)
|
||||
<!-- #END LANGUAGE_SWITCHER -->
|
||||
|
||||
## 简介
|
||||
|
||||
HMCL 是一款开源、跨平台的 Minecraft 启动器,支持模组管理、游戏自定义、游戏自动安装 (Forge、NeoForge、Fabric、Quilt、LiteLoader 和 OptiFine)、整合包创建、界面自定义等功能。
|
||||
HMCL 是一款开源、跨平台的 Minecraft 启动器,支持模组管理、游戏自定义、游戏自动安装 (Forge、NeoForge、Cleanroom、Fabric、Quilt、LiteLoader 和 OptiFine)、整合包创建、界面自定义等功能。
|
||||
|
||||
HMCL 有着强大的跨平台能力。它不仅支持 Windows、Linux、macOS、FreeBSD 等常见的操作系统,同时也支持 x86、ARM、RISC-V、MIPS、LoongArch 等不同的 CPU 架构。你可以使用 HMCL 在不同平台上轻松地游玩 Minecraft。
|
||||
|
||||
|
||||
@@ -2,20 +2,19 @@
|
||||
|
||||
<!-- #BEGIN COPY -->
|
||||
<!-- #PROPERTY NAME=BADGES -->
|
||||
[](https://ci.huangyuhui.net/job/HMCL)
|
||||

|
||||
[](https://github.com/HMCL-dev/HMCL/releases)
|
||||

|
||||
[](https://discord.gg/jVvC7HfM6U)
|
||||
[](https://docs.hmcl.net/groups.html)
|
||||
<!-- #END COPY -->
|
||||
|
||||
<!-- #BEGIN LANGUAGE_SWITCHER -->
|
||||
[English](README.md) | **中文** ([简体](README_zh.md), **繁體**, [文言](README_lzh.md)) | [日本語](README_ja.md) | [español](README_es.md) | [русский](README_ru.md) | [українська](README_uk.md)
|
||||
English ([Standard](README.md), [uʍoᗡ ǝpᴉsd∩](README_en_Qabs.md)) | **中文** ([简体](README_zh.md), **繁體**, [文言](README_lzh.md)) | [日本語](README_ja.md) | [español](README_es.md) | [русский](README_ru.md) | [українська](README_uk.md)
|
||||
<!-- #END LANGUAGE_SWITCHER -->
|
||||
|
||||
## 簡介
|
||||
|
||||
HMCL 是一款開源、跨平臺的 Minecraft 啟動器,支援模組管理、遊戲客製化、遊戲自動安裝 (Forge、NeoForge、Fabric、Quilt、LiteLoader 和 OptiFine)、模組包建立、介面客製化等功能。
|
||||
HMCL 是一款開源、跨平臺的 Minecraft 啟動器,支援模組管理、遊戲客製化、遊戲自動安裝 (Forge、NeoForge、Cleanroom、Fabric、Quilt、LiteLoader 和 OptiFine)、模組包建立、介面客製化等功能。
|
||||
|
||||
HMCL 有著強大的跨平臺能力。它不僅支援 Windows、Linux、macOS、FreeBSD 等常見的作業系統,同時也支援 x86、ARM、RISC-V、MIPS、LoongArch 等不同的 CPU 架構。你可以使用 HMCL 在不同平臺上輕鬆地遊玩 Minecraft。
|
||||
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
# Hello Minecraft! Launcher Release Schedule
|
||||
|
||||
<!-- #BEGIN LANGUAGE_SWITCHER -->
|
||||
**English** | [中文](ReleaseSchedule_zh.md)
|
||||
<!-- #END LANGUAGE_SWITCHER -->
|
||||
|
||||
This document describes the HMCL version release schedule starting from October 2025.
|
||||
|
||||
## Versioning Rules
|
||||
|
||||
### Version Branches
|
||||
|
||||
HMCL has multiple **version branches**, each named in the format `<major>.<minor>` (e.g., `3.7`).
|
||||
|
||||
The major version is incremented only when there are significant architectural changes in HMCL,
|
||||
while the minor version is incremented regularly according to the release schedule.
|
||||
|
||||
### Version Types
|
||||
|
||||
HMCL has two types of versions: **Stable** and **Development**.
|
||||
|
||||
#### Stable Version
|
||||
|
||||
The HMCL stable version is suitable for users who prioritize software stability.
|
||||
New features are merged into the stable version only after thorough testing.
|
||||
|
||||
The stable version number follows the naming rule `<version branch>.<build number>` (e.g., `3.7.1`).
|
||||
The build number is calculated independently for each version branch.
|
||||
|
||||
#### Development Version
|
||||
|
||||
The HMCL development version is suitable for users who want to experience new features first.
|
||||
The development version includes the latest features and bug fixes,
|
||||
but may also have more issues due to insufficient testing.
|
||||
|
||||
The development version follows the naming rule `<version branch>.0.<build number>` (e.g., `3.7.0.300`).
|
||||
The build number is shared globally across all version branches.
|
||||
|
||||
## Release Channels
|
||||
|
||||
HMCL has two main release channels: **Stable Channel** and **Development Channel**.
|
||||
They are used to release the HMCL stable and development versions, respectively.
|
||||
|
||||
To test HMCL versions before official release, we will push updates to some users in advance.
|
||||
Users can enable the "Preview HMCL releases early" option on the "Settings > General" page to receive preview updates from the corresponding channel.
|
||||
|
||||
## Release Model
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
title HMCL Version Lifecycle (Example)
|
||||
section 3.9
|
||||
Development Phase: a1, 2025-11-15, 30d
|
||||
Preview Phase: a2, after a1, 16d
|
||||
Stable Release: milestone, after a2, 0d
|
||||
Maintenance Phase: a3, after a2, 31d
|
||||
section 3.10
|
||||
Development Phase: b1, after a1, 31d
|
||||
Preview Phase: b2, after b1, 16d
|
||||
Stable Release: milestone, after b2, 0d
|
||||
Maintenance Phase: b3, after b2, 31d
|
||||
section 3.11
|
||||
Development Phase: c1, after b1, 31d
|
||||
Preview Phase: c2, after c1, 16d
|
||||
Stable Release: milestone, after c2, 0d
|
||||
Maintenance Phase: c3, after c2, 30d
|
||||
```
|
||||
|
||||
In general, we release a new version branch every month,
|
||||
and each version branch `x.y` has a lifecycle of two and a half months.
|
||||
|
||||
The lifecycle of each version branch is divided into the following stages:
|
||||
|
||||
1. **Development Phase** (Mid-month M ~ Mid-month M+1)
|
||||
|
||||
During this phase, the version is developed in the Git branch `main`.
|
||||
|
||||
Typically, we release a development version `x.y.0.<build number>` based on this branch every week,
|
||||
which includes all features and bug fixes merged during the week.
|
||||
|
||||
2. **Preview Phase** (Mid-month M+1 ~ End of month M+1)
|
||||
|
||||
In mid-month M+1, we fork the `main` branch to create the `release/x.y` branch,
|
||||
which corresponds to the fixed version branch `x.y`.
|
||||
|
||||
At the same time, the version branch corresponding to the `main` branch is incremented to `x.y♯`,
|
||||
entering the development phase of the next version branch.
|
||||
|
||||
After the fork, no new features will be added to this version branch;
|
||||
only bug fixes and data updates will be made.
|
||||
|
||||
Within a few days, we will release a stable preview version `x.y.1` and
|
||||
push it to users in the stable channel who have enabled preview updates.
|
||||
|
||||
Before the end of month M+1, the stable version corresponding to version branch `x.y` will remain in preview status.
|
||||
If issues are found during the preview, we will release and push new stable preview versions (such as `x.y.2`, `x.y.3`, etc.) after fixing them.
|
||||
|
||||
3. **Stable Release** (End of month M+1)
|
||||
|
||||
At the end of month M+1, if there are no unexpected issues, we will promote the latest stable preview `x.y.z` to the official release and push it to all users in the stable channel.
|
||||
|
||||
4. **Maintenance Phase** (Month M+2)
|
||||
|
||||
In month M+2, version branch `x.y` enters the maintenance phase.
|
||||
We will release stable updates irregularly based on the number and severity of issues fixed,
|
||||
and push them to all users in the stable channel.
|
||||
|
||||
At the end of month M+2, after the official release of the stable version corresponding to version branch `x.y♯`, the lifecycle of version branch `x.y` ends, the Git branch `release/x.y` is
|
||||
archived, and no longer receives updates.
|
||||
|
||||
## Long-Term Support Version Branches
|
||||
|
||||
Some special version branches are selected as Long-Term Support (LTS) branches.
|
||||
Their lifecycle does not end with the official release of the next version branch's stable version.
|
||||
We will continue to backport necessary patches to these branches for a longer period.
|
||||
|
||||
List of Long-Term Support Version Branches:
|
||||
|
||||
| Version Branch | Official Release Date | End of Lifecycle | Current Support Status | Notes |
|
||||
|----------------|:---------------------:|:----------------:|:----------------------:|:-------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| 3.6 | November 23, 2024 | TBD | Supported | This is the last version branch that supports running on Java 8.<br>It is suitable for users on legacy platforms such as Windows XP. |
|
||||
@@ -0,0 +1,106 @@
|
||||
# Hello Minecraft! Launcher 发布计划
|
||||
|
||||
<!-- #BEGIN LANGUAGE_SWITCHER -->
|
||||
[English](ReleaseSchedule.md) | **中文**
|
||||
<!-- #END LANGUAGE_SWITCHER -->
|
||||
|
||||
本文介绍了自 2025 年 10 月起的 HMCL 版本发布计划。
|
||||
|
||||
## 版本规则
|
||||
|
||||
### 版本分支
|
||||
|
||||
HMCL 存在多个**版本分支**,每个版本分支都以 `<主版本号>.<次版本号>` 的形式命名 (例如 `3.7`)。
|
||||
|
||||
其中,主版本号仅在 HMCL 发生重大架构变化时递增,次版本号会根据发布计划定时递增。
|
||||
|
||||
### 版本类型
|
||||
|
||||
HMCL 具有两种版本类型: **稳定版**和**开发版**。
|
||||
|
||||
#### 稳定版
|
||||
|
||||
HMCL 稳定版适合优先追求软件稳定性的用户使用。新功能在经过充分测试后才会被合并到稳定版中。
|
||||
|
||||
HMCL 稳定版版本号遵循 `<版本分支>.<构建号>` 的命名规则 (例如 `3.7.1`)。其中构建号对每个版本分支独立计算。
|
||||
|
||||
#### 开发版
|
||||
|
||||
HMCL 开发版适合希望优先体验新功能的用户使用。开发版会包含最新功能和 BUG 修复,但因为未经过充分测试,也可能会存在更多问题。
|
||||
|
||||
HMCL 开发版遵循 `<版本分支>.0.<构建号>` 的命名规则 (例如 `3.7.0.300`)。其中构建号对所有版本分支全局共享。
|
||||
|
||||
## 发布通道
|
||||
|
||||
HMCL 具有两个主要发布通道:**稳定版通道**和**开发版通道**。
|
||||
它们分别用于发布 HMCL 稳定版和开发版。
|
||||
|
||||
为了在正式发布前测试 HMCL 版本,我们会向部分用户优先推送更新。
|
||||
用户可以在「设置 > 通用」页面中打开「提前预览 HMCL 版本」选项以接收到对应通道的预览更新。
|
||||
|
||||
## 发布模型
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
title HMCL 版本生命周期 (示例)
|
||||
section 3.9
|
||||
开发阶段: a1, 2025-11-15, 30d
|
||||
预览阶段: a2, after a1, 16d
|
||||
稳定版发布: milestone, after a2, 0d
|
||||
维护阶段: a3, after a2, 31d
|
||||
section 3.10
|
||||
开发阶段: b1, after a1, 31d
|
||||
预览阶段: b2, after b1, 16d
|
||||
稳定版发布: milestone, after b2, 0d
|
||||
维护阶段: b3, after b2, 31d
|
||||
section 3.11
|
||||
开发阶段: c1, after b1, 31d
|
||||
预览阶段: c2, after c1, 16d
|
||||
稳定版发布: milestone, after c2, 0d
|
||||
维护阶段: c3, after c2, 30d
|
||||
```
|
||||
|
||||
通常情况下,我们每个月发布一个版本分支,每个版本分支 `x.y` 生命周期为两个半月。
|
||||
|
||||
每个版本分支生命周期分为以下阶段:
|
||||
|
||||
1. **开发阶段** (M 月中旬 ~ M+1 月中旬)
|
||||
|
||||
在此阶段,该版本在 Git 分支 `main` 中进行开发。
|
||||
|
||||
通常情况下,我们每周都会基于该分支发布一个开发版 `x.y.0.<构建号>`,其中包含当周合并的所有功能和错误修复。
|
||||
|
||||
2. **预览阶段** (M+1 月中旬 ~ M+1 月底)
|
||||
|
||||
在 M+1 月中旬,我们会基于 Git 分支 `main` 分叉出分支 `release/x.y`,其对应的版本分支固定为 `x.y`。
|
||||
|
||||
与此同时,Git 分支 `main` 对应的版本分支递增至 `x.y♯`,进入下一个版本分支的开发版阶段。
|
||||
|
||||
在完成分叉后,该版本分支不会新增更多功能,只会进行错误修复和数据更新。
|
||||
|
||||
我们会在数日内发布稳定版预览 `x.y.1`,并将其推送给稳定版通道且接受预览更新的用户。
|
||||
|
||||
在 M+1 月底前,版本分支 `x.y` 对应的稳定版将一直保持预览状态。
|
||||
若在预览过程中发现问题,我们会在修复后发布并推送新稳定版预览 (如 `x.y.2`、`x.y.3` 等)。
|
||||
|
||||
3. **稳定版发布** (M+1 月底)
|
||||
|
||||
在 M+1 月底,如果没有意外情况,我们会将最新的稳定版预览 `x.y.z` 提升为正式版本,推送给稳定版通道的全部用户。
|
||||
|
||||
4. **维护阶段** (M+2 月)
|
||||
|
||||
在 M+2 月,版本分支 `x.y` 进入维护阶段。我们会根据修复的问题数量和严重程度不定期地发布稳定版更新,并推送给稳定版通道的全部用户。
|
||||
|
||||
在 M+2 月底,版本分支 `x.y♯` 对应的稳定版正式发布后,版本分支 `x.y` 生命周期结束,Git 分支 `release/x.y` 被存档,不再接受更新。
|
||||
|
||||
## 长期支持版本分支
|
||||
|
||||
部分特殊版本分支会被我们选择为长期支持 (LTS) 版本分支,
|
||||
其生命周期不会因为下一个版本分支对应的稳定版正式发布而结束。
|
||||
我们会在更长的时间中继续为这些分支移植必要的补丁。
|
||||
|
||||
长期支持版本分支列表:
|
||||
|
||||
| 版本分支 | 正式发布日期 | 生命周期结束日期 | 当前受支持状态 | 备注 |
|
||||
|------|:----------------:|:--------:|:-------:|:---------------------------------------------------------|
|
||||
| 3.6 | 2024 年 11 月 23 日 | 未定 | 支持中 | 这是最后一个支持使用 Java 8 运行的版本分支,<br>该版本分支适用于 Windows XP 等旧平台用户 |
|
||||
@@ -16,7 +16,7 @@ twelvemonkeys = "3.12.0"
|
||||
jna = "5.18.0"
|
||||
pci-ids = "0.4.0"
|
||||
java-info = "1.0"
|
||||
authlib-injector = "1.2.5"
|
||||
authlib-injector = "1.2.6"
|
||||
|
||||
# testing
|
||||
junit = "5.13.4"
|
||||
|
||||
Reference in new issue
Block a user