Compare commits
40
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48417b9b7b | ||
|
|
738000ff39 | ||
|
|
9b5445b686 | ||
|
|
abf58b2c20 | ||
|
|
fd97f6c321 | ||
|
|
abd37a093a | ||
|
|
0f0cf55509 | ||
|
|
95a1496389 | ||
|
|
47cc9ebe2b | ||
|
|
3ebbdb60ef | ||
|
|
e8306ea59a | ||
|
|
6f7c922ac0 | ||
|
|
74abecaae7 | ||
|
|
ad5b74d8c7 | ||
|
|
5f84e8cfeb | ||
|
|
519ed0ad5f | ||
|
|
61096142d5 | ||
|
|
7c7c36f8aa | ||
|
|
dbb614dce1 | ||
|
|
5a4825d419 | ||
|
|
d87cf91cdf | ||
|
|
0782f40fe0 | ||
|
|
b7a5b484b5 | ||
|
|
1e472ea463 | ||
|
|
816a8539ea | ||
|
|
c81e510bfa | ||
|
|
85b68ad135 | ||
|
|
7d5d74d0c6 | ||
|
|
e0ecd5e695 | ||
|
|
5e0687c138 | ||
|
|
b827167a3d | ||
|
|
20711c9d10 | ||
|
|
15c1042991 | ||
|
|
1275e38995 | ||
|
|
312a2e0cde | ||
|
|
35ce95857a | ||
|
|
6b78f56298 | ||
|
|
b43c8ceae5 | ||
|
|
704d4e9cc0 | ||
|
|
96471e3a92 |
No files matched your search
@@ -8,16 +8,16 @@ body:
|
||||
attributes:
|
||||
value: |
|
||||
提交前请确认:
|
||||
* 该问题确实是 **HMCL 的错误**,而**不是 Minecraft 非正常退出**,如果你的 Minecraft 非正常退出,请前往 [QQ 群](https://docs.hmcl.net/groups.html)/[KOOK 频道](https://kook.top/Kx7n3t)/[Discord 频道](https://discord.gg/jVvC7HfM6U) 中获取帮助。
|
||||
* 你的启动器版本是**最新的快照版本**,可以点击 [此处](https://github.com/burningtnt/HMCL-Snapshot-Update/raw/master/datas/HMCL-dev.jar) 下载最新快照版本。
|
||||
* 该问题确实是 **HMCL 的错误**,而**不是 Minecraft 非正常退出**,如果你的 Minecraft 非正常退出,请前往 [QQ 群](https://docs.hmcl.net/groups.html)/[Discord 频道](https://discord.gg/jVvC7HfM6U) 中获取帮助。
|
||||
* 你的启动器版本是**最新的快照版本**,可以点击 [此处](https://zkitefly.github.io/HMCL-Snapshot-Update/) 下载最新快照版本。
|
||||
|
||||
如果你的问题并不属于上述两类,你可以选取另一种 Issue 类型,或者直接前往 [QQ 群](https://docs.hmcl.net/groups.html)/[KOOK 频道](https://kook.top/Kx7n3t)/[Discord 频道](https://discord.gg/jVvC7HfM6U) 中获取帮助。
|
||||
如果你的问题并不属于上述两类,你可以选取另一种 Issue 类型,或者直接前往 [QQ 群](https://docs.hmcl.net/groups.html)/[Discord 频道](https://discord.gg/jVvC7HfM6U) 中获取帮助。
|
||||
|
||||
Before submitting, please confirm:
|
||||
* The issue is indeed **an error of HMCL**, not **Minecraft abnormal exit**. If your Minecraft exits abnormally, please go to the [QQ group](https://docs.hmcl.net/groups.html) or [KOOK channel](https://kook.top/Kx7n3t) or [Discord channel](https://discord.gg/jVvC7HfM6U) for help.
|
||||
* The issue is indeed **an error of HMCL**, not **Minecraft abnormal exit**. If your Minecraft exits abnormally, please go to the [QQ group](https://docs.hmcl.net/groups.html) or [Discord channel](https://discord.gg/jVvC7HfM6U) for help.
|
||||
* Your launcher version is **the latest snapshot version**. You can click [here](https://github.com/burningtnt/HMCL-Snapshot-Update/raw/master/datas/HMCL-dev.jar) to download the latest snapshot version.
|
||||
|
||||
If your issue does not fall into the above two categories, you can choose another type of issue or directly go to the [QQ group](https://docs.hmcl.net/groups.html) or [KOOK channel](https://kook.top/Kx7n3t) or [Discord channel](https://discord.gg/jVvC7HfM6U) for help.
|
||||
If your issue does not fall into the above two categories, you can choose another type of issue or directly go to the [QQ group](https://docs.hmcl.net/groups.html) or [Discord channel](https://discord.gg/jVvC7HfM6U) for help.
|
||||
- type: input
|
||||
id: platform
|
||||
attributes:
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: KOOK 频道 | KOOK Channel
|
||||
url: https://kook.top/Kx7n3t
|
||||
about: Hello Minecraft! Launcher 的官方 KOOK 频道。| The official KOOK channel of Hello Minecraft! Laucher.
|
||||
- name: Discord 频道 | Discord Channel
|
||||
url: https://discord.gg/jVvC7HfM6U
|
||||
about: Hello Minecraft! Launcher 的官方 Discord 频道。| The official Discord channel of Hello Minecraft! Launcher.
|
||||
- name: QQ 群 | QQ Groups
|
||||
url: https://docs.hmcl.net/groups.html
|
||||
about: Hello Minecraft! Launcher 的官方 QQ 交流群。| The official QQ groups of Hello Minecraft! Laucher.
|
||||
- name: Discord 频道 | Discord Channel
|
||||
url: https://discord.gg/jVvC7HfM6U
|
||||
about: Hello Minecraft! Launcher 的官方 Discord 频道。| The official Discord channel of Hello Minecraft! Launcher.
|
||||
- name: 其他反馈 | Others
|
||||
url: https://github.com/HMCL-dev/HMCL/discussions/new/choose
|
||||
about: 通过 Discussions 反馈其他问题。| Report other problems in Discussions.
|
||||
@@ -1,86 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 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.countly;
|
||||
|
||||
import org.jackhuang.hmcl.util.io.HttpRequest;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Calendar;
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
public class Countly {
|
||||
|
||||
private String deviceId;
|
||||
private String endpoint;
|
||||
private String serverURL;
|
||||
|
||||
public void sendMetric(String metrics) throws IOException {
|
||||
HttpRequest.GET(serverURL + endpoint,
|
||||
pair("begin_session", "1"),
|
||||
pair("session_id", "1"),
|
||||
pair("metrics", metrics),
|
||||
pair("device_id", deviceId),
|
||||
pair("timestamp", Long.toString(System.currentTimeMillis())),
|
||||
pair("tz", Integer.toString(getTimezoneOffset())),
|
||||
pair("hour", Integer.toString(currentHour())),
|
||||
pair("dow", Integer.toString(currentDayOfWeek())),
|
||||
pair("app_key", APP_KEY),
|
||||
pair("sdk_name", "java-native"),
|
||||
pair("sdk_version", "20.11.1"))
|
||||
.getString();
|
||||
}
|
||||
|
||||
private static int getTimezoneOffset() {
|
||||
return ZonedDateTime.now().getOffset().getTotalSeconds() / 60;
|
||||
}
|
||||
|
||||
private static String getLocale() {
|
||||
final Locale locale = Locale.getDefault();
|
||||
return locale.getLanguage() + "_" + locale.getCountry();
|
||||
}
|
||||
|
||||
private static int currentHour() {
|
||||
return Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
|
||||
}
|
||||
|
||||
private int currentDayOfWeek() {
|
||||
int day = Calendar.getInstance().get(Calendar.DAY_OF_WEEK);
|
||||
switch (day) {
|
||||
case Calendar.SUNDAY:
|
||||
return 0;
|
||||
case Calendar.MONDAY:
|
||||
return 1;
|
||||
case Calendar.TUESDAY:
|
||||
return 2;
|
||||
case Calendar.WEDNESDAY:
|
||||
return 3;
|
||||
case Calendar.THURSDAY:
|
||||
return 4;
|
||||
case Calendar.FRIDAY:
|
||||
return 5;
|
||||
case Calendar.SATURDAY:
|
||||
return 6;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static final String APP_KEY = "";
|
||||
}
|
||||
@@ -5,14 +5,8 @@ import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.io.File;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public class CrashReport {
|
||||
|
||||
@@ -20,24 +14,16 @@ public class CrashReport {
|
||||
private final Throwable throwable;
|
||||
private final String stackTrace;
|
||||
|
||||
private boolean nonFatal;
|
||||
|
||||
public CrashReport(Thread thread, Throwable throwable) {
|
||||
this.thread = thread;
|
||||
this.throwable = throwable;
|
||||
stackTrace = StringUtils.getStackTrace(throwable);
|
||||
nonFatal = false;
|
||||
}
|
||||
|
||||
public Throwable getThrowable() {
|
||||
return this.throwable;
|
||||
}
|
||||
|
||||
public CrashReport setNonFatal() {
|
||||
nonFatal = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean shouldBeReport() {
|
||||
if (!stackTrace.contains("org.jackhuang"))
|
||||
return false;
|
||||
@@ -48,23 +34,6 @@ public class CrashReport {
|
||||
return true;
|
||||
}
|
||||
|
||||
public Map<String, Object> getMetrics(long runningTime) {
|
||||
return mapOf(
|
||||
pair("_run", runningTime),
|
||||
pair("_app_version", Metadata.VERSION),
|
||||
pair("_os", OperatingSystem.SYSTEM_NAME),
|
||||
pair("_os_version", OperatingSystem.SYSTEM_VERSION),
|
||||
pair("_disk_current", getDiskAvailable()),
|
||||
pair("_disk_total", getDiskTotal()),
|
||||
pair("_ram_current", getMemoryAvailable()),
|
||||
pair("_ram_total", Runtime.getRuntime().maxMemory() / BYTES_IN_MB),
|
||||
pair("_error", stackTrace),
|
||||
pair("_logs", LOG.getLogs()),
|
||||
pair("_name", throwable.getLocalizedMessage()),
|
||||
pair("_nonfatal", nonFatal)
|
||||
);
|
||||
}
|
||||
|
||||
public String getDisplayText() {
|
||||
return "---- Hello Minecraft! Crash Report ----\n" +
|
||||
" Version: " + Metadata.VERSION + "\n" +
|
||||
@@ -82,29 +51,4 @@ public class CrashReport {
|
||||
" JVM Total Memory: " + Runtime.getRuntime().totalMemory() + "\n" +
|
||||
" JVM Free Memory: " + Runtime.getRuntime().freeMemory() + "\n";
|
||||
}
|
||||
|
||||
private static final Long BYTES_IN_MB = 1024L * 1024;
|
||||
|
||||
private static long getMemoryAvailable() {
|
||||
Long total = Runtime.getRuntime().totalMemory();
|
||||
Long availMem = Runtime.getRuntime().freeMemory();
|
||||
return (total - availMem) / BYTES_IN_MB;
|
||||
}
|
||||
|
||||
private static long getDiskAvailable() {
|
||||
long total = 0, free = 0;
|
||||
for (File f : File.listRoots()) {
|
||||
total += f.getTotalSpace();
|
||||
free += f.getUsableSpace();
|
||||
}
|
||||
return (total - free) / BYTES_IN_MB;
|
||||
}
|
||||
|
||||
private static long getDiskTotal() {
|
||||
long total = 0;
|
||||
for (File f : File.listRoots()) {
|
||||
total += f.getTotalSpace();
|
||||
}
|
||||
return total / BYTES_IN_MB;
|
||||
}
|
||||
}
|
||||
@@ -241,7 +241,7 @@ public final class LauncherHelper {
|
||||
launchingStepsPane.fireEvent(new DialogCloseEvent());
|
||||
if (!success) {
|
||||
Exception ex = executor.getException();
|
||||
if (!(ex instanceof CancellationException)) {
|
||||
if (ex != null && !(ex instanceof CancellationException)) {
|
||||
String message;
|
||||
if (ex instanceof ModpackCompletionException) {
|
||||
if (ex.getCause() instanceof FileNotFoundException)
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 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.game;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public enum LoadingState {
|
||||
DEPENDENCIES("launch.state.dependencies"),
|
||||
MODS("launch.state.modpack"),
|
||||
LOGGING_IN("launch.state.logging_in"),
|
||||
LAUNCHING("launch.state.waiting_launching"),
|
||||
DONE("launch.state.done");
|
||||
|
||||
private final String key;
|
||||
|
||||
LoadingState(String key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public String getLocalizedMessage() {
|
||||
return i18n(key);
|
||||
}
|
||||
}
|
||||
@@ -54,17 +54,19 @@ import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
|
||||
import static org.jackhuang.hmcl.util.Lang.immutableListOf;
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
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;
|
||||
|
||||
/**
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class Accounts {
|
||||
private Accounts() {}
|
||||
private Accounts() {
|
||||
}
|
||||
|
||||
private static final AuthlibInjectorArtifactProvider AUTHLIB_INJECTOR_DOWNLOADER = createAuthlibInjectorArtifactProvider();
|
||||
|
||||
private static void triggerAuthlibInjectorUpdateCheck() {
|
||||
if (AUTHLIB_INJECTOR_DOWNLOADER instanceof AuthlibInjectorDownloader) {
|
||||
Schedulers.io().execute(() -> {
|
||||
@@ -87,6 +89,7 @@ public final class Accounts {
|
||||
// ==== login type / account factory mapping ====
|
||||
private static final Map<String, AccountFactory<?>> type2factory = new HashMap<>();
|
||||
private static final Map<AccountFactory<?>, String> factory2type = new HashMap<>();
|
||||
|
||||
static {
|
||||
type2factory.put("offline", FACTORY_OFFLINE);
|
||||
type2factory.put("authlibInjector", FACTORY_AUTHLIB_INJECTOR);
|
||||
@@ -130,7 +133,7 @@ public final class Accounts {
|
||||
private static final String GLOBAL_PREFIX = "$GLOBAL:";
|
||||
private static final ObservableList<Map<Object, Object>> globalAccountStorages = FXCollections.observableArrayList();
|
||||
|
||||
private static final ObservableList<Account> accounts = observableArrayList(account -> new Observable[] { account });
|
||||
private static final ObservableList<Account> accounts = observableArrayList(account -> new Observable[]{account});
|
||||
private static final ObjectProperty<Account> selectedAccount = new SimpleObjectProperty<>(Accounts.class, "selectedAccount");
|
||||
|
||||
/**
|
||||
@@ -465,6 +468,8 @@ public final class Accounts {
|
||||
} else {
|
||||
return i18n("account.methods.microsoft.error.unknown", errorCode);
|
||||
}
|
||||
} else if (exception instanceof MicrosoftService.XBox400Exception) {
|
||||
return i18n("account.methods.microsoft.error.wrong_verify_method");
|
||||
} else if (exception instanceof MicrosoftService.NoMinecraftJavaEditionProfileException) {
|
||||
return i18n("account.methods.microsoft.error.no_character");
|
||||
} else if (exception instanceof MicrosoftService.NoXuiException) {
|
||||
|
||||
@@ -148,7 +148,10 @@ public class Theme {
|
||||
public String[] getStylesheets(String overrideFontFamily) {
|
||||
String css = "/assets/css/blue.css";
|
||||
|
||||
String fontFamily = System.getProperty("hmcl.font.override", overrideFontFamily);
|
||||
String fontFamily = overrideFontFamily == null
|
||||
? System.getProperty("hmcl.font.override", System.getenv("HMCL_FONT"))
|
||||
: overrideFontFamily;
|
||||
|
||||
String fontStyle = null;
|
||||
if (fontFamily == null) {
|
||||
Optional<Font> font = tryLoadFont();
|
||||
|
||||
@@ -337,7 +337,7 @@ public class GameCrashWindow extends Stage {
|
||||
|
||||
TwoLineListItem version = new TwoLineListItem();
|
||||
version.getStyleClass().setAll("two-line-item-second-large");
|
||||
version.setTitle(i18n("archive.game_version"));
|
||||
version.setTitle(i18n("game.version"));
|
||||
version.setSubtitle(GameCrashWindow.this.version.getId());
|
||||
|
||||
TwoLineListItem total_memory = new TwoLineListItem();
|
||||
|
||||
@@ -18,9 +18,10 @@
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
@@ -55,29 +56,77 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
public class InstallerItem extends Control {
|
||||
private final String id;
|
||||
private final VersionIconType iconType;
|
||||
public final StringProperty libraryVersion = new SimpleStringProperty();
|
||||
public final StringProperty incompatibleLibraryName = new SimpleStringProperty();
|
||||
public final StringProperty dependencyName = new SimpleStringProperty();
|
||||
public final BooleanProperty incompatibleWithGame = new SimpleBooleanProperty();
|
||||
public final BooleanProperty removable = new SimpleBooleanProperty();
|
||||
public final BooleanProperty upgradable = new SimpleBooleanProperty(false);
|
||||
public final BooleanProperty installable = new SimpleBooleanProperty(true);
|
||||
public final ObjectProperty<EventHandler<? super MouseEvent>> removeAction = new SimpleObjectProperty<>();
|
||||
public final ObjectProperty<EventHandler<? super MouseEvent>> action = new SimpleObjectProperty<>();
|
||||
private final Style style;
|
||||
private final ObjectProperty<InstalledState> versionProperty = new SimpleObjectProperty<>(this, "version", null);
|
||||
private final ObjectProperty<State> resolvedStateProperty = new SimpleObjectProperty<>(this, "resolvedState", InstallableState.INSTANCE);
|
||||
|
||||
private Style style = Style.LIST_ITEM;
|
||||
private final ObjectProperty<EventHandler<? super MouseEvent>> installActionProperty = new SimpleObjectProperty<>(this, "installAction");
|
||||
private final ObjectProperty<EventHandler<? super MouseEvent>> removeActionProperty = new SimpleObjectProperty<>(this, "removeAction");
|
||||
|
||||
public interface State {
|
||||
}
|
||||
|
||||
public static final class InstallableState implements State {
|
||||
public static final InstallableState INSTANCE = new InstallableState();
|
||||
|
||||
private InstallableState() {
|
||||
}
|
||||
}
|
||||
|
||||
public static final class IncompatibleState implements State {
|
||||
private final String incompatibleItemName;
|
||||
private final String incompatibleItemVersion;
|
||||
|
||||
public IncompatibleState(String incompatibleItemName, String incompatibleItemVersion) {
|
||||
this.incompatibleItemName = incompatibleItemName;
|
||||
this.incompatibleItemVersion = incompatibleItemVersion;
|
||||
}
|
||||
|
||||
public String getIncompatibleItemName() {
|
||||
return incompatibleItemName;
|
||||
}
|
||||
|
||||
public String getIncompatibleItemVersion() {
|
||||
return incompatibleItemVersion;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class InstalledState implements State {
|
||||
private final String version;
|
||||
private final boolean external;
|
||||
private final boolean incompatibleWithGame;
|
||||
|
||||
public InstalledState(String version, boolean external, boolean incompatibleWithGame) {
|
||||
this.version = version;
|
||||
this.external = external;
|
||||
this.incompatibleWithGame = incompatibleWithGame;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public boolean isExternal() {
|
||||
return external;
|
||||
}
|
||||
|
||||
public boolean isIncompatibleWithGame() {
|
||||
return incompatibleWithGame;
|
||||
}
|
||||
}
|
||||
|
||||
public enum Style {
|
||||
LIST_ITEM,
|
||||
CARD,
|
||||
}
|
||||
|
||||
public InstallerItem(LibraryAnalyzer.LibraryType id) {
|
||||
this(id.getPatchId());
|
||||
public InstallerItem(LibraryAnalyzer.LibraryType id, Style style) {
|
||||
this(id.getPatchId(), style);
|
||||
}
|
||||
|
||||
public InstallerItem(String id) {
|
||||
public InstallerItem(String id, Style style) {
|
||||
this.id = id;
|
||||
this.style = style;
|
||||
|
||||
switch (id) {
|
||||
case "game":
|
||||
@@ -109,55 +158,51 @@ public class InstallerItem extends Control {
|
||||
}
|
||||
}
|
||||
|
||||
public void setStyleMode(Style style) {
|
||||
this.style = style;
|
||||
}
|
||||
|
||||
public void setState(String libraryVersion, boolean incompatibleWithGame, boolean removable) {
|
||||
this.libraryVersion.set(libraryVersion);
|
||||
this.incompatibleWithGame.set(incompatibleWithGame);
|
||||
this.removable.set(removable);
|
||||
}
|
||||
|
||||
public String getLibraryId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public ObjectProperty<InstalledState> versionProperty() {
|
||||
return versionProperty;
|
||||
}
|
||||
|
||||
public ObjectProperty<State> resolvedStateProperty() {
|
||||
return resolvedStateProperty;
|
||||
}
|
||||
|
||||
public ObjectProperty<EventHandler<? super MouseEvent>> installActionProperty() {
|
||||
return installActionProperty;
|
||||
}
|
||||
|
||||
public ObjectProperty<EventHandler<? super MouseEvent>> removeActionProperty() {
|
||||
return removeActionProperty;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new InstallerItemSkin(this);
|
||||
}
|
||||
|
||||
public final static class InstallerItemGroup {
|
||||
public final InstallerItem game = new InstallerItem(MINECRAFT);
|
||||
public final InstallerItem fabric = new InstallerItem(FABRIC);
|
||||
public final InstallerItem fabricApi = new InstallerItem(FABRIC_API);
|
||||
public final InstallerItem forge = new InstallerItem(FORGE);
|
||||
public final InstallerItem neoForge = new InstallerItem(NEO_FORGE);
|
||||
public final InstallerItem liteLoader = new InstallerItem(LITELOADER);
|
||||
public final InstallerItem optiFine = new InstallerItem(OPTIFINE);
|
||||
public final InstallerItem quilt = new InstallerItem(QUILT);
|
||||
public final InstallerItem quiltApi = new InstallerItem(QUILT_API);
|
||||
private final InstallerItem game;
|
||||
|
||||
private final InstallerItem[] libraries;
|
||||
|
||||
private final HashMap<InstallerItem, Set<InstallerItem>> incompatibleMap = new HashMap<>();
|
||||
|
||||
private Set<InstallerItem> getIncompatibles(InstallerItem item) {
|
||||
private Set<InstallerItem> getIncompatibles(Map<InstallerItem, Set<InstallerItem>> incompatibleMap, InstallerItem item) {
|
||||
return incompatibleMap.computeIfAbsent(item, it -> new HashSet<>());
|
||||
}
|
||||
|
||||
private void addIncompatibles(InstallerItem item, InstallerItem... others) {
|
||||
Set<InstallerItem> set = getIncompatibles(item);
|
||||
private void addIncompatibles(Map<InstallerItem, Set<InstallerItem>> incompatibleMap, InstallerItem item, InstallerItem... others) {
|
||||
Set<InstallerItem> set = getIncompatibles(incompatibleMap, item);
|
||||
for (InstallerItem other : others) {
|
||||
set.add(other);
|
||||
getIncompatibles(other).add(item);
|
||||
getIncompatibles(incompatibleMap, other).add(item);
|
||||
}
|
||||
}
|
||||
|
||||
private void mutualIncompatible(InstallerItem... items) {
|
||||
private void mutualIncompatible(Map<InstallerItem, Set<InstallerItem>> incompatibleMap, InstallerItem... items) {
|
||||
for (InstallerItem item : items) {
|
||||
Set<InstallerItem> set = getIncompatibles(item);
|
||||
Set<InstallerItem> set = getIncompatibles(incompatibleMap, item);
|
||||
|
||||
for (InstallerItem item2 : items) {
|
||||
if (item2 != item) {
|
||||
@@ -167,43 +212,66 @@ public class InstallerItem extends Control {
|
||||
}
|
||||
}
|
||||
|
||||
public InstallerItemGroup(String gameVersion) {
|
||||
mutualIncompatible(forge, fabric, quilt, neoForge, liteLoader);
|
||||
addIncompatibles(optiFine, fabric, quilt, neoForge);
|
||||
addIncompatibles(fabricApi, forge, quiltApi, neoForge, liteLoader, optiFine);
|
||||
addIncompatibles(quiltApi, forge, fabric, fabricApi, neoForge, liteLoader, optiFine);
|
||||
public InstallerItemGroup(String gameVersion, Style style) {
|
||||
game = new InstallerItem(MINECRAFT, style);
|
||||
InstallerItem fabric = new InstallerItem(FABRIC, style);
|
||||
InstallerItem fabricApi = new InstallerItem(FABRIC_API, style);
|
||||
InstallerItem forge = new InstallerItem(FORGE, style);
|
||||
InstallerItem neoForge = new InstallerItem(NEO_FORGE, style);
|
||||
InstallerItem liteLoader = new InstallerItem(LITELOADER, style);
|
||||
InstallerItem optiFine = new InstallerItem(OPTIFINE, style);
|
||||
InstallerItem quilt = new InstallerItem(QUILT, style);
|
||||
InstallerItem quiltApi = new InstallerItem(QUILT_API, style);
|
||||
|
||||
InvalidationListener listener = o -> {
|
||||
for (Map.Entry<InstallerItem, Set<InstallerItem>> entry : incompatibleMap.entrySet()) {
|
||||
InstallerItem item = entry.getKey();
|
||||
Map<InstallerItem, Set<InstallerItem>> incompatibleMap = new HashMap<>();
|
||||
mutualIncompatible(incompatibleMap, forge, fabric, quilt, neoForge);
|
||||
addIncompatibles(incompatibleMap, liteLoader, fabric, quilt, neoForge);
|
||||
addIncompatibles(incompatibleMap, optiFine, fabric, quilt, neoForge);
|
||||
addIncompatibles(incompatibleMap, fabricApi, forge, quiltApi, neoForge, liteLoader, optiFine);
|
||||
addIncompatibles(incompatibleMap, quiltApi, forge, fabric, fabricApi, neoForge, liteLoader, optiFine);
|
||||
|
||||
String incompatibleId = null;
|
||||
for (InstallerItem other : entry.getValue()) {
|
||||
if (other.libraryVersion.get() != null) {
|
||||
incompatibleId = other.id;
|
||||
break;
|
||||
for (Map.Entry<InstallerItem, Set<InstallerItem>> entry : incompatibleMap.entrySet()) {
|
||||
InstallerItem item = entry.getKey();
|
||||
Set<InstallerItem> incompatibleItems = entry.getValue();
|
||||
|
||||
Observable[] bindings = new Observable[incompatibleItems.size() + 1];
|
||||
bindings[0] = item.versionProperty;
|
||||
int i = 1;
|
||||
for (InstallerItem other : incompatibleItems) {
|
||||
bindings[i++] = other.versionProperty;
|
||||
}
|
||||
|
||||
item.resolvedStateProperty.bind(Bindings.createObjectBinding(() -> {
|
||||
InstalledState itemVersion = item.versionProperty.get();
|
||||
if (itemVersion != null) {
|
||||
return itemVersion;
|
||||
}
|
||||
|
||||
for (InstallerItem other : incompatibleItems) {
|
||||
InstalledState otherVersion = other.versionProperty.get();
|
||||
if (otherVersion != null) {
|
||||
return new IncompatibleState(other.id, otherVersion.version);
|
||||
}
|
||||
}
|
||||
|
||||
item.incompatibleLibraryName.set(incompatibleId);
|
||||
}
|
||||
};
|
||||
for (InstallerItem item : incompatibleMap.keySet()) {
|
||||
item.libraryVersion.addListener(listener);
|
||||
return InstallableState.INSTANCE;
|
||||
}, bindings));
|
||||
}
|
||||
|
||||
fabricApi.dependencyName.bind(Bindings.createStringBinding(() -> {
|
||||
if (fabric.libraryVersion.get() == null) return FABRIC.getPatchId();
|
||||
else return null;
|
||||
}, fabric.libraryVersion));
|
||||
if (gameVersion != null) {
|
||||
game.versionProperty.set(new InstalledState(gameVersion, false, false));
|
||||
}
|
||||
|
||||
quiltApi.dependencyName.bind(Bindings.createStringBinding(() -> {
|
||||
if (quilt.libraryVersion.get() == null) return QUILT.getPatchId();
|
||||
else return null;
|
||||
}, quilt.libraryVersion));
|
||||
InstallerItem[] all = {game, forge, neoForge, liteLoader, optiFine, fabric, fabricApi, quilt, quiltApi};
|
||||
|
||||
for (InstallerItem item : all) {
|
||||
if (!item.resolvedStateProperty.isBound()) {
|
||||
item.resolvedStateProperty.bind(item.versionProperty);
|
||||
}
|
||||
}
|
||||
|
||||
if (gameVersion == null) {
|
||||
this.libraries = new InstallerItem[]{game, forge, neoForge, liteLoader, optiFine, fabric, fabricApi, quilt, quiltApi};
|
||||
this.libraries = all;
|
||||
} else if (GameVersionNumber.compare(gameVersion, "1.13") < 0) {
|
||||
this.libraries = new InstallerItem[]{game, forge, liteLoader, optiFine};
|
||||
} else {
|
||||
@@ -211,15 +279,19 @@ public class InstallerItem extends Control {
|
||||
}
|
||||
}
|
||||
|
||||
public InstallerItem getGame() {
|
||||
return game;
|
||||
}
|
||||
|
||||
public InstallerItem[] getLibraries() {
|
||||
return libraries;
|
||||
}
|
||||
}
|
||||
|
||||
public static class InstallerItemSkin extends SkinBase<InstallerItem> {
|
||||
|
||||
private static final class InstallerItemSkin extends SkinBase<InstallerItem> {
|
||||
private static final PseudoClass LIST_ITEM = PseudoClass.getPseudoClass("list-item");
|
||||
private static final PseudoClass CARD = PseudoClass.getPseudoClass("card");
|
||||
private static final WeakListenerHolder holder = new WeakListenerHolder();
|
||||
|
||||
InstallerItemSkin(InstallerItem control) {
|
||||
super(control);
|
||||
@@ -227,6 +299,7 @@ public class InstallerItem extends Control {
|
||||
Pane pane;
|
||||
if (control.style == Style.CARD) {
|
||||
pane = new VBox();
|
||||
holder.add(FXUtils.onWeakChange(pane.widthProperty(), v -> FXUtils.setLimitHeight(pane, v.doubleValue() * 0.7)));
|
||||
} else {
|
||||
pane = new HBox();
|
||||
}
|
||||
@@ -262,18 +335,25 @@ public class InstallerItem extends Control {
|
||||
pane.getChildren().add(statusLabel);
|
||||
HBox.setHgrow(statusLabel, Priority.ALWAYS);
|
||||
statusLabel.textProperty().bind(Bindings.createStringBinding(() -> {
|
||||
String incompatibleWith = control.incompatibleLibraryName.get();
|
||||
String version = control.libraryVersion.get();
|
||||
if (control.incompatibleWithGame.get()) {
|
||||
return i18n("install.installer.change_version", version);
|
||||
} else if (incompatibleWith != null) {
|
||||
return i18n("install.installer.incompatible", i18n("install.installer." + incompatibleWith));
|
||||
} else if (version == null) {
|
||||
State state = control.resolvedStateProperty.get();
|
||||
|
||||
if (state instanceof InstalledState) {
|
||||
InstalledState s = (InstalledState) state;
|
||||
if (s.incompatibleWithGame) {
|
||||
return i18n("install.installer.change_version", s.version);
|
||||
}
|
||||
if (s.external) {
|
||||
return i18n("install.installer.external_version", s.version);
|
||||
}
|
||||
return i18n("install.installer.version", s.version);
|
||||
} else if (state instanceof InstallableState) {
|
||||
return i18n("install.installer.not_installed");
|
||||
} else if (state instanceof IncompatibleState) {
|
||||
return i18n("install.installer.incompatible", i18n("install.installer." + ((IncompatibleState) state).incompatibleItemName));
|
||||
} else {
|
||||
return i18n("install.installer.version", version);
|
||||
throw new AssertionError("Unknown state type: " + state.getClass());
|
||||
}
|
||||
}, control.incompatibleLibraryName, control.incompatibleWithGame, control.libraryVersion));
|
||||
}, control.resolvedStateProperty));
|
||||
BorderPane.setMargin(statusLabel, new Insets(0, 0, 0, 8));
|
||||
BorderPane.setAlignment(statusLabel, Pos.CENTER_LEFT);
|
||||
|
||||
@@ -282,30 +362,48 @@ public class InstallerItem extends Control {
|
||||
buttonsContainer.setAlignment(Pos.CENTER);
|
||||
pane.getChildren().add(buttonsContainer);
|
||||
|
||||
JFXButton closeButton = new JFXButton();
|
||||
closeButton.setGraphic(SVG.CLOSE.createIcon(Theme.blackFill(), -1, -1));
|
||||
closeButton.getStyleClass().add("toggle-icon4");
|
||||
closeButton.visibleProperty().bind(control.removable);
|
||||
closeButton.managedProperty().bind(closeButton.visibleProperty());
|
||||
closeButton.onMouseClickedProperty().bind(control.removeAction);
|
||||
buttonsContainer.getChildren().add(closeButton);
|
||||
JFXButton removeButton = new JFXButton();
|
||||
removeButton.setGraphic(SVG.CLOSE.createIcon(Theme.blackFill(), -1, -1));
|
||||
removeButton.getStyleClass().add("toggle-icon4");
|
||||
if (control.id.equals(MINECRAFT.getPatchId())) {
|
||||
removeButton.setVisible(false);
|
||||
} else {
|
||||
removeButton.visibleProperty().bind(Bindings.createBooleanBinding(() -> control.resolvedStateProperty.get() instanceof InstalledState, control.resolvedStateProperty));
|
||||
}
|
||||
removeButton.managedProperty().bind(removeButton.visibleProperty());
|
||||
removeButton.onMouseClickedProperty().bind(control.removeActionProperty);
|
||||
buttonsContainer.getChildren().add(removeButton);
|
||||
|
||||
JFXButton arrowButton = new JFXButton();
|
||||
arrowButton.graphicProperty().bind(Bindings.createObjectBinding(() -> control.upgradable.get()
|
||||
? SVG.UPDATE.createIcon(Theme.blackFill(), -1, -1)
|
||||
: SVG.ARROW_RIGHT.createIcon(Theme.blackFill(), -1, -1),
|
||||
control.upgradable));
|
||||
arrowButton.getStyleClass().add("toggle-icon4");
|
||||
arrowButton.visibleProperty().bind(Bindings.createBooleanBinding(
|
||||
() -> control.installable.get() && control.incompatibleLibraryName.get() == null,
|
||||
control.installable, control.incompatibleLibraryName));
|
||||
arrowButton.managedProperty().bind(arrowButton.visibleProperty());
|
||||
arrowButton.onMouseClickedProperty().bind(control.action);
|
||||
buttonsContainer.getChildren().add(arrowButton);
|
||||
JFXButton installButton = new JFXButton();
|
||||
installButton.graphicProperty().bind(Bindings.createObjectBinding(() ->
|
||||
control.resolvedStateProperty.get() instanceof InstallableState ?
|
||||
SVG.ARROW_RIGHT.createIcon(Theme.blackFill(), -1, -1) :
|
||||
SVG.UPDATE.createIcon(Theme.blackFill(), -1, -1),
|
||||
control.resolvedStateProperty
|
||||
));
|
||||
installButton.getStyleClass().add("toggle-icon4");
|
||||
installButton.visibleProperty().bind(Bindings.createBooleanBinding(() -> {
|
||||
if (control.installActionProperty.get() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
FXUtils.onChangeAndOperate(arrowButton.visibleProperty(), clickable -> {
|
||||
State state = control.resolvedStateProperty.get();
|
||||
if (state instanceof InstallableState) {
|
||||
return true;
|
||||
}
|
||||
if (state instanceof InstalledState) {
|
||||
return !((InstalledState) state).external;
|
||||
}
|
||||
|
||||
return false;
|
||||
}, control.resolvedStateProperty, control.installActionProperty));
|
||||
installButton.managedProperty().bind(installButton.visibleProperty());
|
||||
installButton.onMouseClickedProperty().bind(control.installActionProperty);
|
||||
buttonsContainer.getChildren().add(installButton);
|
||||
|
||||
FXUtils.onChangeAndOperate(installButton.visibleProperty(), clickable -> {
|
||||
if (clickable) {
|
||||
container.onMouseClickedProperty().bind(control.action);
|
||||
container.onMouseClickedProperty().bind(control.installActionProperty);
|
||||
pane.setCursor(Cursor.HAND);
|
||||
} else {
|
||||
container.onMouseClickedProperty().unbind();
|
||||
|
||||
@@ -33,7 +33,7 @@ import org.jackhuang.hmcl.util.Lang;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.*;
|
||||
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.MINECRAFT;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
class AdditionalInstallersPage extends InstallersPage {
|
||||
@@ -59,7 +59,7 @@ class AdditionalInstallersPage extends InstallersPage {
|
||||
for (InstallerItem library : group.getLibraries()) {
|
||||
String libraryId = library.getLibraryId();
|
||||
if (libraryId.equals("game")) continue;
|
||||
library.removeAction.set(e -> {
|
||||
library.removeActionProperty().set(e -> {
|
||||
controller.getSettings().put(libraryId, new UpdateInstallerWizardProvider.RemoveVersionAction(libraryId));
|
||||
reload();
|
||||
});
|
||||
@@ -99,12 +99,12 @@ class AdditionalInstallersPage extends InstallersPage {
|
||||
if (!"game".equals(libraryId) && currentGameVersion != null && !currentGameVersion.equals(game) && getVersion(libraryId) == null && alreadyInstalled) {
|
||||
// For third-party libraries, if game version is being changed, and the library is not being reinstalled,
|
||||
// warns the user that we should update the library.
|
||||
library.setState(libraryVersion, /* incompatibleWithGame */ true, /* removable */ true);
|
||||
library.versionProperty().set(new InstallerItem.InstalledState(libraryVersion, false, true));
|
||||
compatible = false;
|
||||
} else if (alreadyInstalled || getVersion(libraryId) != null) {
|
||||
library.setState(libraryVersion, /* incompatibleWithGame */ false, /* removable */ true);
|
||||
library.versionProperty().set(new InstallerItem.InstalledState(libraryVersion, false, false));
|
||||
} else {
|
||||
library.setState(/* libraryVersion */ null, /* incompatibleWithGame */ false, /* removable */ false);
|
||||
library.versionProperty().set(null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +69,6 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
private final TabHeader.Tab<DownloadListPage> modTab = new TabHeader.Tab<>("modTab");
|
||||
private final TabHeader.Tab<DownloadListPage> modpackTab = new TabHeader.Tab<>("modpackTab");
|
||||
private final TabHeader.Tab<DownloadListPage> resourcePackTab = new TabHeader.Tab<>("resourcePackTab");
|
||||
private final TabHeader.Tab<DownloadListPage> customizationTab = new TabHeader.Tab<>("customizationTab");
|
||||
private final TabHeader.Tab<DownloadListPage> worldTab = new TabHeader.Tab<>("worldTab");
|
||||
private final TransitionPane transitionPane = new TransitionPane();
|
||||
private final DownloadNavigator versionPageNavigator = new DownloadNavigator();
|
||||
@@ -80,7 +79,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
newGameTab.setNodeSupplier(loadVersionFor(() -> new VersionsPage(versionPageNavigator, i18n("install.installer.choose", i18n("install.installer.game")), "", DownloadProviders.getDownloadProvider(),
|
||||
"game", versionPageNavigator::onGameSelected)));
|
||||
modpackTab.setNodeSupplier(loadVersionFor(() -> {
|
||||
ModpackDownloadListPage page = new ModpackDownloadListPage(Versions::downloadModpackImpl, false);
|
||||
DownloadListPage page = HMCLLocalizedDownloadListPage.ofModPack(Versions::downloadModpackImpl, false);
|
||||
|
||||
JFXButton installLocalModpackButton = FXUtils.newRaisedButton(i18n("install.modpack"));
|
||||
installLocalModpackButton.setOnAction(e -> Versions.importModpack());
|
||||
@@ -88,9 +87,8 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
page.getActions().add(installLocalModpackButton);
|
||||
return page;
|
||||
}));
|
||||
modTab.setNodeSupplier(loadVersionFor(() -> new ModDownloadListPage((profile, version, file) -> download(profile, version, file, "mods"), true)));
|
||||
resourcePackTab.setNodeSupplier(loadVersionFor(() -> new ResourcePackDownloadListPage((profile, version, file) -> download(profile, version, file, "resourcepacks"), true)));
|
||||
customizationTab.setNodeSupplier(loadVersionFor(() -> new DownloadListPage(CurseForgeRemoteModRepository.CUSTOMIZATIONS)));
|
||||
modTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofMod((profile, version, file) -> download(profile, version, file, "mods"), true)));
|
||||
resourcePackTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofResourcePack((profile, version, file) -> download(profile, version, file, "resourcepacks"), true)));
|
||||
worldTab.setNodeSupplier(loadVersionFor(() -> new DownloadListPage(CurseForgeRemoteModRepository.WORLDS)));
|
||||
tab = new TabHeader(newGameTab, modpackTab, modTab, resourcePackTab, worldTab);
|
||||
|
||||
@@ -129,12 +127,6 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
item.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(resourcePackTab));
|
||||
item.setOnAction(e -> tab.select(resourcePackTab));
|
||||
})
|
||||
// .addNavigationDrawerItem(item -> {
|
||||
// item.setTitle(i18n("download.curseforge.customization"));
|
||||
// item.setLeftGraphic(wrap(SVG::script));
|
||||
// item.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(customizationTab));
|
||||
// item.setOnAction(e -> selectTabIfCurseForgeAvailable(customizationTab));
|
||||
// })
|
||||
.addNavigationDrawerItem(item -> {
|
||||
item.setTitle(i18n("world"));
|
||||
item.setLeftGraphic(wrap(SVG.EARTH));
|
||||
@@ -170,7 +162,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
|
||||
Path runDirectory = profile.getRepository().hasVersion(version) ? profile.getRepository().getRunDirectory(version).toPath() : profile.getRepository().getBaseDirectory().toPath();
|
||||
|
||||
Controllers.prompt(i18n("archive.name"), (result, resolve, reject) -> {
|
||||
Controllers.prompt(i18n("archive.file.name"), (result, resolve, reject) -> {
|
||||
if (!OperatingSystem.isNameValid(result)) {
|
||||
reject.accept(i18n("install.new_game.malformed"));
|
||||
return;
|
||||
@@ -212,9 +204,6 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
if (resourcePackTab.isInitialized()) {
|
||||
resourcePackTab.getNode().loadVersion(profile, null);
|
||||
}
|
||||
if (customizationTab.isInitialized()) {
|
||||
customizationTab.getNode().loadVersion(profile, null);
|
||||
}
|
||||
if (worldTab.isInitialized()) {
|
||||
worldTab.getNode().loadVersion(profile, null);
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ public class InstallersPage extends Control implements WizardPage {
|
||||
|
||||
public InstallersPage(WizardController controller, HMCLGameRepository repository, String gameVersion, DownloadProvider downloadProvider) {
|
||||
this.controller = controller;
|
||||
this.group = new InstallerItem.InstallerItemGroup(gameVersion);
|
||||
this.group = new InstallerItem.InstallerItemGroup(gameVersion, getInstallerItemStyle());
|
||||
|
||||
txtName.getValidators().addAll(
|
||||
new RequiredValidator(),
|
||||
@@ -63,33 +63,31 @@ public class InstallersPage extends Control implements WizardPage {
|
||||
installable.bind(createBooleanBinding(txtName::validate, txtName.textProperty()));
|
||||
txtName.setText(gameVersion);
|
||||
|
||||
group.game.installable.setValue(false);
|
||||
|
||||
for (InstallerItem item : group.getLibraries()) {
|
||||
item.setStyleMode(InstallerItem.Style.CARD);
|
||||
}
|
||||
|
||||
for (InstallerItem library : group.getLibraries()) {
|
||||
String libraryId = library.getLibraryId();
|
||||
if (libraryId.equals(LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId())) continue;
|
||||
library.action.set(e -> {
|
||||
library.installActionProperty().set(e -> {
|
||||
if (LibraryAnalyzer.LibraryType.FABRIC_API.getPatchId().equals(libraryId)) {
|
||||
Controllers.dialog(i18n("install.installer.fabric-api.warning"), i18n("message.warning"), MessageDialogPane.MessageType.WARNING);
|
||||
}
|
||||
|
||||
if (library.incompatibleLibraryName.get() == null)
|
||||
if (!(library.resolvedStateProperty().get() instanceof InstallerItem.IncompatibleState))
|
||||
controller.onNext(new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer." + libraryId)), gameVersion, downloadProvider, libraryId, () -> controller.onPrev(false)));
|
||||
});
|
||||
library.removeAction.set(e -> {
|
||||
library.removeActionProperty().set(e -> {
|
||||
controller.getSettings().remove(libraryId);
|
||||
reload();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected InstallerItem.Style getInstallerItemStyle() {
|
||||
return InstallerItem.Style.CARD;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return i18n("install.new_game");
|
||||
return group.getGame().versionProperty().get().getVersion();
|
||||
}
|
||||
|
||||
private String getVersion(String id) {
|
||||
@@ -100,11 +98,9 @@ public class InstallersPage extends Control implements WizardPage {
|
||||
for (InstallerItem library : group.getLibraries()) {
|
||||
String libraryId = library.getLibraryId();
|
||||
if (controller.getSettings().containsKey(libraryId)) {
|
||||
library.libraryVersion.set(getVersion(libraryId));
|
||||
library.removable.set(true);
|
||||
library.versionProperty().set(new InstallerItem.InstalledState(getVersion(libraryId), false, false));
|
||||
} else {
|
||||
library.libraryVersion.set(null);
|
||||
library.removable.set(false);
|
||||
library.versionProperty().set(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -148,7 +144,7 @@ public class InstallersPage extends Control implements WizardPage {
|
||||
versionNamePane.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
control.txtName.setMaxWidth(300);
|
||||
versionNamePane.getChildren().setAll(new Label(i18n("archive.name")), control.txtName);
|
||||
versionNamePane.getChildren().setAll(new Label(i18n("version.name")), control.txtName);
|
||||
root.setTop(versionNamePane);
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ public abstract class ModpackPage extends SpinnerPane implements WizardPage {
|
||||
|
||||
BorderPane archiveNamePane = new BorderPane();
|
||||
{
|
||||
Label label = new Label(i18n("archive.name"));
|
||||
Label label = new Label(i18n("archive.file.name"));
|
||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
||||
archiveNamePane.setLeft(label);
|
||||
|
||||
|
||||
@@ -59,13 +59,7 @@ public class FeedbackPage extends SpinnerPane {
|
||||
discord.setSubtitle(i18n("feedback.discord.statement"));
|
||||
discord.setExternalLink("https://discord.gg/jVvC7HfM6U");
|
||||
|
||||
IconedTwoLineListItem kookapp = new IconedTwoLineListItem();
|
||||
kookapp.setImage(FXUtils.newBuiltinImage("/assets/img/kookapp.png"));
|
||||
kookapp.setTitle(i18n("feedback.kookapp"));
|
||||
kookapp.setSubtitle(i18n("feedback.kookapp.statement"));
|
||||
kookapp.setExternalLink("https://kook.top/Kx7n3t");
|
||||
|
||||
community.getContent().setAll(users, github, discord, kookapp);
|
||||
community.getContent().setAll(users, github, discord);
|
||||
}
|
||||
|
||||
content.getChildren().addAll(
|
||||
|
||||
@@ -43,7 +43,6 @@ import org.jackhuang.hmcl.mod.RemoteModRepository;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
@@ -52,6 +51,7 @@ import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.AggregatedObservableList;
|
||||
import org.jackhuang.hmcl.util.Holder;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
@@ -64,10 +64,10 @@ import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.selectedItemPropertyFor;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
|
||||
public class DownloadListPage extends Control implements DecoratorPage, VersionPage.VersionLoadable {
|
||||
protected final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>();
|
||||
@@ -87,7 +87,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
protected final ListProperty<String> downloadSources = new SimpleListProperty<>(this, "downloadSources", FXCollections.observableArrayList());
|
||||
protected final StringProperty downloadSource = new SimpleStringProperty();
|
||||
private final WeakListenerHolder listenerHolder = new WeakListenerHolder();
|
||||
private TaskExecutor executor;
|
||||
private int searchID = 0;
|
||||
protected RemoteModRepository repository;
|
||||
|
||||
private Runnable retrySearch;
|
||||
@@ -163,11 +163,8 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
setLoading(true);
|
||||
setFailed(false);
|
||||
|
||||
if (executor != null && !executor.isCancelled()) {
|
||||
executor.cancel();
|
||||
}
|
||||
|
||||
executor = Task.supplyAsync(() -> {
|
||||
int currentSearchID = searchID = searchID + 1;
|
||||
Task.supplyAsync(() -> {
|
||||
Profile.ProfileVersion version = this.version.get();
|
||||
if (StringUtils.isBlank(version.getVersion())) {
|
||||
return userGameVersion;
|
||||
@@ -176,9 +173,11 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
? version.getProfile().getRepository().getGameVersion(version.getVersion()).orElse("")
|
||||
: "";
|
||||
}
|
||||
}).thenApplyAsync(gameVersion ->
|
||||
repository.search(gameVersion, category, pageOffset, 50, searchFilter, sort, RemoteModRepository.SortOrder.DESC)
|
||||
).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
}).thenApplyAsync(gameVersion -> repository.search(gameVersion, category, pageOffset, 50, searchFilter, sort, RemoteModRepository.SortOrder.DESC)).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (searchID != currentSearchID) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
if (exception == null) {
|
||||
items.setAll(result.getResults().collect(Collectors.toList()));
|
||||
@@ -196,7 +195,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
return i18n("curse.category." + category);
|
||||
}
|
||||
|
||||
protected String getLocalizedCategoryIndent(ModDownloadListPageSkin.CategoryIndented category) {
|
||||
private String getLocalizedCategoryIndent(ModDownloadListPageSkin.CategoryIndented category) {
|
||||
return StringUtils.repeats(' ', category.indent * 4) +
|
||||
(category.getCategory() == null
|
||||
? i18n("curse.category.0")
|
||||
@@ -344,13 +343,14 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
sortComboBox.getSelectionModel().select(0);
|
||||
searchPane.addRow(rowIndex++, new Label(i18n("mods.category")), categoryStackPane, new Label(i18n("search.sort")), sortStackPane);
|
||||
|
||||
StringProperty previousSearchFilter = new SimpleStringProperty(this, "Previous Seach Filter", "");
|
||||
IntegerProperty filterID = new SimpleIntegerProperty(this, "Filter ID", 0);
|
||||
IntegerProperty currentFilterID = new SimpleIntegerProperty(this, "Current Filter ID", -1);
|
||||
EventHandler<ActionEvent> searchAction = e -> {
|
||||
if (!previousSearchFilter.get().equals(nameField.getText())) {
|
||||
if (currentFilterID.get() != filterID.get()) {
|
||||
control.pageOffset.set(0);
|
||||
}
|
||||
currentFilterID.set(filterID.get());
|
||||
|
||||
previousSearchFilter.set(nameField.getText());
|
||||
getSkinnable().search(gameVersionField.getSelectionModel().getSelectedItem(),
|
||||
Optional.ofNullable(categoryComboBox.getSelectionModel().getSelectedItem())
|
||||
.map(CategoryIndented::getCategory)
|
||||
@@ -360,60 +360,93 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
sortComboBox.getSelectionModel().getSelectedItem());
|
||||
};
|
||||
|
||||
control.listenerHolder.add(FXUtils.observeWeak(
|
||||
() -> filterID.set(filterID.get() + 1),
|
||||
|
||||
control.downloadSource,
|
||||
gameVersionField.getSelectionModel().selectedItemProperty(),
|
||||
categoryComboBox.getSelectionModel().selectedItemProperty(),
|
||||
nameField.textProperty(),
|
||||
sortComboBox.getSelectionModel().selectedItemProperty()
|
||||
));
|
||||
|
||||
HBox actionsBox = new HBox(8);
|
||||
GridPane.setColumnSpan(actionsBox, 4);
|
||||
actionsBox.setAlignment(Pos.CENTER);
|
||||
{
|
||||
AggregatedObservableList<Node> actions = new AggregatedObservableList<>();
|
||||
|
||||
Holder<Runnable> changeButton = new Holder<>();
|
||||
|
||||
JFXButton firstPageButton = FXUtils.newBorderButton(i18n("search.first_page"));
|
||||
firstPageButton.setOnAction(event -> {
|
||||
control.pageOffset.set(0);
|
||||
changeButton.value.run();
|
||||
searchAction.handle(event);
|
||||
});
|
||||
firstPageButton.setDisable(true);
|
||||
control.pageCount.addListener((observable, oldValue, newValue) -> firstPageButton.setDisable(control.pageCount.get() == -1));
|
||||
|
||||
JFXButton previousPageButton = FXUtils.newBorderButton(i18n("search.previous_page"));
|
||||
previousPageButton.setOnAction(event -> {
|
||||
if (control.pageOffset.get() > 0) {
|
||||
control.pageOffset.set(control.pageOffset.get() - 1);
|
||||
int pageOffset = control.pageOffset.get();
|
||||
if (pageOffset > 0) {
|
||||
control.pageOffset.set(pageOffset - 1);
|
||||
changeButton.value.run();
|
||||
searchAction.handle(event);
|
||||
}
|
||||
});
|
||||
previousPageButton.setDisable(true);
|
||||
control.pageOffset.addListener((observable, oldValue, newValue) -> previousPageButton.setDisable(
|
||||
control.pageCount.get() == -1 || control.pageOffset.get() == 0
|
||||
));
|
||||
|
||||
Label pageOffset = new Label(i18n("search.page_n", 0, "-"));
|
||||
control.pageOffset.addListener((observable, oldValue, newValue) -> pageOffset.setText(i18n(
|
||||
"search.page_n", control.pageOffset.get() + 1, control.pageCount.get() == -1 ? "-" : control.pageCount.getValue().toString()
|
||||
)));
|
||||
control.pageCount.addListener((observable, oldValue, newValue) -> pageOffset.setText(i18n(
|
||||
"search.page_n", control.pageOffset.get() + 1, control.pageCount.get() == -1 ? "-" : control.pageCount.getValue().toString()
|
||||
)));
|
||||
Label pageDescription = new Label();
|
||||
pageDescription.textProperty().bind(Bindings.createStringBinding(() -> {
|
||||
int pageCount = control.pageCount.get();
|
||||
return i18n("search.page_n", control.pageOffset.get() + 1, pageCount == -1 ? "-" : String.valueOf(pageCount));
|
||||
}, control.pageOffset, control.pageCount));
|
||||
|
||||
JFXButton nextPageButton = FXUtils.newBorderButton(i18n("search.next_page"));
|
||||
nextPageButton.setOnAction(event -> {
|
||||
control.pageOffset.set(control.pageOffset.get() + 1);
|
||||
searchAction.handle(event);
|
||||
int nv = control.pageOffset.get() + 1;
|
||||
if (nv < control.pageCount.get()) {
|
||||
control.pageOffset.set(nv);
|
||||
changeButton.value.run();
|
||||
searchAction.handle(event);
|
||||
}
|
||||
});
|
||||
nextPageButton.setDisable(true);
|
||||
control.pageOffset.addListener((observable, oldValue, newValue) -> nextPageButton.setDisable(
|
||||
control.pageCount.get() == -1 || control.pageOffset.get() >= control.pageCount.get() - 1
|
||||
));
|
||||
control.pageCount.addListener((observable, oldValue, newValue) -> nextPageButton.setDisable(
|
||||
control.pageCount.get() == -1 || control.pageOffset.get() >= control.pageCount.get() - 1
|
||||
));
|
||||
|
||||
JFXButton lastPageButton = FXUtils.newBorderButton(i18n("search.last_page"));
|
||||
lastPageButton.setOnAction(event -> {
|
||||
control.pageOffset.set(control.pageCount.get() - 1);
|
||||
changeButton.value.run();
|
||||
searchAction.handle(event);
|
||||
});
|
||||
|
||||
firstPageButton.setDisable(true);
|
||||
previousPageButton.setDisable(true);
|
||||
lastPageButton.setDisable(true);
|
||||
control.pageCount.addListener((observable, oldValue, newValue) -> lastPageButton.setDisable(control.pageCount.get() == -1 || control.pageOffset.get() >= control.pageCount.get() - 1));
|
||||
nextPageButton.setDisable(true);
|
||||
|
||||
changeButton.value = () -> {
|
||||
int pageOffset = control.pageOffset.get();
|
||||
int pageCount = control.pageCount.get();
|
||||
|
||||
boolean disablePrevious = pageOffset == 0;
|
||||
firstPageButton.setDisable(disablePrevious);
|
||||
previousPageButton.setDisable(disablePrevious);
|
||||
|
||||
boolean disableNext = pageOffset == pageCount - 1;
|
||||
nextPageButton.setDisable(disableNext);
|
||||
lastPageButton.setDisable(disableNext || pageCount == -1);
|
||||
};
|
||||
|
||||
FXUtils.onChange(control.pageCount, pageCountN -> {
|
||||
int pageCount = pageCountN.intValue();
|
||||
|
||||
if (pageCount != -1) {
|
||||
if (control.pageOffset.get() + 1 >= pageCount) {
|
||||
control.pageOffset.set(pageCount - 1);
|
||||
}
|
||||
}
|
||||
|
||||
changeButton.value.run();
|
||||
});
|
||||
|
||||
Pane placeholder = new Pane();
|
||||
HBox.setHgrow(placeholder, Priority.SOMETIMES);
|
||||
@@ -421,13 +454,14 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
JFXButton searchButton = FXUtils.newRaisedButton(i18n("search"));
|
||||
searchButton.setOnAction(searchAction);
|
||||
|
||||
actions.appendList(FXCollections.observableArrayList(firstPageButton, previousPageButton, pageOffset, nextPageButton, lastPageButton, placeholder, searchButton));
|
||||
actions.appendList(FXCollections.observableArrayList(firstPageButton, previousPageButton, pageDescription, nextPageButton, lastPageButton, placeholder, searchButton));
|
||||
actions.appendList(control.actions);
|
||||
Bindings.bindContent(actionsBox.getChildren(), actions.getAggregatedList());
|
||||
}
|
||||
|
||||
searchPane.addRow(rowIndex++, actionsBox);
|
||||
|
||||
FXUtils.onChange(control.downloadSource, v -> searchAction.handle(null));
|
||||
nameField.setOnAction(searchAction);
|
||||
gameVersionField.setOnAction(searchAction);
|
||||
categoryComboBox.setOnAction(searchAction);
|
||||
|
||||
@@ -94,22 +94,11 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
}
|
||||
|
||||
private void loadModVersions() {
|
||||
File versionJar = StringUtils.isNotBlank(version.getVersion())
|
||||
? version.getProfile().getRepository().getVersionJar(version.getVersion())
|
||||
: null;
|
||||
|
||||
setLoading(true);
|
||||
setFailed(false);
|
||||
|
||||
Task.supplyAsync(() -> {
|
||||
Stream<RemoteMod.Version> versions = addon.getData().loadVersions(repository);
|
||||
// if (StringUtils.isNotBlank(version.getVersion())) {
|
||||
// Optional<String> gameVersion = GameVersion.minecraftVersion(versionJar);
|
||||
// if (gameVersion.isPresent()) {
|
||||
// return sortVersions(
|
||||
// .filter(file -> file.getGameVersions().contains(gameVersion.get())));
|
||||
// }
|
||||
// }
|
||||
return sortVersions(versions);
|
||||
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception == null) {
|
||||
@@ -286,28 +275,44 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
if (control.version.getProfile() != null && control.version.getVersion() != null) {
|
||||
HMCLGameRepository repository = control.version.getProfile().getRepository();
|
||||
Version game = repository.getResolvedPreservingPatchesVersion(control.version.getVersion());
|
||||
LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(game, repository.getGameVersion(game).orElse(null));
|
||||
libraryAnalyzer.getVersion(LibraryAnalyzer.LibraryType.MINECRAFT).ifPresent(currentGameVersion -> {
|
||||
Set<ModLoaderType> currentGameModLoaders = libraryAnalyzer.getModLoaders();
|
||||
if (control.versions.containsKey(currentGameVersion)) {
|
||||
control.versions.get(currentGameVersion).stream()
|
||||
.filter(version1 -> version1.getLoaders().isEmpty() || version1.getLoaders().stream().anyMatch(currentGameModLoaders::contains))
|
||||
.findFirst()
|
||||
.ifPresent(value -> list.getContent().addAll(
|
||||
ComponentList.createComponentListTitle(i18n("mods.download.recommend", currentGameVersion)),
|
||||
new ModItem(value, control)
|
||||
));
|
||||
String gameVersion = repository.getGameVersion(game).orElse(null);
|
||||
|
||||
if (gameVersion != null) {
|
||||
List<RemoteMod.Version> modVersions = control.versions.get(gameVersion);
|
||||
if (modVersions != null && !modVersions.isEmpty()) {
|
||||
Set<ModLoaderType> targetLoaders = LibraryAnalyzer.analyze(game, gameVersion).getModLoaders();
|
||||
|
||||
resolve:
|
||||
for (RemoteMod.Version modVersion : modVersions) {
|
||||
for (ModLoaderType loader : modVersion.getLoaders()) {
|
||||
if (targetLoaders.contains(loader)) {
|
||||
list.getContent().addAll(
|
||||
ComponentList.createComponentListTitle(i18n("mods.download.recommend", gameVersion)),
|
||||
new ModItem(modVersion, control)
|
||||
);
|
||||
break resolve;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (String gameVersion : control.versions.keys().stream()
|
||||
.sorted(Collections.reverseOrder(GameVersionNumber::compare))
|
||||
.collect(Collectors.toList())) {
|
||||
ComponentList sublist = new ComponentList(() ->
|
||||
control.versions.get(gameVersion).stream()
|
||||
.map(version -> new ModItem(version, control))
|
||||
.collect(Collectors.toList()));
|
||||
List<RemoteMod.Version> versions = control.versions.get(gameVersion);
|
||||
if (versions == null || versions.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ComponentList sublist = new ComponentList(() -> {
|
||||
ArrayList<ModItem> items = new ArrayList<>(versions.size());
|
||||
for (RemoteMod.Version v: versions) {
|
||||
items.add(new ModItem(v, control));
|
||||
}
|
||||
return items;
|
||||
});
|
||||
sublist.getStyleClass().add("no-padding");
|
||||
sublist.setTitle("Minecraft " + gameVersion);
|
||||
|
||||
|
||||
+43
-13
@@ -28,28 +28,60 @@ import java.util.MissingResourceException;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class ResourcePackDownloadListPage extends DownloadListPage {
|
||||
public ResourcePackDownloadListPage(DownloadPage.DownloadCallback callback, boolean versionSelection) {
|
||||
public final class HMCLLocalizedDownloadListPage extends DownloadListPage {
|
||||
public static DownloadListPage ofMod(DownloadPage.DownloadCallback callback, boolean versionSelection) {
|
||||
return new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteModRepository.Type.MOD, CurseForgeRemoteModRepository.MODS, ModrinthRemoteModRepository.MODS);
|
||||
}
|
||||
|
||||
public static DownloadListPage ofCurseForgeMod(DownloadPage.DownloadCallback callback, boolean versionSelection) {
|
||||
return new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteModRepository.Type.MOD, CurseForgeRemoteModRepository.MODS, null);
|
||||
}
|
||||
|
||||
public static DownloadListPage ofModrinthMod(DownloadPage.DownloadCallback callback, boolean versionSelection) {
|
||||
return new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteModRepository.Type.MOD, null, ModrinthRemoteModRepository.MODS);
|
||||
}
|
||||
|
||||
public static DownloadListPage ofModPack(DownloadPage.DownloadCallback callback, boolean versionSelection) {
|
||||
return new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteModRepository.Type.MODPACK, CurseForgeRemoteModRepository.MODPACKS, ModrinthRemoteModRepository.MODPACKS);
|
||||
}
|
||||
|
||||
public static DownloadListPage ofResourcePack(DownloadPage.DownloadCallback callback, boolean versionSelection) {
|
||||
return new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteModRepository.Type.RESOURCE_PACK, CurseForgeRemoteModRepository.RESOURCE_PACKS, ModrinthRemoteModRepository.RESOURCE_PACKS);
|
||||
}
|
||||
|
||||
private HMCLLocalizedDownloadListPage(DownloadPage.DownloadCallback callback, boolean versionSelection, RemoteModRepository.Type type, CurseForgeRemoteModRepository curseForge, ModrinthRemoteModRepository modrinth) {
|
||||
super(null, callback, versionSelection);
|
||||
|
||||
repository = new Repository();
|
||||
repository = new Repository(type, curseForge, modrinth);
|
||||
|
||||
supportChinese.set(true);
|
||||
downloadSources.get().setAll("mods.curseforge", "mods.modrinth");
|
||||
if (CurseForgeRemoteModRepository.isAvailable())
|
||||
if (curseForge != null) {
|
||||
downloadSource.set("mods.curseforge");
|
||||
else
|
||||
} else if (modrinth != null) {
|
||||
downloadSource.set("mods.modrinth");
|
||||
} else {
|
||||
throw new AssertionError("Should not be here.");
|
||||
}
|
||||
}
|
||||
|
||||
private class Repository extends LocalizedRemoteModRepository {
|
||||
private final RemoteModRepository.Type type;
|
||||
private final CurseForgeRemoteModRepository curseForge;
|
||||
private final ModrinthRemoteModRepository modrinth;
|
||||
|
||||
public Repository(Type type, CurseForgeRemoteModRepository curseForge, ModrinthRemoteModRepository modrinth) {
|
||||
this.type = type;
|
||||
this.curseForge = curseForge;
|
||||
this.modrinth = modrinth;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RemoteModRepository getBackedRemoteModRepository() {
|
||||
if ("mods.modrinth".equals(downloadSource.get())) {
|
||||
return ModrinthRemoteModRepository.RESOURCE_PACKS;
|
||||
return modrinth;
|
||||
} else {
|
||||
return CurseForgeRemoteModRepository.RESOURCE_PACKS;
|
||||
return curseForge;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,19 +96,17 @@ public class ResourcePackDownloadListPage extends DownloadListPage {
|
||||
|
||||
@Override
|
||||
public Type getType() {
|
||||
return Type.MOD;
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getLocalizedCategory(String category) {
|
||||
String key;
|
||||
if ("mods.modrinth".equals(downloadSource.get())) {
|
||||
key = "modrinth.category." + category;
|
||||
} else {
|
||||
key = "curse.category." + category;
|
||||
if (category.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
String key = ("mods.modrinth".equals(downloadSource.get()) ? "modrinth" : "curse") + ".category." + category;
|
||||
try {
|
||||
return I18n.getResourceBundle().getString(key);
|
||||
} catch (MissingResourceException e) {
|
||||
@@ -30,7 +30,6 @@ import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.task.TaskListener;
|
||||
import org.jackhuang.hmcl.ui.*;
|
||||
import org.jackhuang.hmcl.ui.download.UpdateInstallerWizardProvider;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
@@ -39,7 +38,6 @@ import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
@@ -74,35 +72,36 @@ public class InstallerListPage extends ListPageBase<InstallerItem> implements Ve
|
||||
|
||||
return LibraryAnalyzer.analyze(profile.getRepository().getResolvedPreservingPatchesVersion(versionId), gameVersion);
|
||||
}).thenAcceptAsync(analyzer -> {
|
||||
Function<String, Runnable> removeAction = libraryId -> () -> {
|
||||
profile.getDependency().removeLibraryAsync(version, libraryId)
|
||||
itemsProperty().clear();
|
||||
|
||||
InstallerItem.InstallerItemGroup group = new InstallerItem.InstallerItemGroup(gameVersion, InstallerItem.Style.LIST_ITEM);
|
||||
|
||||
// Conventional libraries: game, fabric, forge, neoforge, liteloader, optifine
|
||||
for (InstallerItem item : group.getLibraries()) {
|
||||
String libraryId = item.getLibraryId();
|
||||
String libraryVersion = analyzer.getVersion(libraryId).orElse(null);
|
||||
|
||||
if (libraryVersion != null) {
|
||||
item.versionProperty().set(new InstallerItem.InstalledState(
|
||||
libraryVersion,
|
||||
analyzer.getLibraryStatus(libraryId) != LibraryAnalyzer.LibraryMark.LibraryStatus.CLEAR,
|
||||
false
|
||||
));
|
||||
} else {
|
||||
item.versionProperty().set(null);
|
||||
}
|
||||
|
||||
item.installActionProperty().set(e -> {
|
||||
Controllers.getDecorator().startWizard(new UpdateInstallerWizardProvider(profile, gameVersion, version, libraryId, libraryVersion));
|
||||
});
|
||||
|
||||
item.removeActionProperty().set(e -> profile.getDependency().removeLibraryAsync(version, libraryId)
|
||||
.thenComposeAsync(profile.getRepository()::saveAsync)
|
||||
.withComposeAsync(profile.getRepository().refreshVersionsAsync())
|
||||
.withRunAsync(Schedulers.javafx(), () -> loadVersion(this.profile, this.versionId))
|
||||
.start();
|
||||
};
|
||||
.start());
|
||||
|
||||
itemsProperty().clear();
|
||||
|
||||
InstallerItem.InstallerItemGroup group = new InstallerItem.InstallerItemGroup(gameVersion);
|
||||
|
||||
// Conventional libraries: game, fabric, forge, neoforge, liteloader, optifine
|
||||
for (InstallerItem installerItem : group.getLibraries()) {
|
||||
String libraryId = installerItem.getLibraryId();
|
||||
String libraryVersion = analyzer.getVersion(libraryId).orElse(null);
|
||||
installerItem.libraryVersion.set(libraryVersion);
|
||||
installerItem.upgradable.set(libraryVersion != null);
|
||||
installerItem.installable.set(true);
|
||||
installerItem.action.set(e -> {
|
||||
Controllers.getDecorator().startWizard(new UpdateInstallerWizardProvider(profile, gameVersion, version, libraryId, libraryVersion));
|
||||
});
|
||||
boolean removable = !LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId().equals(libraryId) && libraryVersion != null;
|
||||
installerItem.removable.set(removable);
|
||||
if (removable) {
|
||||
Runnable action = removeAction.apply(libraryId);
|
||||
installerItem.removeAction.set(e -> action.run());
|
||||
}
|
||||
itemsProperty().add(installerItem);
|
||||
itemsProperty().add(item);
|
||||
}
|
||||
|
||||
// other third-party libraries which are unable to manage.
|
||||
@@ -114,21 +113,13 @@ public class InstallerListPage extends ListPageBase<InstallerItem> implements Ve
|
||||
if (LibraryAnalyzer.LibraryType.fromPatchId(libraryId) != null)
|
||||
continue;
|
||||
|
||||
Runnable action = removeAction.apply(libraryId);
|
||||
|
||||
InstallerItem installerItem = new InstallerItem(libraryId);
|
||||
installerItem.libraryVersion.set(libraryVersion);
|
||||
installerItem.installable.set(false);
|
||||
installerItem.upgradable.bind(installerItem.installable);
|
||||
installerItem.removable.set(true);
|
||||
installerItem.removeAction.set(e -> action.run());
|
||||
|
||||
if (libraryVersion != null && Lang.test(() -> profile.getDependency().getVersionList(libraryId))) {
|
||||
installerItem.installable.set(true);
|
||||
installerItem.action.set(e -> {
|
||||
Controllers.getDecorator().startWizard(new UpdateInstallerWizardProvider(profile, gameVersion, version, libraryId, libraryVersion));
|
||||
});
|
||||
}
|
||||
InstallerItem installerItem = new InstallerItem(libraryId, InstallerItem.Style.LIST_ITEM);
|
||||
installerItem.versionProperty().set(new InstallerItem.InstalledState(libraryVersion, false, false));
|
||||
installerItem.removeActionProperty().set(e -> profile.getDependency().removeLibraryAsync(version, libraryId)
|
||||
.thenComposeAsync(profile.getRepository()::saveAsync)
|
||||
.withComposeAsync(profile.getRepository().refreshVersionsAsync())
|
||||
.withRunAsync(Schedulers.javafx(), () -> loadVersion(this.profile, this.versionId))
|
||||
.start());
|
||||
|
||||
itemsProperty().add(installerItem);
|
||||
}
|
||||
@@ -175,7 +166,8 @@ public class InstallerListPage extends ListPageBase<InstallerItem> implements Ve
|
||||
@Override
|
||||
protected List<Node> initializeToolbar(InstallerListPage skinnable) {
|
||||
return Collections.singletonList(
|
||||
createToolbarButton2(i18n("install.installer.install_offline"), SVG.PLUS, skinnable::installOffline));
|
||||
createToolbarButton2(i18n("install.installer.install_offline"), SVG.PLUS, skinnable::installOffline)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import org.jackhuang.hmcl.game.LocalizedRemoteModRepository;
|
||||
import org.jackhuang.hmcl.mod.RemoteModRepository;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository;
|
||||
import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class ModDownloadListPage extends DownloadListPage {
|
||||
public ModDownloadListPage(DownloadPage.DownloadCallback callback, boolean versionSelection) {
|
||||
super(null, callback, versionSelection);
|
||||
|
||||
repository = new Repository();
|
||||
|
||||
supportChinese.set(true);
|
||||
downloadSources.get().setAll("mods.curseforge", "mods.modrinth");
|
||||
if (CurseForgeRemoteModRepository.isAvailable())
|
||||
downloadSource.set("mods.curseforge");
|
||||
else
|
||||
downloadSource.set("mods.modrinth");
|
||||
}
|
||||
|
||||
private class Repository extends LocalizedRemoteModRepository {
|
||||
|
||||
@Override
|
||||
protected RemoteModRepository getBackedRemoteModRepository() {
|
||||
if ("mods.modrinth".equals(downloadSource.get())) {
|
||||
return ModrinthRemoteModRepository.MODS;
|
||||
} else {
|
||||
return CurseForgeRemoteModRepository.MODS;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SortType getBackedRemoteModRepositorySortOrder() {
|
||||
if ("mods.modrinth".equals(downloadSource.get())) {
|
||||
return SortType.NAME;
|
||||
} else {
|
||||
return SortType.POPULARITY;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getType() {
|
||||
return Type.MOD;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getLocalizedCategory(String category) {
|
||||
if ("mods.modrinth".equals(downloadSource.get())) {
|
||||
return i18n("modrinth.category." + category);
|
||||
} else {
|
||||
return i18n("curse.category." + category);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getLocalizedOfficialPage() {
|
||||
if ("mods.modrinth".equals(downloadSource.get())) {
|
||||
return i18n("mods.modrinth");
|
||||
} else {
|
||||
return i18n("mods.curseforge");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,10 @@ import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.mod.*;
|
||||
import org.jackhuang.hmcl.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.mod.ModLoaderType;
|
||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||
import org.jackhuang.hmcl.mod.RemoteModRepository;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository;
|
||||
import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
@@ -46,20 +49,21 @@ 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.*;
|
||||
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.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 java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.*;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -67,10 +71,10 @@ import java.util.stream.Collectors;
|
||||
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.logging.Logger.LOG;
|
||||
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;
|
||||
|
||||
class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
|
||||
@@ -252,7 +256,7 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
|
||||
StringBuilder message = new StringBuilder(localModFile.getFileName());
|
||||
if (isNotBlank(localModFile.getGameVersion()))
|
||||
message.append(", ").append(i18n("archive.game_version")).append(": ").append(localModFile.getGameVersion());
|
||||
message.append(", ").append(i18n("game.version")).append(": ").append(localModFile.getGameVersion());
|
||||
if (isNotBlank(localModFile.getAuthors()))
|
||||
message.append(", ").append(i18n("archive.author")).append(": ").append(localModFile.getAuthors());
|
||||
this.message = message.toString();
|
||||
@@ -289,25 +293,65 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
titleContainer.setSpacing(8);
|
||||
|
||||
ImageView imageView = new ImageView();
|
||||
if (StringUtils.isNotBlank(modInfo.getModInfo().getLogoPath())) {
|
||||
Task.supplyAsync(() -> {
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modInfo.getModInfo().getFile())) {
|
||||
Path iconPath = fs.getPath(modInfo.getModInfo().getLogoPath());
|
||||
Task.supplyAsync(() -> {
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modInfo.getModInfo().getFile())) {
|
||||
String logoPath = modInfo.getModInfo().getLogoPath();
|
||||
if (StringUtils.isNotBlank(logoPath)) {
|
||||
Path iconPath = fs.getPath(logoPath);
|
||||
if (Files.exists(iconPath)) {
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
Files.copy(iconPath, stream);
|
||||
return new ByteArrayInputStream(stream.toByteArray());
|
||||
try (InputStream stream = Files.newInputStream(iconPath)) {
|
||||
Image image = new Image(stream, 40, 40, true, true);
|
||||
if (!image.isError() && image.getWidth() == image.getHeight())
|
||||
return image;
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to load image " + logoPath, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}).whenComplete(Schedulers.javafx(), (stream, exception) -> {
|
||||
if (stream != null) {
|
||||
imageView.setImage(new Image(stream, 40, 40, true, true));
|
||||
} else {
|
||||
imageView.setImage(FXUtils.newBuiltinImage("/assets/img/command.png", 40, 40, true, true));
|
||||
|
||||
List<String> defaultPaths = new ArrayList<>(Arrays.asList(
|
||||
"icon.png",
|
||||
"logo.png",
|
||||
"mod_logo.png",
|
||||
"pack.png",
|
||||
"logoFile.png"
|
||||
));
|
||||
|
||||
String id = modInfo.getModInfo().getId();
|
||||
if (StringUtils.isNotBlank(id)) {
|
||||
defaultPaths.addAll(Arrays.asList(
|
||||
"assets/" + id + "/icon.png",
|
||||
"assets/" + id.replace("-", "") + "/icon.png",
|
||||
id + ".png",
|
||||
id + "-logo.png",
|
||||
id + "-icon.png",
|
||||
id + "_logo.png",
|
||||
id + "_icon.png"
|
||||
));
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
for (String path : defaultPaths) {
|
||||
Path iconPath = fs.getPath(path);
|
||||
if (Files.exists(iconPath)) {
|
||||
try (InputStream stream = Files.newInputStream(iconPath)) {
|
||||
Image image = new Image(stream, 40, 40, true, true);
|
||||
if (!image.isError() && image.getWidth() == image.getHeight())
|
||||
return image;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to load icon", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}).whenComplete(Schedulers.javafx(), (image, exception) -> {
|
||||
if (image != null) {
|
||||
imageView.setImage(image);
|
||||
} else {
|
||||
imageView.setImage(FXUtils.newBuiltinImage("/assets/img/command.png", 40, 40, true, true));
|
||||
}
|
||||
}).start();
|
||||
|
||||
TwoLineListItem title = new TwoLineListItem();
|
||||
title.setTitle(modInfo.getModInfo().getName());
|
||||
@@ -323,18 +367,16 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
setBody(description);
|
||||
|
||||
if (StringUtils.isNotBlank(modInfo.getModInfo().getId())) {
|
||||
Lang.<Pair<String, RemoteModRepository>>immutableListOf(
|
||||
for (Pair<String, ? extends RemoteModRepository> item : Arrays.asList(
|
||||
pair("mods.curseforge", CurseForgeRemoteModRepository.MODS),
|
||||
pair("mods.modrinth", ModrinthRemoteModRepository.MODS)
|
||||
).forEach(item -> {
|
||||
String text = item.getKey();
|
||||
RemoteModRepository remoteModRepository = item.getValue();
|
||||
|
||||
JFXHyperlink button = new JFXHyperlink(i18n(text));
|
||||
)) {
|
||||
RemoteModRepository repository = item.getValue();
|
||||
JFXHyperlink button = new JFXHyperlink(i18n(item.getKey()));
|
||||
Task.runAsync(() -> {
|
||||
Optional<RemoteMod.Version> versionOptional = remoteModRepository.getRemoteVersionByLocalFile(modInfo.getModInfo(), modInfo.getModInfo().getFile());
|
||||
Optional<RemoteMod.Version> versionOptional = repository.getRemoteVersionByLocalFile(modInfo.getModInfo(), modInfo.getModInfo().getFile());
|
||||
if (versionOptional.isPresent()) {
|
||||
RemoteMod remoteMod = remoteModRepository.getModById(versionOptional.get().getModid());
|
||||
RemoteMod remoteMod = repository.getModById(versionOptional.get().getModid());
|
||||
FXUtils.runInFX(() -> {
|
||||
for (ModLoaderType modLoaderType : versionOptional.get().getLoaders()) {
|
||||
String loaderName;
|
||||
@@ -357,15 +399,16 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
if (!title.getTags().contains(loaderName)) {
|
||||
title.getTags().add(loaderName);
|
||||
List<String> tags = title.getTags();
|
||||
if (!tags.contains(loaderName)) {
|
||||
tags.add(loaderName);
|
||||
}
|
||||
}
|
||||
|
||||
button.setOnAction(e -> {
|
||||
fireEvent(new DialogCloseEvent());
|
||||
Controllers.navigate(new DownloadPage(
|
||||
new DownloadListPage(remoteModRepository),
|
||||
repository instanceof CurseForgeRemoteModRepository ? HMCLLocalizedDownloadListPage.ofCurseForgeMod(null, false) : HMCLLocalizedDownloadListPage.ofModrinthMod(null, false),
|
||||
remoteMod,
|
||||
new Profile.ProfileVersion(ModListPageSkin.this.getSkinnable().getProfile(), ModListPageSkin.this.getSkinnable().getVersionId()),
|
||||
null
|
||||
@@ -377,7 +420,7 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
}).start();
|
||||
button.setDisable(true);
|
||||
getActions().add(button);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(modInfo.getModInfo().getUrl())) {
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2022 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import org.jackhuang.hmcl.game.LocalizedRemoteModRepository;
|
||||
import org.jackhuang.hmcl.mod.RemoteModRepository;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository;
|
||||
import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class ModpackDownloadListPage extends DownloadListPage {
|
||||
public ModpackDownloadListPage(DownloadPage.DownloadCallback callback, boolean versionSelection) {
|
||||
super(null, callback, versionSelection);
|
||||
|
||||
repository = new Repository();
|
||||
|
||||
supportChinese.set(true);
|
||||
downloadSources.get().setAll("mods.curseforge", "mods.modrinth");
|
||||
if (CurseForgeRemoteModRepository.isAvailable())
|
||||
downloadSource.set("mods.curseforge");
|
||||
else
|
||||
downloadSource.set("mods.modrinth");
|
||||
}
|
||||
|
||||
private class Repository extends LocalizedRemoteModRepository {
|
||||
|
||||
@Override
|
||||
protected RemoteModRepository getBackedRemoteModRepository() {
|
||||
if ("mods.modrinth".equals(downloadSource.get())) {
|
||||
return ModrinthRemoteModRepository.MODPACKS;
|
||||
} else {
|
||||
return CurseForgeRemoteModRepository.MODPACKS;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SortType getBackedRemoteModRepositorySortOrder() {
|
||||
if ("mods.modrinth".equals(downloadSource.get())) {
|
||||
return SortType.NAME;
|
||||
} else {
|
||||
return SortType.POPULARITY;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getType() {
|
||||
return Type.MODPACK;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getLocalizedCategory(String category) {
|
||||
if ("mods.modrinth".equals(downloadSource.get())) {
|
||||
return i18n("modrinth.category." + category);
|
||||
} else {
|
||||
return i18n("curse.category." + category);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getLocalizedOfficialPage() {
|
||||
if ("mods.modrinth".equals(downloadSource.get())) {
|
||||
return i18n("mods.modrinth");
|
||||
} else {
|
||||
return i18n("mods.curseforge");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ import org.jackhuang.hmcl.event.EventBus;
|
||||
import org.jackhuang.hmcl.event.EventPriority;
|
||||
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
|
||||
import org.jackhuang.hmcl.game.GameRepository;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
@@ -164,9 +165,11 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
}
|
||||
|
||||
private void clearAssets() {
|
||||
File baseDirectory = getProfile().getRepository().getBaseDirectory();
|
||||
FileUtils.deleteDirectoryQuietly(new File(baseDirectory, "assets"));
|
||||
FileUtils.deleteDirectoryQuietly(new File(baseDirectory, "resources"));
|
||||
HMCLGameRepository baseDirectory = getProfile().getRepository();
|
||||
FileUtils.deleteDirectoryQuietly(new File(baseDirectory.getBaseDirectory(), "assets"));
|
||||
if (version.get() != null) {
|
||||
FileUtils.deleteDirectoryQuietly(new File(baseDirectory.getRunDirectory(version.get().getVersion()), "resources"));
|
||||
}
|
||||
}
|
||||
|
||||
private void clearJunkFiles() {
|
||||
|
||||
@@ -30,7 +30,9 @@ import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.game.GameDirectoryType;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.ProcessPriority;
|
||||
import org.jackhuang.hmcl.setting.*;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
@@ -184,7 +186,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
gameDirSublist = new ComponentSublist();
|
||||
gameDirSublist.getContent().add(gameDirItem);
|
||||
gameDirSublist.setTitle(i18n("settings.game.working_directory"));
|
||||
gameDirSublist.setHasSubtitle(true);
|
||||
gameDirSublist.setHasSubtitle(versionId != null);
|
||||
gameDirItem.disableProperty().bind(modpack);
|
||||
gameDirCustomOption = new MultiFileItem.FileOption<>(i18n("settings.custom"), GameDirectoryType.CUSTOM)
|
||||
.setChooserTitle(i18n("settings.game.working_directory.choose"))
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.util;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
@@ -19,6 +36,9 @@ import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public final class NativePatcher {
|
||||
private NativePatcher() {
|
||||
}
|
||||
@@ -114,9 +134,10 @@ public final class NativePatcher {
|
||||
if (library.isNative()) {
|
||||
Library replacement = replacements.getOrDefault(library.getName() + ":natives", NONEXISTENT_LIBRARY);
|
||||
if (replacement == NONEXISTENT_LIBRARY) {
|
||||
LOG.warning("No alternative native library " + library.getName() + " provided for platform " + javaVersion.getPlatform());
|
||||
LOG.warning("No alternative native library " + library.getName() + ":natives provided for platform " + javaVersion.getPlatform());
|
||||
newLibraries.add(library);
|
||||
} else if (replacement != null) {
|
||||
LOG.info("Replace " + library.getName() + ":natives with " + replacement.getName());
|
||||
newLibraries.add(replacement);
|
||||
}
|
||||
} else {
|
||||
@@ -124,6 +145,7 @@ public final class NativePatcher {
|
||||
if (replacement == NONEXISTENT_LIBRARY) {
|
||||
newLibraries.add(library);
|
||||
} else if (replacement != null) {
|
||||
LOG.info("Replace " + library.getName() + " with " + replacement.getName());
|
||||
newLibraries.add(replacement);
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.4 KiB |
@@ -107,6 +107,7 @@ account.methods.microsoft.error.country_unavailable=Xbox Live is not available i
|
||||
account.methods.microsoft.error.missing_xbox_account=Your Microsoft account does not have a linked Xbox account yet. Please create one before continuing.
|
||||
account.methods.microsoft.error.no_character=Your account does not own the Minecraft Java Edition.\nThe game profile may not have been created,\nplease click the link above to create it.
|
||||
account.methods.microsoft.error.unknown=Failed to log in, error: %d.
|
||||
account.methods.microsoft.error.wrong_verify_method=Please log in using your account & password on the Microsoft account login page. Please do not use a verification code to log in.
|
||||
account.methods.microsoft.logging_in=Logging in...
|
||||
account.methods.microsoft.hint=Please click on the "login" button, and copy the code shown here later to finish the login process in the opened browser window.\n\
|
||||
\n\
|
||||
@@ -158,8 +159,7 @@ account.username=Username
|
||||
|
||||
archive.author=Author(s)
|
||||
archive.date=Publish Date
|
||||
archive.game_version=Game Version
|
||||
archive.name=Filename
|
||||
archive.file.name=File Name
|
||||
archive.version=Version
|
||||
|
||||
assets.download=Downloading Assets
|
||||
@@ -211,6 +211,7 @@ curse.category.4482=Extra Large
|
||||
curse.category.4472=Tech
|
||||
curse.category.4473=Magic
|
||||
curse.category.5128=Vanilla+
|
||||
curse.category.7418=Horror
|
||||
|
||||
# https://addons-ecs.forgesvc.net/api/v2/category/section/6
|
||||
curse.category.5299=Education
|
||||
@@ -319,7 +320,6 @@ download=Download
|
||||
download.hint=Install games and modpacks or download mods, resource packs and worlds
|
||||
download.code.404=File not found on the remote server: %s
|
||||
download.content=Addons
|
||||
download.curseforge.customization=Shaders, and game customization
|
||||
download.curseforge.unavailable=HMCL nightly build does not support access to CurseForge, please use release version or beta version to download.
|
||||
download.existing=The file cannot be saved because it already exists. You can use 'Save As' to save the file elsewhere.
|
||||
download.external_link=Open Download Website
|
||||
@@ -398,8 +398,6 @@ feedback.discord=Discord
|
||||
feedback.discord.statement=Join our Discord community.
|
||||
feedback.github=GitHub Issue
|
||||
feedback.github.statement=Creating an issue on GitHub.
|
||||
feedback.kookapp=KOOK
|
||||
feedback.kookapp.statement=Join our KOOK community.
|
||||
feedback.qq_group=HMCL User Group
|
||||
feedback.qq_group.statement=Join HMCL user QQ group.
|
||||
|
||||
@@ -415,7 +413,6 @@ folder.saves=Saves
|
||||
folder.screenshots=Screenshots
|
||||
|
||||
game=Game
|
||||
#game.crash.feedback=Is it a common issue that almost everyone has had? Please leave feedback so that we can improve the crash analysis system!
|
||||
game.crash.feedback=<b>Please do not share screenshots of this interface with others! </b> If you ask for help from others, please click to export the game crash information in the lower left corner and send the exported file to others for analysis.
|
||||
game.crash.info=Crash Info
|
||||
game.crash.reason=Crash Cause
|
||||
@@ -618,7 +615,7 @@ install.failed.malformed=The downloaded files are corrupted. You can try fixing
|
||||
install.failed.optifine_conflict=Cannot install both Fabric, OptiFine, and Forge on Minecraft 1.13 or above.
|
||||
install.failed.optifine_forge_1.17=For Minecraft version 1.17.1 or lower, Forge only supports OptiFine H1 Pre2 or newer. You can install them under the snapshot versions tab.
|
||||
install.failed.version_mismatch=This library requires the game version %s, but the installed one is %s.
|
||||
install.installer.change_version=Version %s is not compatible with the current game version. Click here to replace it with another version or delete it.
|
||||
install.installer.change_version=%s Incompatible
|
||||
install.installer.choose=Choose Your %s Version
|
||||
install.installer.depend=Requires %s
|
||||
install.installer.fabric=Fabric
|
||||
@@ -640,6 +637,7 @@ install.installer.optifine=OptiFine
|
||||
install.installer.quilt=Quilt
|
||||
install.installer.quilt-api=QSL/QFAPI
|
||||
install.installer.version=%s
|
||||
install.installer.external_version=%s Installed by external process, which cannot be configured
|
||||
install.modpack=Install a Modpack
|
||||
install.new_game=Add a New Instance
|
||||
install.new_game.already_exists=This instance already exists. Please use another name.
|
||||
@@ -1226,6 +1224,7 @@ update.no_browser=Cannot open in the system browser. But, we copied the link to
|
||||
update.tooltip=Update
|
||||
|
||||
version=Games
|
||||
version.name=Instance Name
|
||||
version.cannot_read=Unable to parse the game version, automatic installation cannot continue.
|
||||
version.empty=No Instances
|
||||
version.empty.add=Add an Instance
|
||||
|
||||
@@ -138,8 +138,7 @@ account.username=Nombre de usuario
|
||||
|
||||
archive.author=Autor(es)
|
||||
archive.date=Fecha de publicación
|
||||
archive.game_version=Versión del juego
|
||||
archive.name=Nombre de archivo
|
||||
archive.file.name=Nombre de archivo
|
||||
archive.version=Versión
|
||||
|
||||
assets.download=Descargando assets
|
||||
@@ -188,6 +187,7 @@ curse.category.4482=Extra grande
|
||||
curse.category.4472=Tecnología
|
||||
curse.category.4473=Magia
|
||||
curse.category.5128=Vanilla+
|
||||
curse.category.7418=Horror
|
||||
|
||||
# https://addons-ecs.forgesvc.net/api/v2/category/section/6
|
||||
curse.category.5299=Educación
|
||||
@@ -295,7 +295,6 @@ download=Descargar
|
||||
download.hint=Instalar juegos y modpacks o descargar mods, paquetes de recursos y mapas
|
||||
download.code.404=Archivo no encontrado en el servidor remoto: %s
|
||||
download.content=Complementos
|
||||
download.curseforge.customization=Luz y sombras, y personalización del juego
|
||||
download.existing=El archivo no se puede guardar porque ya existe. Puedes usar 'Guardar como' para guardar el archivo en otro lugar.
|
||||
download.external_link=Abrir sitio web
|
||||
download.failed=Falló la descarga de %1$s, código de respuesta: %2$d
|
||||
@@ -348,8 +347,6 @@ Por ejemplo, no podrá utilizar authlib-injector o cambiar el skin de su cuenta
|
||||
feedback=Comentarios
|
||||
feedback.discord=Discord
|
||||
feedback.discord.statement=¡Únete a nuestra comunidad de Discord!
|
||||
feedback.kookapp=KOOK
|
||||
feedback.kookapp.statement=¡Únete a nuestra comunidad de KaiHeiLa!
|
||||
|
||||
file=Archivo
|
||||
|
||||
@@ -362,7 +359,6 @@ folder.saves=Guardados
|
||||
folder.screenshots=Capturas de pantalla
|
||||
|
||||
game=Juego
|
||||
#game.crash.feedback=¿Es un problema común que casi todo el mundo ha tenido? Por favor, deja tus comentarios para que podamos mejorar.
|
||||
game.crash.feedback=<b>¡Por favor, no comparta capturas de pantalla de esta interfaz con otras personas! </b> Si solicita ayuda a otros, haga clic para exportar la información del bloqueo del juego en la esquina inferior izquierda y envíe el archivo exportado a otros para que lo analicen.
|
||||
game.crash.info=Información sobre el fallo
|
||||
game.crash.reason=Causa del fallo
|
||||
@@ -483,13 +479,6 @@ game.crash.reason.fabric_reports_an_error_and_gives_a_solution=Forge puede haber
|
||||
game.crash.reason.java_version_is_too_high=El juego actual se bloqueó porque la versión de Java es demasiado alta y no puede seguir ejecutándose.\nCambie a una versión anterior de Java en la pestaña Java Path de Configuración global del juego o Configuración específica del juego antes de iniciar el juego.\nSi no es así, puede descargarlo desde <a href="https://www.java.com/download/">java.com (Java8)</a> o <a href="https://bell-sw.com/pages/downloads/#downloads">BellSoft Liberica Full JRE (Java17)</a> y otras plataformas para descargar e instalar una (reinicie el iniciador después de la instalación).
|
||||
game.crash.reason.mod_name=El juego actual no puede continuar ejecutándose debido a un problema con el nombre del archivo mod. El nombre del archivo de\nMod solo debe usar mayúsculas y minúsculas (Aa ~ Zz), números (0 ~ 9), líneas horizontales (-), subrayado (_) y puntos (.) en toda la mitad del inglés.\n Por favor, vaya a la carpeta mod para agregar un carácter de Cumplimiento anterior a todos los nombres de archivo mod que no cumplan.
|
||||
game.crash.reason.incomplete_forge_installation=O jogo atual não pode continuar devido a uma instalação incompleta do Forge / NeoForge.\nDesinstale e reinstale o Forge / NeoForge em Configurações de versão - Instalação automática.
|
||||
game.crash.reason.forge_error=Forge puede haber proporcionado un mensaje de error. \nPuede ver el registro y realizar las acciones correspondientes de acuerdo con la información de registro en el informe de errores. \nSi no ve un mensaje de error, puede consultar el informe de errores para saber cómo ocurrió el error.\n%1$s
|
||||
game.crash.reason.mod_solution0=El juego actual no puede seguir ejecutándose debido a algunos problemas de modificación. \nPuede consultar el registro para ver si hay un mod incorrecto.
|
||||
game.crash.reason.mod_profile_causes_game_crash=El juego actual no puede seguir ejecutándose debido a un problema con el perfil de mod. \nPuede consultar el registro del mod defectuoso y su archivo de configuración.
|
||||
game.crash.reason.fabric_reports_an_error_and_gives_a_solution=Forge puede haber dado un mensaje de error. \nPuede ver el registro y realizar las acciones correspondientes de acuerdo con la información de registro en el informe de errores. \nSi no ve un mensaje de error, puede consultar el informe de errores para saber cómo ocurrió el error.
|
||||
game.crash.reason.java_version_is_too_high=El juego actual se bloqueó porque la versión de Java es demasiado alta y no puede seguir ejecutándose. \nCambie a una versión anterior de Java en la pestaña Java Path de Configuración global del juego o Configuración específica del juego antes de iniciar el juego. \nSi no es así, puede descargarlo desde <a href="https://www.java.com/download/">java.com (Java8)</a> o <a href="https://bell-sw.com/pages/downloads/#downloads">BellSoft Liberica Full JRE (Java17)</a> y otras plataformas para descargar e instalar una (reinicie el iniciador después de la instalación).
|
||||
game.crash.reason.mod_name=El juego actual no puede continuar ejecutándose debido a un problema con el nombre del archivo mod. El nombre del archivo de \nMod solo debe usar mayúsculas y minúsculas (Aa ~ Zz), números (0 ~ 9), líneas horizontales (-), subrayado (_) y puntos (.) en toda la mitad del inglés. \n Por favor, vaya a la carpeta mod para agregar un carácter de Cumplimiento anterior a todos los nombres de archivo mod que no cumplan.
|
||||
game.crash.reason.incomplete_forge_installation=O jogo atual não pode continuar devido a uma instalação incompleta do Forge. \nDesinstale e reinstale o Forge em Configurações de versão - Instalação automática.
|
||||
game.crash.reason.modlauncher_8=El juego se ha bloqueado porque tu versión actual de Forge no es compatible con tu instalación de Java. Por favor, intente actualizar Forge, o intente utilizar Java 8u312/11.0.13/17.0.1 o versiones anteriores.\n\
|
||||
\n\
|
||||
8u312 y anteriores\:\n\
|
||||
|
||||
@@ -125,8 +125,7 @@ account.username=ユーザー名
|
||||
|
||||
archive.author=作成者
|
||||
archive.date=公開日
|
||||
archive.game_version=ゲームバージョン
|
||||
archive.name=名前
|
||||
archive.file.name=名前
|
||||
archive.version=バージョン
|
||||
|
||||
Assets.download=アセットのダウンロード
|
||||
@@ -174,6 +173,7 @@ curse.category.4479=ハードコア
|
||||
curse.category.4482=特大
|
||||
curse.category.4472=Tech
|
||||
curse.category.4473=魔法
|
||||
curse.category.7418=ホラー
|
||||
|
||||
# https://addons-ecs.forgesvc.net/api/v2/category/section/6
|
||||
curse.category.5129=Vanilla+
|
||||
@@ -277,7 +277,6 @@ download=ダウンロード
|
||||
download.hint=ゲームや modpack をインストールするか、mod、リソース パック、マップをダウンロードします
|
||||
download.code.404=リモートサーバーにファイルが見つかりません:%s
|
||||
download.content=ゲームコンテンツ
|
||||
download.curseforge.customization=光と影、およびゲームのカスタマイズ
|
||||
download.existing=ファイルは既に存在するため、保存できません。「名前を付けて保存」を選択して、ファイルを別の場所に保存できます。
|
||||
download.external_link=ダウンロードサイトを開く
|
||||
download.failed=%1$s のダウンロードに失敗しました、応答コード:%2$d
|
||||
@@ -311,17 +310,12 @@ fatal.javafx.missing=JavaFXがありません。\nJava11以降を使用してい
|
||||
fatal.config_loading_failure=構成にアクセスできません。\nHelloMinecraftを確認してください。Launcherには、「%s」とその中のファイルへの読み取りおよび書き込みアクセス権があります。
|
||||
fatal.migration_requires_manual_reboot=更新が完了しました。Hello Minecraftを再開してください!ランチャー。
|
||||
fatal.apply_update_failure=ごめんなさい、Hello Minecraft! Launcher 何か問題が発生したため、ランチャーは更新を完了できませんでした。\nただし、Hello Minecraftをダウンロードすることで、手動で更新を終了できます。%s からのランチャー。\nこの問題を報告することを検討してください。
|
||||
fatal.config_loading_failure=構成にアクセスできません。\nHelloMinecraftを確認してください。 Launcherには、「%s」とその中のファイルへの読み取りおよび書き込みアクセス権があります。\nmacOS の場合は、「デスクトップ」、「ダウンロード」、「ドキュメント」以外の権限を持つ場所に HMCL を配置して、再試行してください。
|
||||
fatal.migration_requires_manual_reboot=更新が完了しました。 Hello Minecraftを再開してください!ランチャー。
|
||||
fatal.apply_update_failure=ごめんなさい、Hello Minecraft! Launcher 何か問題が発生したため、ランチャーは更新を完了できませんでした。\nただし、Hello Minecraftをダウンロードすることで、手動で更新を終了できます。 %s からのランチャー。\nこの問題を報告することを検討してください。
|
||||
fatal.samba=If you are trying to run HMCL in a shared folder by Samba, HMCL may not working, please try updating your Java or running HMCL in a local folder.
|
||||
fatal.illegal_char=ユーザーフォルダーのパスに不正な文字'='が含まれています, ログインアカウントやオフラインログインではスキンの変更ができなくなり。
|
||||
|
||||
feedback=フィードバック
|
||||
feedback.discord=Discord
|
||||
feedback.discord.statement=チャットに参加しよう!
|
||||
feedback.kookapp=KOOK
|
||||
feedback.kookapp.statement=チャットに参加しよう!
|
||||
|
||||
file=ファイル
|
||||
|
||||
@@ -334,7 +328,6 @@ folder.saves=マップ
|
||||
folder.screenshots=スクリーンショット
|
||||
|
||||
game=ゲーム
|
||||
#game.crash.feedback=問題は非常に一般的ですが、理由がわかりませんでしたか?フィードバックページでフィードバックをお寄せください。
|
||||
game.crash.feedback=<b>このインターフェースのスクリーンショットを他の人と共有しないでください。 </b> 他の人に助けを求める場合は、左下隅にあるゲーム クラッシュ情報をクリックしてエクスポートし、エクスポートされたファイルを分析のために他の人に送信してください。
|
||||
game.crash.info=ゲームステータス
|
||||
game.crash.reason=クラッシュアナライザー
|
||||
@@ -871,7 +864,7 @@ update.channel.dev.title=ベータ版のヒント
|
||||
update.channel.nightly=アルファ
|
||||
update.channel.nightly.hint=アルファ版を使用しています。これには、ベータ版およびリリース版と比較していくつかの追加機能が含まれている可能性があり、テストにのみ使用されます。\n\
|
||||
アルファ版は不安定です!\n\
|
||||
問題が発生した場合は、<a href="hmcl://settings/feedback">フィードバックページ</a>にアクセスして報告するか、チャット <a href="https://discord.gg/jVvC7HfM6U">Discord</a>または<a href="https://kook.top/Kx7n3t">KOOK</a>で問題を報告します。
|
||||
問題が発生した場合は、<a href="hmcl://settings/feedback">フィードバックページ</a>にアクセスして報告するか、チャット <a href="https://discord.gg/jVvC7HfM6U">Discord</a>で問題を報告します。
|
||||
update.channel.nightly.title=アルファ版のヒント
|
||||
update.channel.stable=リリース
|
||||
update.checking=更新の確認
|
||||
|
||||
@@ -133,8 +133,7 @@ account.username=Имя пользователя
|
||||
|
||||
archive.author=Автор(ы)
|
||||
archive.date=Дата публикации
|
||||
archive.game_version=Версия игры
|
||||
archive.name=Имя файла
|
||||
archive.file.name=Имя файла
|
||||
archive.version=Версия
|
||||
|
||||
assets.download=Скачивание Assets
|
||||
@@ -185,6 +184,7 @@ curse.category.4482=Extra Large
|
||||
curse.category.4472=Tech
|
||||
curse.category.4473=Magic
|
||||
curse.category.5128=Vanilla+
|
||||
curse.category.7418=Horror
|
||||
|
||||
# https://addons-ecs.forgesvc.net/api/v2/category/section/6
|
||||
curse.category.5299=Education
|
||||
@@ -293,7 +293,6 @@ download=Скачать
|
||||
download.hint=Установите игры и пакеты модов или загрузите моды, пакеты ресурсов и карты
|
||||
download.code.404=Файл не найден на удалённом сервере: %s
|
||||
download.content=Игровой контент
|
||||
download.curseforge.customization=Свет и тень, а также настройка игры
|
||||
download.curseforge.unavailable=Лаунчер версии Nightly не поддерживает доступ к CurseForge, используйте Release или Beta для скачивания.
|
||||
download.existing=Файл существует и по этому не может быть сохранён. Можно использовать «Сохранить как», чтобы сохранить файл в другом месте.
|
||||
download.external_link=Открыть сайт
|
||||
@@ -339,8 +338,6 @@ fatal.illegal_char=Недопустимый символ '=' в пути к па
|
||||
feedback=Обратная связь
|
||||
feedback.discord=Discord
|
||||
feedback.discord.statement=Присоединиться!
|
||||
feedback.kookapp=KOOK
|
||||
feedback.kookapp.statement=Присоединиться!
|
||||
|
||||
file=Файл
|
||||
|
||||
@@ -353,7 +350,6 @@ folder.saves=Сохранения
|
||||
folder.screenshots=Снимки экрана
|
||||
|
||||
game=Игра
|
||||
#game.crash.feedback=Проблема настолько распространена, но мы не выяснили причину? Оставь нам отзыв на странице обратной связи!
|
||||
game.crash.feedback=<b>Пожалуйста, не делитесь скриншотами этого интерфейса с другими! </b> Если вы попросите помощи у других, нажмите, чтобы экспортировать информацию о сбое игры в левом нижнем углу, и отправьте экспортированный файл другим для анализа.
|
||||
game.crash.info=Статус игры
|
||||
game.crash.reason=Анализатор сбоев
|
||||
|
||||
@@ -106,6 +106,7 @@ account.methods.microsoft.error.country_unavailable=你所在的國家或地區
|
||||
account.methods.microsoft.error.missing_xbox_account=你的微軟帳號尚未關聯 XBox 帳號,你必須先創建 XBox 帳號,才能登入遊戲。
|
||||
account.methods.microsoft.error.no_character=該帳戶未包含 Minecraft Java 版購買記錄\n可能未創建遊戲檔案,請點擊上方鏈接創建
|
||||
account.methods.microsoft.error.unknown=登入失敗,錯誤碼:%d
|
||||
account.methods.microsoft.error.wrong_verify_method=請在 Microsoft 帳號登陸頁面使用帳號 + 密碼登入。請不要使用驗證碼登入。
|
||||
account.methods.microsoft.logging_in=登入中...
|
||||
account.methods.microsoft.makegameidsettings=創建檔案/編輯檔案名稱
|
||||
account.methods.microsoft.hint=你需要按照以下步驟添加賬戶:\n\
|
||||
@@ -167,8 +168,7 @@ account.username=使用者名稱
|
||||
|
||||
archive.author=作者
|
||||
archive.date=發布日期
|
||||
archive.game_version=遊戲版本
|
||||
archive.name=名稱
|
||||
archive.file.name=檔案名稱
|
||||
archive.version=版本
|
||||
|
||||
assets.download=下載資源
|
||||
@@ -221,6 +221,7 @@ curse.category.4482=大型模組包
|
||||
curse.category.4472=科技
|
||||
curse.category.4473=魔法
|
||||
curse.category.5128=原版增強
|
||||
curse.category.7418=恐怖
|
||||
|
||||
# https://addons-ecs.forgesvc.net/api/v2/category/section/6
|
||||
curse.category.5299=教育
|
||||
@@ -328,7 +329,6 @@ download=下載
|
||||
download.hint=安裝遊戲和整合包或下載模組、資源包和地圖
|
||||
download.code.404=遠端伺服器沒有需要下載的檔案: %s
|
||||
download.content=遊戲內容
|
||||
download.curseforge.customization=光影與遊戲定制
|
||||
download.curseforge.unavailable=HMCL 預覽版暫不支持訪問 CurseForge,請使用穩定版或測試版進行下載。
|
||||
download.existing=檔案已存在,無法保存。你可以選擇另存為將檔案保存至其他地方。
|
||||
download.external_link=打開下載網站
|
||||
@@ -385,8 +385,6 @@ feedback.discord=Discord
|
||||
feedback.discord.statement=歡迎加入 Discord 討論區,加入後請遵守討論區規定。
|
||||
feedback.github=GitHub Issue
|
||||
feedback.github.statement=打開一個 GitHub Issue。
|
||||
feedback.kookapp=KOOK
|
||||
feedback.kookapp.statement=歡迎加入 KOOK 討論區,加入後請遵守討論區規定。
|
||||
feedback.qq_group=HMCL 用戶群
|
||||
feedback.qq_group.statement=歡迎加入 HMCL 用戶群,加入後請遵守群規。
|
||||
|
||||
@@ -402,7 +400,6 @@ folder.saves=遊戲存檔資料夾
|
||||
folder.screenshots=截圖資料夾
|
||||
|
||||
game=遊戲
|
||||
#game.crash.feedback=查看了日誌發現問題很常見?歡迎在 <a href="https://discord.gg/jVvC7HfM6U">Discord</a> 或 <a href="https://kook.top/Kx7n3t">KOOK 群</a>提交回饋,幫助我們豐富崩潰分析系統!
|
||||
game.crash.feedback=<b>請不要將本界面截圖給他人! </b>如果你要求助他人,請你點擊左下角 導出遊戲崩潰信息 後將導出的文件發送給他人以供分析。\n你可以點擊下方的 幫助 前往社區尋求幫助。
|
||||
game.crash.info=遊戲訊息
|
||||
game.crash.reason=崩潰原因
|
||||
@@ -502,7 +499,7 @@ install.failed.malformed=剛才下載的檔案格式損壞。您可以切換到
|
||||
install.failed.optifine_conflict=暫不支援 OptiFine 與 Forge 同時安裝在 Minecraft 1.13 上
|
||||
install.failed.optifine_forge_1.17=Minecraft 1.17.1 下,僅 OptiFine H1 Pre2 及以上版本能相容 Forge。你可以從 OptiFine 測試版中選擇最新版本。
|
||||
install.failed.version_mismatch=該軟體需要的遊戲版本為 %s,但實際的遊戲版本為 %s。
|
||||
install.installer.change_version=%s,該版本與當前遊戲不相容,您需要點擊此處更換版本或刪除
|
||||
install.installer.change_version=%s 與當前遊戲不相容,請更換版本
|
||||
install.installer.choose=選擇 %s 版本
|
||||
install.installer.depend=需要先安裝 %s
|
||||
install.installer.fabric=Fabric
|
||||
@@ -524,6 +521,7 @@ install.installer.optifine=OptiFine
|
||||
install.installer.quilt=Quilt
|
||||
install.installer.quilt-api=QSL/QFAPI
|
||||
install.installer.version=%s
|
||||
install.installer.external_version=%s 由外部安裝的版本,無法解除安裝或更換
|
||||
install.modpack=安裝模組包
|
||||
install.new_game=安裝新遊戲版本
|
||||
install.new_game.already_exists=此版本已經存在,請重新命名
|
||||
@@ -1083,6 +1081,7 @@ update.no_browser=無法開啟瀏覽器,網址已經複製到剪貼簿了,
|
||||
update.tooltip=更新
|
||||
|
||||
version=遊戲
|
||||
version.name=遊戲版本名稱
|
||||
version.cannot_read=讀取遊戲版本失敗,無法進行自動安裝
|
||||
version.empty=沒有遊戲版本
|
||||
version.empty.add=進入下載頁安裝遊戲
|
||||
|
||||
@@ -101,12 +101,13 @@ account.methods.microsoft=微软账户
|
||||
account.methods.microsoft.birth=如何修改账户出生日期
|
||||
account.methods.microsoft.close_page=已完成微软账户授权,接下来启动器还需要完成剩余登录步骤。你已经可以关闭本页面了。
|
||||
account.methods.microsoft.deauthorize=解除账户授权
|
||||
account.methods.microsoft.error.add_family=由于你未满 18 岁,你的账户必须被加入到家庭中才能登录游戏。你也可以点击上方【账户设置页】更改你的账户的出生日期,使年龄满 18 岁以上以继续登录。\n你可以点击右上角帮助按钮进行求助。
|
||||
account.methods.microsoft.error.add_family_probably=请检查你的账户设置,如果年龄未满 18 岁,你的账户必须被加入到家庭中才能登录游戏。你也可以点击上方链接更改你的账户的出生日期,使年龄满 18 岁以上以继续登录。\n你可以点击右上角帮助按钮进行求助。
|
||||
account.methods.microsoft.error.add_family=请点击上方【账户设置页】更改你的账户的出生日期,使年龄满 18 岁以上。或将账户加入到家庭中。\n你可以点击右上角帮助按钮进行求助。
|
||||
account.methods.microsoft.error.add_family_probably=请点击上方【账户设置页】更改你的账户的出生日期,使年龄满 18 岁以上。或将账户加入到家庭中。\n你可以点击右上角帮助按钮进行求助。
|
||||
account.methods.microsoft.error.country_unavailable=你所在的国家或地区不受 XBox Live 的支持。
|
||||
account.methods.microsoft.error.missing_xbox_account=你的微软账户尚未关联 XBox 账户,你必须先创建 XBox 账户,才能登录游戏。\n你可以点击右上角帮助按钮进行求助。
|
||||
account.methods.microsoft.error.no_character=该账户未包含 Minecraft Java 版购买记录\n若已购买,则可能未创建游戏档案,请点击上方链接创建。\n若确定该账户完成了上述步骤,请先在 Minecraft 官网(minecraft.net)登录一次账户,然后再在启动器登录。\n你可以点击右上角帮助按钮进行求助。
|
||||
account.methods.microsoft.error.unknown=登录失败,错误码:%d
|
||||
account.methods.microsoft.error.missing_xbox_account=请点击上方【创建档案】关联 XBox 账户。\n你可以点击右上角帮助按钮进行求助。
|
||||
account.methods.microsoft.error.no_character=请确认你购买了 Minecraft: Java 版。若已购买,该账户未包含 Minecraft Java 版购买记录\n请点击【创建档案】创建游戏档案。\n你可以点击右上角帮助按钮进行求助。
|
||||
account.methods.microsoft.error.unknown=未知问题。错误码:%d。\n你可以点击右上角帮助按钮进行求助。
|
||||
account.methods.microsoft.error.wrong_verify_method=请在 Microsoft 账户登陆页面使用账户 + 密码登录。请不要使用验证码登录。\n你可以点击右上角帮助按钮进行求助。
|
||||
account.methods.microsoft.logging_in=登录中……
|
||||
account.methods.microsoft.makegameidsettings=创建档案/编辑档案名称
|
||||
account.methods.microsoft.hint=你需要按照以下步骤添加账户:\n\
|
||||
@@ -168,8 +169,7 @@ account.username=用户名
|
||||
|
||||
archive.author=作者
|
||||
archive.date=发布日期
|
||||
archive.game_version=游戏版本
|
||||
archive.name=名称
|
||||
archive.file.name=文件名
|
||||
archive.version=版本
|
||||
|
||||
assets.download=下载资源
|
||||
@@ -222,6 +222,7 @@ curse.category.4482=大型整合包
|
||||
curse.category.4472=科技
|
||||
curse.category.4473=魔法
|
||||
curse.category.5128=原版增强
|
||||
curse.category.7418=恐怖
|
||||
|
||||
# https://addons-ecs.forgesvc.net/api/v2/category/section/6
|
||||
curse.category.5299=教育
|
||||
@@ -329,7 +330,6 @@ download=下载
|
||||
download.hint=安装游戏和整合包或下载模组、资源包和地图
|
||||
download.code.404=远程服务器不包含需要下载的文件: %s
|
||||
download.content=游戏内容
|
||||
download.curseforge.customization=光影与游戏定制
|
||||
download.curseforge.unavailable=HMCL 预览版暂不支持访问 CurseForge,请使用稳定版或测试版进行下载。
|
||||
download.existing=文件已存在,无法保存。你可以在模组选择栏中的右侧按钮另存为将文件保存至其他地方。
|
||||
download.external_link=打开下载网站
|
||||
@@ -387,8 +387,6 @@ feedback.discord=Discord
|
||||
feedback.discord.statement=欢迎加入 Discord 讨论区,加入后请遵守讨论区规定。
|
||||
feedback.github=GitHub Issue
|
||||
feedback.github.statement=打开一个 GitHub Issue。
|
||||
feedback.kookapp=KOOK
|
||||
feedback.kookapp.statement=欢迎加入 KOOK 讨论区,加入后请遵守讨论区规定。
|
||||
feedback.qq_group=HMCL 用户群
|
||||
feedback.qq_group.statement=欢迎加入 HMCL 用户群,加入后请遵守群规。
|
||||
|
||||
@@ -404,7 +402,6 @@ folder.saves=存档文件夹
|
||||
folder.screenshots=截图文件夹
|
||||
|
||||
game=游戏
|
||||
#game.crash.feedback=查看了日志发现该问题很常见?欢迎在下方的 “帮助” 中提交反馈,帮助我们丰富崩溃分析系统!
|
||||
game.crash.feedback=<b>请不要将本界面截图给他人!</b>如果你要求助他人,请你点击左下角<b> 导出游戏崩溃信息 </b>后将导出的文件发送给他人以供分析。\n你可以点击下方的<b> 帮助 </b>前往交流群寻求帮助。
|
||||
game.crash.info=游戏信息
|
||||
game.crash.reason=崩溃原因
|
||||
@@ -501,7 +498,7 @@ install.failed.malformed=下载的文件格式损坏。您可以在设置-下载
|
||||
install.failed.optifine_conflict=暂不支持 OptiFine, Fabric 或 OptiFine , Forge 同时安装在 Minecraft 1.13 及以上版本
|
||||
install.failed.optifine_forge_1.17=Minecraft 1.17.1 下,仅 OptiFine H1 Pre2 及以上版本能兼容 Forge。你可以从 OptiFine 测试版中选择最新版本。
|
||||
install.failed.version_mismatch=该组件需要的游戏版本为 %s,但实际的游戏版本为 %s。
|
||||
install.installer.change_version=%s,该版本与当前游戏不兼容,您需要点击此处更换版本或删除
|
||||
install.installer.change_version=%s 与当前游戏不兼容,请更换版本
|
||||
install.installer.choose=选择 %s 版本
|
||||
install.installer.depend=需要先安装 %s
|
||||
install.installer.fabric=Fabric
|
||||
@@ -523,6 +520,7 @@ install.installer.optifine=OptiFine
|
||||
install.installer.quilt=Quilt
|
||||
install.installer.quilt-api=QSL/QFAPI
|
||||
install.installer.version=%s
|
||||
install.installer.external_version=%s 由外部安装的版本,无法卸载或更换
|
||||
install.modpack=安装整合包
|
||||
install.new_game=安装新游戏版本
|
||||
install.new_game.already_exists=此版本已经存在,请换一个名字
|
||||
@@ -1082,6 +1080,7 @@ update.no_browser=无法打开浏览器,网址已经复制到剪贴板了,
|
||||
update.tooltip=更新
|
||||
|
||||
version=游戏
|
||||
version.name=游戏版本名称
|
||||
version.cannot_read=读取游戏版本失败,无法进行自动安装
|
||||
version.empty=没有游戏版本
|
||||
version.empty.add=进入下载页安装游戏
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -21,8 +21,10 @@ import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.auth.*;
|
||||
import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.OAuth;
|
||||
import org.jackhuang.hmcl.auth.ServerDisconnectException;
|
||||
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.CompleteGameProfile;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.Texture;
|
||||
@@ -44,8 +46,8 @@ import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Lang.threadPool;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public class MicrosoftService {
|
||||
private static final String SCOPE = "XboxLive.signin offline_access";
|
||||
@@ -110,30 +112,40 @@ public class MicrosoftService {
|
||||
}
|
||||
|
||||
private MicrosoftSession authenticateViaLiveAccessToken(String liveAccessToken, String liveRefreshToken) throws IOException, JsonParseException, AuthenticationException {
|
||||
// Authenticate with XBox Live
|
||||
XBoxLiveAuthenticationResponse xboxResponse = HttpRequest
|
||||
.POST("https://user.auth.xboxlive.com/user/authenticate")
|
||||
.json(mapOf(
|
||||
pair("Properties",
|
||||
mapOf(pair("AuthMethod", "RPS"), pair("SiteName", "user.auth.xboxlive.com"),
|
||||
pair("RpsTicket", "d=" + liveAccessToken))),
|
||||
pair("RelyingParty", "http://auth.xboxlive.com"), pair("TokenType", "JWT")))
|
||||
.retry(5)
|
||||
.accept("application/json").getJson(XBoxLiveAuthenticationResponse.class);
|
||||
String uhs;
|
||||
XBoxLiveAuthenticationResponse xboxResponse, minecraftXstsResponse;
|
||||
try {
|
||||
// Authenticate with XBox Live
|
||||
xboxResponse = HttpRequest
|
||||
.POST("https://user.auth.xboxlive.com/user/authenticate")
|
||||
.json(mapOf(
|
||||
pair("Properties",
|
||||
mapOf(pair("AuthMethod", "RPS"), pair("SiteName", "user.auth.xboxlive.com"),
|
||||
pair("RpsTicket", "d=" + liveAccessToken))),
|
||||
pair("RelyingParty", "http://auth.xboxlive.com"), pair("TokenType", "JWT")))
|
||||
.retry(5)
|
||||
.accept("application/json")
|
||||
.getJson(XBoxLiveAuthenticationResponse.class);
|
||||
|
||||
String uhs = getUhs(xboxResponse, null);
|
||||
uhs = getUhs(xboxResponse, null);
|
||||
|
||||
// Authenticate Minecraft with XSTS
|
||||
XBoxLiveAuthenticationResponse minecraftXstsResponse = HttpRequest
|
||||
.POST("https://xsts.auth.xboxlive.com/xsts/authorize")
|
||||
.json(mapOf(
|
||||
pair("Properties",
|
||||
mapOf(pair("SandboxId", "RETAIL"),
|
||||
pair("UserTokens", Collections.singletonList(xboxResponse.token)))),
|
||||
pair("RelyingParty", "rp://api.minecraftservices.com/"), pair("TokenType", "JWT")))
|
||||
.ignoreHttpErrorCode(401)
|
||||
.retry(5)
|
||||
.getJson(XBoxLiveAuthenticationResponse.class);
|
||||
minecraftXstsResponse = HttpRequest
|
||||
.POST("https://xsts.auth.xboxlive.com/xsts/authorize")
|
||||
.json(mapOf(
|
||||
pair("Properties",
|
||||
mapOf(pair("SandboxId", "RETAIL"),
|
||||
pair("UserTokens", Collections.singletonList(xboxResponse.token)))),
|
||||
pair("RelyingParty", "rp://api.minecraftservices.com/"), pair("TokenType", "JWT")))
|
||||
.ignoreHttpErrorCode(401)
|
||||
.retry(5)
|
||||
.getJson(XBoxLiveAuthenticationResponse.class);
|
||||
} catch (ResponseCodeException e) {
|
||||
if (e.getResponseCode() == 400) {
|
||||
throw new XBox400Exception();
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
getUhs(minecraftXstsResponse, uhs);
|
||||
|
||||
@@ -290,6 +302,9 @@ public class MicrosoftService {
|
||||
public static final long ADD_FAMILY = 2148916238L;
|
||||
}
|
||||
|
||||
public static class XBox400Exception extends AuthenticationException {
|
||||
}
|
||||
|
||||
public static class NoMinecraftJavaEditionProfileException extends AuthenticationException {
|
||||
}
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
package org.jackhuang.hmcl.auth.microsoft;
|
||||
|
||||
import org.jackhuang.hmcl.auth.AuthInfo;
|
||||
import org.jackhuang.hmcl.util.logging.Logger;
|
||||
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
||||
import org.jackhuang.hmcl.util.logging.Logger;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
@@ -87,7 +87,7 @@ public class MicrosoftSession {
|
||||
.orElseThrow(() -> new IllegalArgumentException("accessToken is missing"));
|
||||
String refreshToken = tryCast(storage.get("refreshToken"), String.class)
|
||||
.orElseThrow(() -> new IllegalArgumentException("refreshToken is missing"));
|
||||
Long notAfter = tryCast(storage.get("notAfter"), Long.class).orElse(0L);
|
||||
Long notAfter = tryCast(storage.get("notAfter"), Number.class).map(Number::longValue).orElse(0L);
|
||||
String userId = tryCast(storage.get("userid"), String.class)
|
||||
.orElseThrow(() -> new IllegalArgumentException("userid is missing"));
|
||||
return new MicrosoftSession(tokenType, accessToken, notAfter, refreshToken, new User(userId), new GameProfile(uuid, name));
|
||||
|
||||
@@ -19,7 +19,10 @@ package org.jackhuang.hmcl.download;
|
||||
|
||||
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;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@@ -51,6 +54,15 @@ public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
|
||||
return Optional.ofNullable(libraries.get(type.getPatchId())).map(Pair::getKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* If a library is provided in $.patches, it's structure is so clear that we can do any operation.
|
||||
* Otherwise, we must guess how are these libraries mixed.
|
||||
* Maybe a guessing implementation will be provided in the future. But by now, we simply set it to JUST_EXISTED.
|
||||
*/
|
||||
public LibraryMark.LibraryStatus getLibraryStatus(String type) {
|
||||
return version.hasPatch(type) ? LibraryMark.LibraryStatus.CLEAR : LibraryMark.LibraryStatus.JUST_EXISTED;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Iterator<LibraryMark> iterator() {
|
||||
@@ -65,7 +77,7 @@ public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
|
||||
@Override
|
||||
public LibraryMark next() {
|
||||
Map.Entry<String, Pair<Library, String>> entry = impl.next();
|
||||
return new LibraryMark(entry.getKey(), entry.getValue().getValue());
|
||||
return new LibraryMark(entry.getKey(), entry.getValue().getValue(), getLibraryStatus(entry.getKey()));
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -85,13 +97,9 @@ public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
|
||||
}
|
||||
|
||||
public boolean hasModLauncher() {
|
||||
final String modLauncher = "cpw.mods.modlauncher.Launcher";
|
||||
return modLauncher.equals(version.getMainClass()) || version.getPatches().stream().anyMatch(patch -> modLauncher.equals(patch.getMainClass()));
|
||||
}
|
||||
|
||||
public boolean hasBootstrapLauncher() {
|
||||
final String bootstrapLauncher = "cpw.mods.bootstraplauncher.BootstrapLauncher";
|
||||
return bootstrapLauncher.equals(version.getMainClass()) || version.getPatches().stream().anyMatch(patch -> bootstrapLauncher.equals(patch.getMainClass()));
|
||||
return LibraryAnalyzer.MOD_LAUNCHER_MAIN.equals(version.getMainClass()) || version.getPatches().stream().anyMatch(
|
||||
patch -> LibraryAnalyzer.MOD_LAUNCHER_MAIN.equals(patch.getMainClass())
|
||||
);
|
||||
}
|
||||
|
||||
private Version removingMatchedLibrary(Version version, String libraryId) {
|
||||
@@ -184,7 +192,7 @@ public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
|
||||
private final Pattern FORGE_VERSION_MATCHER = Pattern.compile("^([0-9.]+)-(?<forge>[0-9.]+)(-([0-9.]+))?$");
|
||||
|
||||
@Override
|
||||
public String patchVersion(Version gameVersion, String libraryVersion) {
|
||||
protected String patchVersion(Version gameVersion, String libraryVersion) {
|
||||
Matcher matcher = FORGE_VERSION_MATCHER.matcher(libraryVersion);
|
||||
if (matcher.find()) {
|
||||
return matcher.group("forge");
|
||||
@@ -193,7 +201,7 @@ public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matchLibrary(Library library, List<Library> libraries) {
|
||||
protected boolean matchLibrary(Library library, List<Library> libraries) {
|
||||
for (Library l : libraries) {
|
||||
if (NEO_FORGE.matchLibrary(l, libraries)) {
|
||||
return false;
|
||||
@@ -206,7 +214,7 @@ public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
|
||||
private final Pattern NEO_FORGE_VERSION_MATCHER = Pattern.compile("^([0-9.]+)-(?<forge>[0-9.]+)(-([0-9.]+))?$");
|
||||
|
||||
@Override
|
||||
public String patchVersion(Version gameVersion, String libraryVersion) {
|
||||
protected String patchVersion(Version gameVersion, String libraryVersion) {
|
||||
Matcher matcher = NEO_FORGE_VERSION_MATCHER.matcher(libraryVersion);
|
||||
if (matcher.find()) {
|
||||
return matcher.group("forge");
|
||||
@@ -289,22 +297,37 @@ public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean matchLibrary(Library library, List<Library> libraries) {
|
||||
protected boolean matchLibrary(Library library, List<Library> libraries) {
|
||||
return group.matcher(library.getGroupId()).matches() && artifact.matcher(library.getArtifactId()).matches();
|
||||
}
|
||||
|
||||
public String patchVersion(Version gameVersion, String libraryVersion) {
|
||||
protected String patchVersion(Version gameVersion, String libraryVersion) {
|
||||
return libraryVersion;
|
||||
}
|
||||
}
|
||||
|
||||
public static class LibraryMark {
|
||||
public final static class LibraryMark {
|
||||
/**
|
||||
* If a library is provided in $.patches, it's structure is so clear that we can do any operation.
|
||||
* Otherwise, we must guess how are these libraries mixed.
|
||||
* Maybe a guessing implementation will be provided in the future. But by now, we simply set it to JUST_EXISTED.
|
||||
*/
|
||||
public enum LibraryStatus {
|
||||
CLEAR, UNSURE, JUST_EXISTED
|
||||
}
|
||||
|
||||
private final String libraryId;
|
||||
private final String libraryVersion;
|
||||
/**
|
||||
* If this version is installed by HMCL, instead of external process,
|
||||
* which means $.patches contains this library, structureClear is true.
|
||||
*/
|
||||
private final LibraryStatus status;
|
||||
|
||||
public LibraryMark(@NotNull String libraryId, @Nullable String libraryVersion) {
|
||||
private LibraryMark(@NotNull String libraryId, @Nullable String libraryVersion, LibraryStatus status) {
|
||||
this.libraryId = libraryId;
|
||||
this.libraryVersion = libraryVersion;
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@@ -316,12 +339,27 @@ public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
|
||||
public String getLibraryVersion() {
|
||||
return libraryVersion;
|
||||
}
|
||||
|
||||
public LibraryStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
public static final String VANILLA_MAIN = "net.minecraft.client.main.Main";
|
||||
public static final String LAUNCH_WRAPPER_MAIN = "net.minecraft.launchwrapper.Launch";
|
||||
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 Set<String> FORGE_OPTIFINE_MAIN = new HashSet<>(Lang.immutableListOf(
|
||||
LibraryAnalyzer.VANILLA_MAIN,
|
||||
LibraryAnalyzer.LAUNCH_WRAPPER_MAIN,
|
||||
LibraryAnalyzer.MOD_LAUNCHER_MAIN,
|
||||
LibraryAnalyzer.BOOTSTRAP_LAUNCHER_MAIN,
|
||||
LibraryAnalyzer.FORGE_BOOTSTRAP_MAIN
|
||||
));
|
||||
|
||||
public static final VersionRange<VersionNumber> FORGE_OPTIFINE_BROKEN_RANGE = VersionNumber.between("48.0.0", "49.0.50");
|
||||
|
||||
public static final String[] FORGE_TWEAKERS = new String[]{
|
||||
"net.minecraftforge.legacy._1_5_2.LibraryFixerTweaker", // 1.5.2
|
||||
|
||||
@@ -76,7 +76,9 @@ public class MaintainTask extends Task<Version> {
|
||||
}
|
||||
|
||||
List<Library> libraries = version.getLibraries();
|
||||
if (libraries.size() > 0) {
|
||||
if (!libraries.isEmpty()) {
|
||||
// HMCL once use log4j-patch to prevent virus. But now, we only modify log4j2.xml.
|
||||
// Therefore, we remove this library.
|
||||
Library library = libraries.get(0);
|
||||
if ("org.glavo".equals(library.getGroupId())
|
||||
&& ("log4j-patch".equals(library.getArtifactId()) || "log4j-patch-beta9".equals(library.getArtifactId()))
|
||||
@@ -161,8 +163,8 @@ public class MaintainTask extends Task<Version> {
|
||||
Path libraryPath = repository.getLibraryFile(version, hmclTransformerDiscoveryService).toPath();
|
||||
try (InputStream input = MaintainTask.class.getResourceAsStream("/assets/game/HMCLTransformerDiscoveryService-1.0.jar")) {
|
||||
Files.createDirectories(libraryPath.getParent());
|
||||
Files.copy(input, libraryPath, StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (IOException e) {
|
||||
Files.copy(Objects.requireNonNull(input, "Bundled HMCLTransformerDiscoveryService is missing."), libraryPath, StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (IOException | NullPointerException e) {
|
||||
LOG.warning("Unable to unpack HMCLTransformerDiscoveryService", e);
|
||||
}
|
||||
});
|
||||
@@ -282,13 +284,6 @@ public class MaintainTask extends Task<Version> {
|
||||
return version.setLibraries(libraries.stream().filter(Objects::nonNull).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
public static boolean isPurePatched(Version version) {
|
||||
if (!version.isResolvedPreservingPatches())
|
||||
throw new IllegalArgumentException("isPurePatched requires a version resolved preserving patches");
|
||||
|
||||
return version.hasPatch("game");
|
||||
}
|
||||
|
||||
public static Version unique(Version version) {
|
||||
List<Library> libraries = new ArrayList<>();
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ package org.jackhuang.hmcl.download;
|
||||
|
||||
import org.jackhuang.hmcl.download.fabric.FabricAPIVersionList;
|
||||
import org.jackhuang.hmcl.download.fabric.FabricVersionList;
|
||||
import org.jackhuang.hmcl.download.forge.ForgeBMCLVersionList;
|
||||
import org.jackhuang.hmcl.download.forge.ForgeVersionList;
|
||||
import org.jackhuang.hmcl.download.game.GameVersionList;
|
||||
import org.jackhuang.hmcl.download.liteloader.LiteLoaderVersionList;
|
||||
import org.jackhuang.hmcl.download.neoforge.NeoForgeOfficialVersionList;
|
||||
@@ -35,7 +35,7 @@ public class MojangDownloadProvider implements DownloadProvider {
|
||||
private final GameVersionList game;
|
||||
private final FabricVersionList fabric;
|
||||
private final FabricAPIVersionList fabricApi;
|
||||
private final ForgeBMCLVersionList forge;
|
||||
private final ForgeVersionList forge;
|
||||
private final NeoForgeOfficialVersionList neoforge;
|
||||
private final LiteLoaderVersionList liteLoader;
|
||||
private final OptiFineBMCLVersionList optifine;
|
||||
@@ -49,7 +49,7 @@ public class MojangDownloadProvider implements DownloadProvider {
|
||||
this.game = new GameVersionList(this);
|
||||
this.fabric = new FabricVersionList(this);
|
||||
this.fabricApi = new FabricAPIVersionList(this);
|
||||
this.forge = new ForgeBMCLVersionList(apiRoot);
|
||||
this.forge = new ForgeVersionList(this);
|
||||
this.neoforge = new NeoForgeOfficialVersionList(this);
|
||||
this.liteLoader = new LiteLoaderVersionList(this);
|
||||
this.optifine = new OptiFineBMCLVersionList(apiRoot);
|
||||
|
||||
@@ -21,6 +21,7 @@ import com.google.gson.JsonParseException;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import org.jackhuang.hmcl.download.VersionList;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
import org.jackhuang.hmcl.util.io.HttpRequest;
|
||||
@@ -30,13 +31,16 @@ import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Lang.wrap;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class ForgeBMCLVersionList extends VersionList<ForgeRemoteVersion> {
|
||||
private final String apiRoot;
|
||||
@@ -63,10 +67,27 @@ public final class ForgeBMCLVersionList extends VersionList<ForgeRemoteVersion>
|
||||
throw new UnsupportedOperationException("ForgeBMCLVersionList does not support loading the entire Forge remote version list.");
|
||||
}
|
||||
|
||||
private static String toLookupVersion(String gameVersion) {
|
||||
return "1.7.10-pre4".equals(gameVersion) ? "1.7.10_pre4" : gameVersion;
|
||||
}
|
||||
|
||||
private static String fromLookupVersion(String lookupVersion) {
|
||||
return "1.7.10_pre4".equals(lookupVersion) ? "1.7.10-pre4" : lookupVersion;
|
||||
}
|
||||
|
||||
private static String toLookupBranch(String gameVersion, String branch) {
|
||||
if ("1.7.10-pre4".equals(gameVersion)) {
|
||||
return "prerelease";
|
||||
}
|
||||
return Lang.requireNonNullElse(branch, "");
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<?> refreshAsync(String gameVersion) {
|
||||
String lookupVersion = toLookupVersion(gameVersion);
|
||||
|
||||
return CompletableFuture.completedFuture(null)
|
||||
.thenApplyAsync(wrap(unused -> HttpRequest.GET(apiRoot + "/forge/minecraft/" + gameVersion).<List<ForgeVersion>>getJson(new TypeToken<List<ForgeVersion>>() {
|
||||
.thenApplyAsync(wrap(unused -> HttpRequest.GET(apiRoot + "/forge/minecraft/" + lookupVersion).<List<ForgeVersion>>getJson(new TypeToken<List<ForgeVersion>>() {
|
||||
}.getType())))
|
||||
.thenAcceptAsync(forgeVersions -> {
|
||||
lock.writeLock().lock();
|
||||
@@ -80,16 +101,17 @@ public final class ForgeBMCLVersionList extends VersionList<ForgeRemoteVersion>
|
||||
List<String> urls = new ArrayList<>();
|
||||
for (ForgeVersion.File file : version.getFiles())
|
||||
if ("installer".equals(file.getCategory()) && "jar".equals(file.getFormat())) {
|
||||
String classifier = gameVersion + "-" + version.getVersion()
|
||||
+ (StringUtils.isNotBlank(version.getBranch()) ? "-" + version.getBranch() : "");
|
||||
String branch = toLookupBranch(gameVersion, version.getBranch());
|
||||
|
||||
String classifier = lookupVersion + "-" + version.getVersion() + (branch.isEmpty() ? "" : '-' + branch);
|
||||
String fileName1 = "forge-" + classifier + "-" + file.getCategory() + "." + file.getFormat();
|
||||
String fileName2 = "forge-" + classifier + "-" + gameVersion + "-" + file.getCategory() + "." + file.getFormat();
|
||||
String fileName2 = "forge-" + classifier + "-" + lookupVersion + "-" + file.getCategory() + "." + file.getFormat();
|
||||
urls.add("https://files.minecraftforge.net/maven/net/minecraftforge/forge/" + classifier + "/" + fileName1);
|
||||
urls.add("https://files.minecraftforge.net/maven/net/minecraftforge/forge/" + classifier + "-" + gameVersion + "/" + fileName2);
|
||||
urls.add("https://files.minecraftforge.net/maven/net/minecraftforge/forge/" + classifier + "-" + lookupVersion + "/" + fileName2);
|
||||
urls.add(NetworkUtils.withQuery("https://bmclapi2.bangbang93.com/forge/download", mapOf(
|
||||
pair("mcversion", version.getGameVersion()),
|
||||
pair("version", version.getVersion()),
|
||||
pair("branch", version.getBranch()),
|
||||
pair("branch", branch),
|
||||
pair("category", file.getCategory()),
|
||||
pair("format", file.getFormat())
|
||||
)));
|
||||
@@ -108,7 +130,7 @@ public final class ForgeBMCLVersionList extends VersionList<ForgeRemoteVersion>
|
||||
}
|
||||
|
||||
versions.put(gameVersion, new ForgeRemoteVersion(
|
||||
version.getGameVersion(), version.getVersion(), releaseDate, urls));
|
||||
fromLookupVersion(version.getGameVersion()), version.getVersion(), releaseDate, urls));
|
||||
}
|
||||
} finally {
|
||||
lock.writeLock().unlock();
|
||||
|
||||
@@ -102,18 +102,10 @@ public final class ForgeInstallTask extends Task<Version> {
|
||||
String originalMainClass = version.resolve(dependencyManager.getGameRepository()).getMainClass();
|
||||
if (GameVersionNumber.compare("1.13", remote.getGameVersion()) <= 0) {
|
||||
// Forge 1.13 is not compatible with fabric.
|
||||
if (!LibraryAnalyzer.VANILLA_MAIN.equals(originalMainClass)
|
||||
&& !LibraryAnalyzer.MOD_LAUNCHER_MAIN.equals(originalMainClass)
|
||||
&& !LibraryAnalyzer.LAUNCH_WRAPPER_MAIN.equals(originalMainClass)
|
||||
&& !LibraryAnalyzer.BOOTSTRAP_LAUNCHER_MAIN.equals(originalMainClass))
|
||||
if (!LibraryAnalyzer.FORGE_OPTIFINE_MAIN.contains(originalMainClass))
|
||||
throw new UnsupportedInstallationException(UNSUPPORTED_LAUNCH_WRAPPER);
|
||||
} else {
|
||||
// Forge 1.12 and older versions is compatible with vanilla and launchwrapper.
|
||||
// if (!"net.minecraft.client.main.Main".equals(originalMainClass) && !"net.minecraft.launchwrapper.Launch".equals(originalMainClass))
|
||||
// throw new OptiFineInstallTask.UnsupportedOptiFineInstallationException();
|
||||
}
|
||||
|
||||
|
||||
if (detectForgeInstallerType(dependencyManager, version, installer))
|
||||
dependency = new ForgeNewInstallTask(dependencyManager, version, remote.getSelfVersion(), installer);
|
||||
else
|
||||
|
||||
@@ -33,7 +33,7 @@ public final class ForgeVersion implements Validation {
|
||||
private final String jobver;
|
||||
private final String version;
|
||||
private final int build;
|
||||
private final double modified;
|
||||
private final long modified;
|
||||
private final String[][] files;
|
||||
|
||||
/**
|
||||
@@ -44,7 +44,7 @@ public final class ForgeVersion implements Validation {
|
||||
this(null, null, null, null, 0, 0, null);
|
||||
}
|
||||
|
||||
public ForgeVersion(String branch, String mcversion, String jobver, String version, int build, double modified, String[][] files) {
|
||||
public ForgeVersion(String branch, String mcversion, String jobver, String version, int build, long modified, String[][] files) {
|
||||
this.branch = branch;
|
||||
this.mcversion = mcversion;
|
||||
this.jobver = jobver;
|
||||
@@ -74,7 +74,7 @@ public final class ForgeVersion implements Validation {
|
||||
return build;
|
||||
}
|
||||
|
||||
public double getModified() {
|
||||
public long getModified() {
|
||||
return modified;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.HttpRequest;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@@ -43,9 +44,17 @@ public final class ForgeVersionList extends VersionList<ForgeRemoteVersion> {
|
||||
return false;
|
||||
}
|
||||
|
||||
private static String toLookupVersion(String gameVersion) {
|
||||
return "1.7.10-pre4".equals(gameVersion) ? "1.7.10_pre4" : gameVersion;
|
||||
}
|
||||
|
||||
private static String fromLookupVersion(String lookupVersion) {
|
||||
return "1.7.10_pre4".equals(lookupVersion) ? "1.7.10-pre4" : lookupVersion;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<?> refreshAsync() {
|
||||
return HttpRequest.GET(downloadProvider.injectURL(FORGE_LIST)).getJsonAsync(ForgeVersionRoot.class)
|
||||
return HttpRequest.GET(FORGE_LIST).getJsonAsync(ForgeVersionRoot.class)
|
||||
.thenAcceptAsync(root -> {
|
||||
lock.writeLock().lock();
|
||||
|
||||
@@ -55,7 +64,7 @@ public final class ForgeVersionList extends VersionList<ForgeRemoteVersion> {
|
||||
versions.clear();
|
||||
|
||||
for (Map.Entry<String, int[]> entry : root.getGameVersions().entrySet()) {
|
||||
String gameVersion = VersionNumber.normalize(entry.getKey());
|
||||
String gameVersion = fromLookupVersion(VersionNumber.normalize(entry.getKey()));
|
||||
for (int v : entry.getValue()) {
|
||||
ForgeVersion version = root.getNumber().get(v);
|
||||
if (version == null)
|
||||
@@ -71,8 +80,12 @@ public final class ForgeVersionList extends VersionList<ForgeRemoteVersion> {
|
||||
|
||||
if (jar == null)
|
||||
continue;
|
||||
|
||||
versions.put(gameVersion, new ForgeRemoteVersion(
|
||||
version.getGameVersion(), version.getVersion(), null, Collections.singletonList(jar)
|
||||
toLookupVersion(version.getGameVersion()),
|
||||
version.getVersion(),
|
||||
version.getModified() > 0 ? Instant.ofEpochSecond(version.getModified()) : null,
|
||||
Collections.singletonList(jar)
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -82,5 +95,5 @@ public final class ForgeVersionList extends VersionList<ForgeRemoteVersion> {
|
||||
});
|
||||
}
|
||||
|
||||
public static final String FORGE_LIST = "https://files.minecraftforge.net/maven/net/minecraftforge/forge/json";
|
||||
public static final String FORGE_LIST = "https://hmcl-dev.github.io/metadata/forge/";
|
||||
}
|
||||
@@ -18,15 +18,21 @@
|
||||
package org.jackhuang.hmcl.download.game;
|
||||
|
||||
import org.jackhuang.hmcl.download.AbstractDependencyManager;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.game.GameRepository;
|
||||
import org.jackhuang.hmcl.game.Library;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -81,20 +87,23 @@ public final class GameLibrariesTask extends Task<Void> {
|
||||
File file = gameRepository.getLibraryFile(version, library);
|
||||
Path jar = file.toPath();
|
||||
if (!file.isFile()) return true;
|
||||
|
||||
if (!integrityCheck) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
if (integrityCheck && !library.getDownload().validateChecksum(jar, true)) return true;
|
||||
if (integrityCheck &&
|
||||
library.getChecksums() != null && !library.getChecksums().isEmpty() &&
|
||||
!LibraryDownloadTask.checksumValid(file, library.getChecksums())) return true;
|
||||
if (integrityCheck) {
|
||||
String ext = FileUtils.getExtension(file);
|
||||
if (ext.equals("jar")) {
|
||||
try {
|
||||
FileDownloadTask.ZIP_INTEGRITY_CHECK_HANDLER.checkIntegrity(jar, jar);
|
||||
} catch (IOException ignored) {
|
||||
// the Jar file is malformed, so re-download it.
|
||||
return true;
|
||||
}
|
||||
if (!library.getDownload().validateChecksum(jar, true)) {
|
||||
return true;
|
||||
}
|
||||
if (library.getChecksums() != null && !library.getChecksums().isEmpty() && !LibraryDownloadTask.checksumValid(file, library.getChecksums())) {
|
||||
return true;
|
||||
}
|
||||
if (FileUtils.getExtension(file).equals("jar")) {
|
||||
try {
|
||||
FileDownloadTask.ZIP_INTEGRITY_CHECK_HANDLER.checkIntegrity(jar, jar);
|
||||
} catch (IOException ignored) {
|
||||
// the Jar file is malformed, so re-download it.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
@@ -105,16 +114,32 @@ public final class GameLibrariesTask extends Task<Void> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
libraries.stream().filter(Library::appliesToCurrentEnvironment).forEach(library -> {
|
||||
File file = dependencyManager.getGameRepository().getLibraryFile(version, library);
|
||||
if (shouldDownloadLibrary(dependencyManager.getGameRepository(), version, library, integrityCheck)) {
|
||||
if (library.hasDownloadURL() || !"optifine".equals(library.getGroupId()))
|
||||
dependencies.add(new LibraryDownloadTask(dependencyManager, file, library));
|
||||
public void execute() throws IOException {
|
||||
GameRepository gameRepository = dependencyManager.getGameRepository();
|
||||
for (Library library : libraries) {
|
||||
if (!library.appliesToCurrentEnvironment()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
File file = gameRepository.getLibraryFile(version, library);
|
||||
if ("optifine".equals(library.getGroupId()) && file.exists() && GameVersionNumber.asGameVersion(gameRepository.getGameVersion(version)).compareTo("1.20.4") == 0) {
|
||||
String forgeVersion = LibraryAnalyzer.analyze(version, "1.20.4")
|
||||
.getVersion(LibraryAnalyzer.LibraryType.FORGE)
|
||||
.orElse(null);
|
||||
if (forgeVersion != null && LibraryAnalyzer.FORGE_OPTIFINE_BROKEN_RANGE.contains(VersionNumber.asVersion(forgeVersion))) {
|
||||
try (FileSystem fs2 = CompressingUtils.createWritableZipFileSystem(file.toPath())) {
|
||||
Files.deleteIfExists(fs2.getPath("/META-INF/mods.toml"));
|
||||
} catch (IOException e) {
|
||||
throw new IOException("Cannot fix optifine", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (shouldDownloadLibrary(gameRepository, version, library, integrityCheck) && (library.hasDownloadURL() || !"optifine".equals(library.getGroupId()))) {
|
||||
dependencies.add(new LibraryDownloadTask(dependencyManager, file, library));
|
||||
} else {
|
||||
dependencyManager.getCacheRepository().tryCacheLibrary(library, file.toPath());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -57,7 +57,7 @@ public final class JavaRepository {
|
||||
"Minecraft Launcher\\runtime"));
|
||||
case LINUX:
|
||||
case FREEBSD:
|
||||
return Stream.of(FileUtils.tryGetPath(System.getProperty("user.home", ".minecraft/runtime")));
|
||||
return Stream.of(FileUtils.tryGetPath(System.getProperty("user.home"), ".minecraft/runtime"));
|
||||
case OSX:
|
||||
return Stream.of(FileUtils.tryGetPath(System.getProperty("user.home"), "Library/Application Support/minecraft/runtime"));
|
||||
default:
|
||||
|
||||
+3
-12
@@ -22,10 +22,8 @@ import com.google.gson.annotations.SerializedName;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import org.jackhuang.hmcl.download.VersionList;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
import org.jackhuang.hmcl.util.io.HttpRequest;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@@ -62,8 +60,7 @@ public final class NeoForgeBMCLVersionList extends VersionList<NeoForgeRemoteVer
|
||||
@Override
|
||||
public Optional<NeoForgeRemoteVersion> getVersion(String gameVersion, String remoteVersion) {
|
||||
if (gameVersion.equals("1.20.1")) {
|
||||
remoteVersion = NeoForgeRemoteVersion.fixInvalidVersion(remoteVersion);
|
||||
remoteVersion = VersionNumber.compare(remoteVersion, "47.1.85") >= 0 ? "1.20.1-" + remoteVersion : remoteVersion;
|
||||
remoteVersion = NeoForgeRemoteVersion.normalize(remoteVersion);
|
||||
}
|
||||
return super.getVersion(gameVersion, remoteVersion);
|
||||
}
|
||||
@@ -79,16 +76,10 @@ public final class NeoForgeBMCLVersionList extends VersionList<NeoForgeRemoteVer
|
||||
try {
|
||||
versions.clear(gameVersion);
|
||||
for (NeoForgeVersion neoForgeVersion : neoForgeVersions) {
|
||||
String nf = StringUtils.removePrefix(
|
||||
neoForgeVersion.version,
|
||||
"1.20.1".equals(gameVersion) ? "1.20.1-forge-" : "neoforge-" // Som of the version numbers for 1.20.1 are like forge.
|
||||
);
|
||||
versions.put(gameVersion, new NeoForgeRemoteVersion(
|
||||
neoForgeVersion.mcVersion,
|
||||
nf,
|
||||
Collections.singletonList(
|
||||
apiRoot + "/neoforge/version/" + neoForgeVersion.version + "/download/installer.jar"
|
||||
)
|
||||
NeoForgeRemoteVersion.normalize(neoForgeVersion.version),
|
||||
Collections.singletonList(apiRoot + "/neoforge/version/" + neoForgeVersion.version + "/download/installer.jar")
|
||||
));
|
||||
}
|
||||
} finally {
|
||||
|
||||
+6
-8
@@ -2,7 +2,6 @@ package org.jackhuang.hmcl.download.neoforge;
|
||||
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.VersionList;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.HttpRequest;
|
||||
|
||||
import java.util.Collections;
|
||||
@@ -31,10 +30,7 @@ public final class NeoForgeOfficialVersionList extends VersionList<NeoForgeRemot
|
||||
@Override
|
||||
public Optional<NeoForgeRemoteVersion> getVersion(String gameVersion, String remoteVersion) {
|
||||
if (gameVersion.equals("1.20.1")) {
|
||||
remoteVersion = NeoForgeRemoteVersion.fixInvalidVersion(remoteVersion);
|
||||
if (!remoteVersion.equals("47.1.82")) {
|
||||
remoteVersion = "1.20.1-" + remoteVersion;
|
||||
}
|
||||
remoteVersion = NeoForgeRemoteVersion.normalize(remoteVersion);
|
||||
}
|
||||
return super.getVersion(gameVersion, remoteVersion);
|
||||
}
|
||||
@@ -52,7 +48,7 @@ public final class NeoForgeOfficialVersionList extends VersionList<NeoForgeRemot
|
||||
|
||||
for (String version : results[0].versions) {
|
||||
versions.put("1.20.1", new NeoForgeRemoteVersion(
|
||||
"1.20.1", StringUtils.removePrefix(version, "1.20.1-"),
|
||||
"1.20.1", NeoForgeRemoteVersion.normalize(version),
|
||||
Collections.singletonList(
|
||||
"https://maven.neoforged.net/releases/net/neoforged/forge/" + version + "/forge-" + version + "-installer.jar"
|
||||
)
|
||||
@@ -60,9 +56,11 @@ public final class NeoForgeOfficialVersionList extends VersionList<NeoForgeRemot
|
||||
}
|
||||
|
||||
for (String version : results[1].versions) {
|
||||
String mcVersion = "1." + version.substring(0, version.indexOf('.', version.indexOf('.') + 1));
|
||||
int si1 = version.indexOf('.'), si2 = version.indexOf('.', version.indexOf('.') + 1);
|
||||
String mcVersion = "1." + version.substring(0, Integer.parseInt(version.substring(si1 + 1, si2)) == 0 ? si1 : si2);
|
||||
|
||||
versions.put(mcVersion, new NeoForgeRemoteVersion(
|
||||
mcVersion, version,
|
||||
mcVersion, NeoForgeRemoteVersion.normalize(version),
|
||||
Collections.singletonList(
|
||||
"https://maven.neoforged.net/releases/net/neoforged/neoforge/" + version + "/neoforge-" + version + "-installer.jar"
|
||||
)
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ public class NeoForgeRemoteVersion extends RemoteVersion {
|
||||
return new NeoForgeInstallTask(dependencyManager, baseVersion, this);
|
||||
}
|
||||
|
||||
public static String fixInvalidVersion(String version) {
|
||||
public static String normalize(String version) {
|
||||
if (version.startsWith("1.20.1-")) {
|
||||
if (version.startsWith("forge-", "1.20.1-".length())) {
|
||||
return version.substring("1.20.1-forge-".length());
|
||||
|
||||
+111
-22
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.download.optifine;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import org.jackhuang.hmcl.download.VersionList;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
@@ -30,7 +31,6 @@ import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class OptiFineBMCLVersionList extends VersionList<OptiFineRemoteVersion> {
|
||||
@@ -48,34 +48,123 @@ public final class OptiFineBMCLVersionList extends VersionList<OptiFineRemoteVer
|
||||
return true;
|
||||
}
|
||||
|
||||
private String fromLookupVersion(String version) {
|
||||
switch (version) {
|
||||
case "1.8.0":
|
||||
return "1.8";
|
||||
case "1.9.0":
|
||||
return "1.9";
|
||||
default:
|
||||
return version;
|
||||
}
|
||||
}
|
||||
|
||||
private String toLookupVersion(String version) {
|
||||
switch (version) {
|
||||
case "1.8":
|
||||
return "1.8.0";
|
||||
case "1.9":
|
||||
return "1.9.0";
|
||||
default:
|
||||
return version;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<?> refreshAsync() {
|
||||
return HttpRequest.GET(apiRoot + "/optifine/versionlist").<List<OptiFineVersion>>getJsonAsync(new TypeToken<List<OptiFineVersion>>() {
|
||||
}.getType())
|
||||
.thenAcceptAsync(root -> {
|
||||
lock.writeLock().lock();
|
||||
}.getType()).thenAcceptAsync(root -> {
|
||||
lock.writeLock().lock();
|
||||
|
||||
try {
|
||||
versions.clear();
|
||||
Set<String> duplicates = new HashSet<>();
|
||||
for (OptiFineVersion element : root) {
|
||||
String version = element.getType() + "_" + element.getPatch();
|
||||
String mirror = "https://bmclapi2.bangbang93.com/optifine/" + element.getGameVersion() + "/" + element.getType() + "/" + element.getPatch();
|
||||
if (!duplicates.add(mirror))
|
||||
continue;
|
||||
try {
|
||||
versions.clear();
|
||||
Set<String> duplicates = new HashSet<>();
|
||||
for (OptiFineVersion element : root) {
|
||||
String version = element.getType() + "_" + element.getPatch();
|
||||
String mirror = "https://bmclapi2.bangbang93.com/optifine/" + toLookupVersion(element.getGameVersion()) + "/" + element.getType() + "/" + element.getPatch();
|
||||
if (!duplicates.add(mirror))
|
||||
continue;
|
||||
|
||||
boolean isPre = element.getPatch() != null && (element.getPatch().startsWith("pre") || element.getPatch().startsWith("alpha"));
|
||||
boolean isPre = element.getPatch() != null && (element.getPatch().startsWith("pre") || element.getPatch().startsWith("alpha"));
|
||||
|
||||
if (StringUtils.isBlank(element.getGameVersion()))
|
||||
continue;
|
||||
if (StringUtils.isBlank(element.getGameVersion()))
|
||||
continue;
|
||||
|
||||
String gameVersion = VersionNumber.normalize(element.getGameVersion());
|
||||
versions.put(gameVersion, new OptiFineRemoteVersion(gameVersion, version, Collections.singletonList(mirror), isPre));
|
||||
}
|
||||
} finally {
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
});
|
||||
String gameVersion = VersionNumber.normalize(fromLookupVersion(element.getGameVersion()));
|
||||
versions.put(gameVersion, new OptiFineRemoteVersion(gameVersion, version, Collections.singletonList(mirror), isPre));
|
||||
}
|
||||
} finally {
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @author huangyuhui
|
||||
*/
|
||||
private static final class OptiFineVersion {
|
||||
|
||||
@SerializedName("dl")
|
||||
private final String downloadLink;
|
||||
|
||||
@SerializedName("ver")
|
||||
private final String version;
|
||||
|
||||
@SerializedName("date")
|
||||
private final String date;
|
||||
|
||||
@SerializedName("type")
|
||||
private final String type;
|
||||
|
||||
@SerializedName("patch")
|
||||
private final String patch;
|
||||
|
||||
@SerializedName("mirror")
|
||||
private final String mirror;
|
||||
|
||||
@SerializedName("mcversion")
|
||||
private final String gameVersion;
|
||||
|
||||
public OptiFineVersion() {
|
||||
this(null, null, null, null, null, null, null);
|
||||
}
|
||||
|
||||
public OptiFineVersion(String downloadLink, String version, String date, String type, String patch, String mirror, String gameVersion) {
|
||||
this.downloadLink = downloadLink;
|
||||
this.version = version;
|
||||
this.date = date;
|
||||
this.type = type;
|
||||
this.patch = patch;
|
||||
this.mirror = mirror;
|
||||
this.gameVersion = gameVersion;
|
||||
}
|
||||
|
||||
public String getDownloadLink() {
|
||||
return downloadLink;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public String getDate() {
|
||||
return date;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getPatch() {
|
||||
return patch;
|
||||
}
|
||||
|
||||
public String getMirror() {
|
||||
return mirror;
|
||||
}
|
||||
|
||||
public String getGameVersion() {
|
||||
return gameVersion;
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
-7
@@ -125,20 +125,23 @@ public final class OptiFineInstallTask extends Task<Version> {
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
String originalMainClass = version.resolve(dependencyManager.getGameRepository()).getMainClass();
|
||||
if (!LibraryAnalyzer.VANILLA_MAIN.equals(originalMainClass) &&
|
||||
!LibraryAnalyzer.LAUNCH_WRAPPER_MAIN.equals(originalMainClass) &&
|
||||
!LibraryAnalyzer.MOD_LAUNCHER_MAIN.equals(originalMainClass) &&
|
||||
!LibraryAnalyzer.BOOTSTRAP_LAUNCHER_MAIN.equals(originalMainClass))
|
||||
if (!LibraryAnalyzer.FORGE_OPTIFINE_MAIN.contains(originalMainClass))
|
||||
throw new UnsupportedInstallationException(UnsupportedInstallationException.UNSUPPORTED_LAUNCH_WRAPPER);
|
||||
|
||||
List<Library> libraries = new ArrayList<>(4);
|
||||
libraries.add(optiFineLibrary);
|
||||
|
||||
FileUtils.copyFile(dest, gameRepository.getLibraryFile(version, optiFineInstallerLibrary).toPath());
|
||||
Path optiFineInstallerLibraryPath = gameRepository.getLibraryFile(version, optiFineInstallerLibrary).toPath();
|
||||
FileUtils.copyFile(dest, optiFineInstallerLibraryPath);
|
||||
|
||||
try (FileSystem fs2 = CompressingUtils.createWritableZipFileSystem(optiFineInstallerLibraryPath)) {
|
||||
Files.deleteIfExists(fs2.getPath("/META-INF/mods.toml"));
|
||||
}
|
||||
|
||||
// Install launch wrapper modified by OptiFine
|
||||
boolean hasLaunchWrapper = false;
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(dest)) {
|
||||
Path optiFineLibraryPath = gameRepository.getLibraryFile(version, optiFineLibrary).toPath();
|
||||
if (Files.exists(fs.getPath("optifine/Patcher.class"))) {
|
||||
String[] command = {
|
||||
JavaVersion.fromCurrentEnvironment().getBinary().toString(),
|
||||
@@ -147,13 +150,17 @@ public final class OptiFineInstallTask extends Task<Version> {
|
||||
"optifine.Patcher",
|
||||
gameRepository.getVersionJar(version).getAbsolutePath(),
|
||||
dest.toString(),
|
||||
gameRepository.getLibraryFile(version, optiFineLibrary).toString()
|
||||
optiFineLibraryPath.toString()
|
||||
};
|
||||
int exitCode = SystemUtils.callExternalProcess(command);
|
||||
if (exitCode != 0)
|
||||
throw new IOException("OptiFine patcher failed, command: " + new CommandBuilder().addAll(Arrays.asList(command)));
|
||||
} else {
|
||||
FileUtils.copyFile(dest, gameRepository.getLibraryFile(version, optiFineLibrary).toPath());
|
||||
FileUtils.copyFile(dest, optiFineLibraryPath);
|
||||
}
|
||||
|
||||
try (FileSystem fs2 = CompressingUtils.createWritableZipFileSystem(optiFineLibraryPath)) {
|
||||
Files.deleteIfExists(fs2.getPath("/META-INF/mods.toml"));
|
||||
}
|
||||
|
||||
Path launchWrapper2 = fs.getPath("launchwrapper-2.0.jar");
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 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.download.optifine;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class OptiFineVersion {
|
||||
|
||||
@SerializedName("dl")
|
||||
private final String downloadLink;
|
||||
|
||||
@SerializedName("ver")
|
||||
private final String version;
|
||||
|
||||
@SerializedName("date")
|
||||
private final String date;
|
||||
|
||||
@SerializedName("type")
|
||||
private final String type;
|
||||
|
||||
@SerializedName("patch")
|
||||
private final String patch;
|
||||
|
||||
@SerializedName("mirror")
|
||||
private final String mirror;
|
||||
|
||||
@SerializedName("mcversion")
|
||||
private final String gameVersion;
|
||||
|
||||
public OptiFineVersion() {
|
||||
this(null, null, null, null, null, null, null);
|
||||
}
|
||||
|
||||
public OptiFineVersion(String downloadLink, String version, String date, String type, String patch, String mirror, String gameVersion) {
|
||||
this.downloadLink = downloadLink;
|
||||
this.version = version;
|
||||
this.date = date;
|
||||
this.type = type;
|
||||
this.patch = patch;
|
||||
this.mirror = mirror;
|
||||
this.gameVersion = gameVersion;
|
||||
}
|
||||
|
||||
public String getDownloadLink() {
|
||||
return downloadLink;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public String getDate() {
|
||||
return date;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getPatch() {
|
||||
return patch;
|
||||
}
|
||||
|
||||
public String getMirror() {
|
||||
return mirror;
|
||||
}
|
||||
|
||||
public String getGameVersion() {
|
||||
return gameVersion;
|
||||
}
|
||||
}
|
||||
@@ -21,14 +21,7 @@ import com.google.gson.JsonParseException;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import org.jackhuang.hmcl.download.MaintainTask;
|
||||
import org.jackhuang.hmcl.download.game.VersionJsonSaveTask;
|
||||
import org.jackhuang.hmcl.event.Event;
|
||||
import org.jackhuang.hmcl.event.EventBus;
|
||||
import org.jackhuang.hmcl.event.GameJsonParseFailedEvent;
|
||||
import org.jackhuang.hmcl.event.LoadedOneVersionEvent;
|
||||
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
|
||||
import org.jackhuang.hmcl.event.RefreshingVersionsEvent;
|
||||
import org.jackhuang.hmcl.event.RemoveVersionEvent;
|
||||
import org.jackhuang.hmcl.event.RenameVersionEvent;
|
||||
import org.jackhuang.hmcl.event.*;
|
||||
import org.jackhuang.hmcl.game.tlauncher.TLauncherVersion;
|
||||
import org.jackhuang.hmcl.mod.ModManager;
|
||||
import org.jackhuang.hmcl.mod.ModpackConfiguration;
|
||||
@@ -45,13 +38,7 @@ import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.TreeMap;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -126,9 +113,12 @@ public class DefaultGameRepository implements GameRepository {
|
||||
@Override
|
||||
public File getRunDirectory(String id) {
|
||||
switch (getGameDirectoryType(id)) {
|
||||
case VERSION_FOLDER: return getVersionRoot(id);
|
||||
case ROOT_FOLDER: return getBaseDirectory();
|
||||
default: throw new IllegalStateException();
|
||||
case VERSION_FOLDER:
|
||||
return getVersionRoot(id);
|
||||
case ROOT_FOLDER:
|
||||
return getBaseDirectory();
|
||||
default:
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -458,7 +448,7 @@ public class DefaultGameRepository implements GameRepository {
|
||||
return assetsDir;
|
||||
|
||||
if (index.isVirtual()) {
|
||||
Path resourcesDir = getBaseDirectory().toPath().resolve("resources");
|
||||
Path resourcesDir = getRunDirectory(version).toPath().resolve("resources");
|
||||
|
||||
int cnt = 0;
|
||||
int tot = index.getObjects().entrySet().size();
|
||||
@@ -507,18 +497,20 @@ public class DefaultGameRepository implements GameRepository {
|
||||
|
||||
/**
|
||||
* read modpack configuration for a version.
|
||||
*
|
||||
* @param version version installed as modpack
|
||||
* @param <M> manifest type of ModpackConfiguration
|
||||
* @param <M> manifest type of ModpackConfiguration
|
||||
* @return modpack configuration object, or null if this version is not a modpack.
|
||||
* @throws VersionNotFoundException if version does not exist.
|
||||
* @throws IOException if an i/o error occurs.
|
||||
* @throws IOException if an i/o error occurs.
|
||||
*/
|
||||
@Nullable
|
||||
public <M> ModpackConfiguration<M> readModpackConfiguration(String version) throws IOException, VersionNotFoundException {
|
||||
if (!hasVersion(version)) throw new VersionNotFoundException(version);
|
||||
File file = getModpackConfiguration(version);
|
||||
if (!file.exists()) return null;
|
||||
return JsonUtils.GSON.fromJson(FileUtils.readText(file), new TypeToken<ModpackConfiguration<M>>(){}.getType());
|
||||
return JsonUtils.GSON.fromJson(FileUtils.readText(file), new TypeToken<ModpackConfiguration<M>>() {
|
||||
}.getType());
|
||||
}
|
||||
|
||||
public boolean isModpack(String version) {
|
||||
|
||||
@@ -35,7 +35,6 @@ import java.util.stream.StreamSupport;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/**
|
||||
@@ -48,8 +47,11 @@ final class GameVersion {
|
||||
private static Optional<String> getVersionFromJson(InputStream versionJson) {
|
||||
try {
|
||||
Map<?, ?> version = JsonUtils.fromNonNullJsonFully(versionJson, Map.class);
|
||||
return tryCast(version.get("id"), String.class);
|
||||
} catch (IOException | JsonParseException e) {
|
||||
String id = (String) version.get("id");
|
||||
if (id != null && id.contains(" / "))
|
||||
id = id.substring(0, id.indexOf(" / "));
|
||||
return Optional.ofNullable(id);
|
||||
} catch (IOException | JsonParseException | ClassCastException e) {
|
||||
LOG.warning("Failed to parse version.json", e);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@@ -295,7 +295,10 @@ public class Version implements Comparable<Version>, Validation {
|
||||
}
|
||||
}
|
||||
|
||||
if (patches != null && !patches.isEmpty()) {
|
||||
if (patches == null) {
|
||||
// This is a version from external launcher. NO need to resolve the patches.
|
||||
return thisVersion;
|
||||
} else if (!patches.isEmpty()) {
|
||||
// Assume patches themselves do not have patches recursively.
|
||||
List<Version> sortedPatches = patches.stream()
|
||||
.sorted(Comparator.comparing(Version::getPriority))
|
||||
|
||||
+7
-3
@@ -94,6 +94,10 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
|
||||
return "asc";
|
||||
}
|
||||
|
||||
private int calculateTotalPages(Response<List<CurseAddon>> response, int pageSize) {
|
||||
return (int) Math.ceil((double) Math.min(response.pagination.totalCount, 10000) / pageSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SearchResult search(String gameVersion, @Nullable RemoteModRepository.Category category, int pageOffset, int pageSize, String searchFilter, SortType sortType, SortOrder sortOrder) throws IOException {
|
||||
int categoryId = 0;
|
||||
@@ -112,7 +116,7 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
|
||||
.getJson(new TypeToken<Response<List<CurseAddon>>>() {
|
||||
}.getType());
|
||||
if (searchFilter.isEmpty()) {
|
||||
return new SearchResult(response.getData().stream().map(CurseAddon::toMod), (int)Math.ceil((double)response.pagination.totalCount / pageSize));
|
||||
return new SearchResult(response.getData().stream().map(CurseAddon::toMod), calculateTotalPages(response, pageSize));
|
||||
}
|
||||
|
||||
// https://github.com/HMCL-dev/HMCL/issues/1549
|
||||
@@ -135,7 +139,7 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
|
||||
}
|
||||
|
||||
return pair(remoteMod, diff);
|
||||
}).sorted(Comparator.comparingInt(Pair::getValue)).map(Pair::getKey), response.getData().stream().map(CurseAddon::toMod), response.pagination.totalCount);
|
||||
}).sorted(Comparator.comparingInt(Pair::getValue)).map(Pair::getKey), response.getData().stream().map(CurseAddon::toMod), calculateTotalPages(response, pageSize));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -156,7 +160,7 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
|
||||
|
||||
long hash = Integer.toUnsignedLong(MurmurHash2.hash32(baos.toByteArray(), baos.size(), 1));
|
||||
|
||||
Response<FingerprintMatchesResult> response = HttpRequest.POST(PREFIX + "/v1/fingerprints")
|
||||
Response<FingerprintMatchesResult> response = HttpRequest.POST(PREFIX + "/v1/fingerprints/432")
|
||||
.json(mapOf(pair("fingerprints", Collections.singletonList(hash))))
|
||||
.header("X-API-KEY", apiKey)
|
||||
.getJson(new TypeToken<Response<FingerprintMatchesResult>>() {
|
||||
|
||||
+1
-1
@@ -95,7 +95,7 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
|
||||
Response<ProjectSearchResult> response = HttpRequest.GET(NetworkUtils.withQuery(PREFIX + "/v2/search", query))
|
||||
.getJson(new TypeToken<Response<ProjectSearchResult>>() {
|
||||
}.getType());
|
||||
return new SearchResult(response.getHits().stream().map(ProjectSearchResult::toMod), (int)Math.ceil((double)response.totalHits / pageSize));
|
||||
return new SearchResult(response.getHits().stream().map(ProjectSearchResult::toMod), (int) Math.ceil((double) response.totalHits / pageSize));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -187,13 +187,18 @@ public abstract class HttpRequest {
|
||||
os.write(bytes);
|
||||
}
|
||||
|
||||
URL url = new URL(this.url);
|
||||
|
||||
if (responseCodeTester != null) {
|
||||
responseCodeTester.accept(new URL(url), con.getResponseCode());
|
||||
responseCodeTester.accept(url, con.getResponseCode());
|
||||
} else {
|
||||
if (con.getResponseCode() / 100 != 2) {
|
||||
if (!ignoreHttpCode && !toleratedHttpCodes.contains(con.getResponseCode())) {
|
||||
String data = NetworkUtils.readData(con);
|
||||
throw new ResponseCodeException(new URL(url), con.getResponseCode(), data);
|
||||
try {
|
||||
throw new ResponseCodeException(url, con.getResponseCode(), NetworkUtils.readData(con));
|
||||
} catch (IOException e) {
|
||||
throw new ResponseCodeException(url, con.getResponseCode(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,13 @@ public final class ResponseCodeException extends IOException {
|
||||
this.data = null;
|
||||
}
|
||||
|
||||
public ResponseCodeException(URL url, int responseCode, Throwable cause) {
|
||||
super("Unable to request url " + url + ", response code: " + responseCode, cause);
|
||||
this.url = url;
|
||||
this.responseCode = responseCode;
|
||||
this.data = null;
|
||||
}
|
||||
|
||||
public ResponseCodeException(URL url, int responseCode, String data) {
|
||||
super("Unable to request url " + url + ", response code: " + responseCode + ", data: " + data);
|
||||
this.url = url;
|
||||
|
||||
@@ -111,13 +111,14 @@ public final class Logger {
|
||||
}
|
||||
}
|
||||
|
||||
private void onShutdown() {
|
||||
private void onExit() {
|
||||
shutdown();
|
||||
try {
|
||||
loggerThread.join();
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
|
||||
String caller = CLASS_NAME + ".onShutdown";
|
||||
String caller = CLASS_NAME + ".onExit";
|
||||
|
||||
if (logRetention > 0 && logFile != null) {
|
||||
List<Pair<Path, int[]>> list = new ArrayList<>();
|
||||
@@ -258,7 +259,7 @@ public final class Logger {
|
||||
loggerThread.setName("HMCL Logger Thread");
|
||||
loggerThread.start();
|
||||
|
||||
Thread cleanerThread = new Thread(this::onShutdown);
|
||||
Thread cleanerThread = new Thread(this::onExit);
|
||||
cleanerThread.setName("HMCL Logger Shutdown Hook");
|
||||
Runtime.getRuntime().addShutdownHook(cleanerThread);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2024 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.versioning;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -10,6 +27,9 @@ import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public abstract class GameVersionNumber implements Comparable<GameVersionNumber> {
|
||||
|
||||
public static String[] getDefaultGameVersions() {
|
||||
@@ -22,26 +42,21 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
|
||||
char ch = version.charAt(0);
|
||||
switch (ch) {
|
||||
case 'r':
|
||||
return Old.parsePreClassic(version);
|
||||
case 'a':
|
||||
case 'b':
|
||||
case 'c':
|
||||
return Old.parseAlphaBetaClassic(version);
|
||||
case 'i':
|
||||
return Old.parseInfdev(version);
|
||||
return Old.parse(version);
|
||||
}
|
||||
|
||||
if (version.equals("0.0")) {
|
||||
if (version.equals("0.0"))
|
||||
return Release.ZERO;
|
||||
}
|
||||
|
||||
if (version.startsWith("1.")) {
|
||||
if (version.startsWith("1."))
|
||||
return Release.parse(version);
|
||||
}
|
||||
|
||||
if (version.length() == 6 && version.charAt(2) == 'w') {
|
||||
if (version.length() == 6 && version.charAt(2) == 'w')
|
||||
return Snapshot.parse(version);
|
||||
}
|
||||
}
|
||||
} catch (IllegalArgumentException ignore) {
|
||||
}
|
||||
@@ -97,9 +112,8 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
|
||||
|
||||
@Override
|
||||
public int compareTo(@NotNull GameVersionNumber other) {
|
||||
if (this.getType() != other.getType()) {
|
||||
if (this.getType() != other.getType())
|
||||
return Integer.compare(this.getType().ordinal(), other.getType().ordinal());
|
||||
}
|
||||
|
||||
return compareToImpl(other);
|
||||
}
|
||||
@@ -110,27 +124,25 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
|
||||
}
|
||||
|
||||
static final class Old extends GameVersionNumber {
|
||||
|
||||
private static final Pattern PATTERN = Pattern.compile("[abc](?<major>[0-9]+)\\.(?<minor>[0-9]+)(\\.(?<patch>[0-9]+))?([^0-9]*(?<additional>[0-9]+).*)?");
|
||||
|
||||
static Old parsePreClassic(String value) {
|
||||
int version;
|
||||
try {
|
||||
version = Integer.parseInt(value.substring("rd-".length()));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
return new Old(value, Type.PRE_CLASSIC, version, 0, 0, 0);
|
||||
}
|
||||
|
||||
static Old parseAlphaBetaClassic(String value) {
|
||||
Matcher matcher = PATTERN.matcher(value);
|
||||
if (!matcher.matches()) {
|
||||
throw new IllegalArgumentException(value);
|
||||
}
|
||||
|
||||
static Old parse(String value) {
|
||||
Type type;
|
||||
int prefixLength = 1;
|
||||
switch (value.charAt(0)) {
|
||||
case 'r':
|
||||
if (!value.startsWith("rd-")) {
|
||||
throw new IllegalArgumentException(value);
|
||||
}
|
||||
|
||||
type = Type.PRE_CLASSIC;
|
||||
prefixLength = "rd-".length();
|
||||
break;
|
||||
case 'i':
|
||||
if (!value.startsWith("inf-")) {
|
||||
throw new IllegalArgumentException(value);
|
||||
}
|
||||
type = Type.INFDEV;
|
||||
prefixLength = "inf-".length();
|
||||
break;
|
||||
case 'a':
|
||||
type = Type.ALPHA;
|
||||
break;
|
||||
@@ -141,60 +153,22 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
|
||||
type = Type.CLASSIC;
|
||||
break;
|
||||
default:
|
||||
throw new AssertionError(value);
|
||||
}
|
||||
|
||||
int major = Integer.parseInt(matcher.group("major"));
|
||||
int minor = Integer.parseInt(matcher.group("minor"));
|
||||
|
||||
String patchString = matcher.group("patch");
|
||||
int patch = patchString != null ? Integer.parseInt(patchString) : 0;
|
||||
|
||||
String additionalString = matcher.group("additional");
|
||||
int additional = additionalString != null ? Integer.parseInt(additionalString) : 0;
|
||||
|
||||
return new Old(value, type, major, minor, patch, additional);
|
||||
}
|
||||
|
||||
static Old parseInfdev(String value) {
|
||||
String version = value.substring("inf-".length());
|
||||
int major;
|
||||
int patch;
|
||||
|
||||
try {
|
||||
major = Integer.parseInt(version);
|
||||
patch = 0;
|
||||
} catch (NumberFormatException e) {
|
||||
int idx = version.indexOf('-');
|
||||
if (idx >= 0) {
|
||||
try {
|
||||
major = Integer.parseInt(version.substring(0, idx));
|
||||
patch = Integer.parseInt(version.substring(idx + 1));
|
||||
} catch (NumberFormatException ignore) {
|
||||
throw new IllegalArgumentException(value);
|
||||
}
|
||||
} else {
|
||||
throw new IllegalArgumentException(value);
|
||||
}
|
||||
}
|
||||
|
||||
return new Old(value, Type.INFDEV, major, 0, patch, 0);
|
||||
}
|
||||
if (value.length() < prefixLength + 1 || !Character.isDigit(value.charAt(prefixLength)))
|
||||
throw new IllegalArgumentException(value);
|
||||
|
||||
return new Old(value, type, VersionNumber.asVersion(value.substring(prefixLength)));
|
||||
}
|
||||
|
||||
final Type type;
|
||||
final int major;
|
||||
final int minor;
|
||||
final int patch;
|
||||
final int additional;
|
||||
final VersionNumber versionNumber;
|
||||
|
||||
private Old(String value, Type type, int major, int minor, int patch, int additional) {
|
||||
private Old(String value, Type type, VersionNumber versionNumber) {
|
||||
super(value);
|
||||
this.type = type;
|
||||
this.major = major;
|
||||
this.minor = minor;
|
||||
this.patch = patch;
|
||||
this.additional = additional;
|
||||
this.versionNumber = versionNumber;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -204,42 +178,26 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
|
||||
|
||||
@Override
|
||||
int compareToImpl(@NotNull GameVersionNumber other) {
|
||||
Old that = (Old) other;
|
||||
int c = Integer.compare(this.major, that.major);
|
||||
if (c != 0) {
|
||||
return c;
|
||||
}
|
||||
|
||||
c = Integer.compare(this.minor, that.minor);
|
||||
if (c != 0) {
|
||||
return c;
|
||||
}
|
||||
|
||||
c = Integer.compare(this.patch, that.patch);
|
||||
if (c != 0) {
|
||||
return c;
|
||||
}
|
||||
|
||||
return Integer.compare(this.additional, that.additional);
|
||||
return this.versionNumber.compareTo(((Old) other).versionNumber);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
if (!(o instanceof Old)) return false;
|
||||
Old other = (Old) o;
|
||||
return major == other.major && minor == other.minor && patch == other.patch && additional == other.additional && type == other.type;
|
||||
return type == other.type && this.versionNumber.compareTo(other.versionNumber) == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(type, major, minor, patch, additional);
|
||||
return Objects.hash(type, versionNumber.hashCode());
|
||||
}
|
||||
}
|
||||
|
||||
static final class Release extends GameVersionNumber {
|
||||
|
||||
private static final Pattern PATTERN = Pattern.compile("1\\.(?<minor>[0-9]+)(\\.(?<patch>[0-9]+))?((?<eaType>(-[a-zA-Z]+| Pre-Release ))(?<eaVersion>[0-9]+))?");
|
||||
private static final Pattern PATTERN = Pattern.compile("1\\.(?<minor>[0-9]+)(\\.(?<patch>[0-9]+))?((?<eaType>(-[a-zA-Z]+| Pre-Release ))(?<eaVersion>.+))?");
|
||||
|
||||
static final int TYPE_GA = Integer.MAX_VALUE;
|
||||
|
||||
@@ -248,7 +206,7 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
|
||||
static final int TYPE_PRE = 2;
|
||||
static final int TYPE_RC = 3;
|
||||
|
||||
static final Release ZERO = new Release("0.0", 0, 0, 0, TYPE_GA, 0);
|
||||
static final Release ZERO = new Release("0.0", 0, 0, 0, TYPE_GA, VersionNumber.ZERO);
|
||||
|
||||
static Release parse(String value) {
|
||||
Matcher matcher = PATTERN.matcher(value);
|
||||
@@ -276,7 +234,7 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
|
||||
}
|
||||
|
||||
String eaVersionString = matcher.group("eaVersion");
|
||||
int eaVersion = eaVersionString == null ? 0 : Integer.parseInt(eaVersionString);
|
||||
VersionNumber eaVersion = eaVersionString != null ? VersionNumber.asVersion(eaVersionString) : VersionNumber.ZERO;
|
||||
|
||||
return new Release(value, 1, minor, patch, eaType, eaVersion);
|
||||
}
|
||||
@@ -286,9 +244,9 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
|
||||
private final int patch;
|
||||
|
||||
private final int eaType;
|
||||
private final int eaVersion;
|
||||
private final VersionNumber eaVersion;
|
||||
|
||||
Release(String value, int major, int minor, int patch, int eaType, int eaVersion) {
|
||||
Release(String value, int major, int minor, int patch, int eaType, VersionNumber eaVersion) {
|
||||
super(value);
|
||||
this.major = major;
|
||||
this.minor = minor;
|
||||
@@ -323,36 +281,31 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
|
||||
return c;
|
||||
}
|
||||
|
||||
return Integer.compare(this.eaVersion, other.eaVersion);
|
||||
return this.eaVersion.compareTo(other.eaVersion);
|
||||
}
|
||||
|
||||
int compareToSnapshot(Snapshot other) {
|
||||
int idx = Arrays.binarySearch(Versions.SNAPSHOT_INTS, other.intValue);
|
||||
if (idx >= 0) {
|
||||
if (idx >= 0)
|
||||
return this.compareToRelease(Versions.SNAPSHOT_PREV[idx]) <= 0 ? -1 : 1;
|
||||
}
|
||||
|
||||
idx = -(idx + 1);
|
||||
if (idx == Versions.SNAPSHOT_INTS.length) {
|
||||
if (idx == Versions.SNAPSHOT_INTS.length)
|
||||
return -1;
|
||||
}
|
||||
|
||||
return this.compareToRelease(Versions.SNAPSHOT_PREV[idx]) <= 0 ? -1 : 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
int compareToImpl(@NotNull GameVersionNumber other) {
|
||||
if (other instanceof Release) {
|
||||
if (other instanceof Release)
|
||||
return compareToRelease((Release) other);
|
||||
}
|
||||
|
||||
if (other instanceof Snapshot) {
|
||||
if (other instanceof Snapshot)
|
||||
return compareToSnapshot((Snapshot) other);
|
||||
}
|
||||
|
||||
if (other instanceof Special) {
|
||||
if (other instanceof Special)
|
||||
return -((Special) other).compareToReleaseOrSnapshot(this);
|
||||
}
|
||||
|
||||
throw new AssertionError(other.getClass());
|
||||
}
|
||||
@@ -367,15 +320,14 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Release other = (Release) o;
|
||||
return major == other.major && minor == other.minor && patch == other.patch && eaType == other.eaType && eaVersion == other.eaVersion;
|
||||
return major == other.major && minor == other.minor && patch == other.patch && eaType == other.eaType && eaVersion.equals(other.eaVersion);
|
||||
}
|
||||
}
|
||||
|
||||
static final class Snapshot extends GameVersionNumber {
|
||||
static Snapshot parse(String value) {
|
||||
if (value.length() != 6 || value.charAt(2) != 'w') {
|
||||
if (value.length() != 6 || value.charAt(2) != 'w')
|
||||
throw new IllegalArgumentException(value);
|
||||
}
|
||||
|
||||
int year;
|
||||
int week;
|
||||
@@ -387,9 +339,8 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
|
||||
}
|
||||
|
||||
char suffix = value.charAt(5);
|
||||
if ((suffix < 'a' || suffix > 'z') && suffix != '~') {
|
||||
if ((suffix < 'a' || suffix > 'z') && suffix != '~')
|
||||
throw new IllegalArgumentException(value);
|
||||
}
|
||||
|
||||
return new Snapshot(value, year, week, suffix);
|
||||
}
|
||||
@@ -412,17 +363,14 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
|
||||
|
||||
@Override
|
||||
int compareToImpl(@NotNull GameVersionNumber other) {
|
||||
if (other instanceof Release) {
|
||||
if (other instanceof Release)
|
||||
return -((Release) other).compareToSnapshot(this);
|
||||
}
|
||||
|
||||
if (other instanceof Snapshot) {
|
||||
if (other instanceof Snapshot)
|
||||
return Integer.compare(this.intValue, ((Snapshot) other).intValue);
|
||||
}
|
||||
|
||||
if (other instanceof Special) {
|
||||
if (other instanceof Special)
|
||||
return -((Special) other).compareToReleaseOrSnapshot(this);
|
||||
}
|
||||
|
||||
throw new AssertionError(other.getClass());
|
||||
}
|
||||
@@ -460,9 +408,8 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
|
||||
}
|
||||
|
||||
VersionNumber asVersionNumber() {
|
||||
if (versionNumber != null) {
|
||||
if (versionNumber != null)
|
||||
return versionNumber;
|
||||
}
|
||||
|
||||
return versionNumber = VersionNumber.asVersion(value);
|
||||
}
|
||||
@@ -473,9 +420,7 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
|
||||
v = ((Special) v).prev;
|
||||
}
|
||||
|
||||
if (v == null) {
|
||||
throw new AssertionError("version: " + value);
|
||||
}
|
||||
if (v == null) throw new AssertionError("version: " + value);
|
||||
|
||||
return v;
|
||||
}
|
||||
@@ -493,28 +438,23 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
|
||||
}
|
||||
|
||||
int compareToSpecial(Special other) {
|
||||
if (this.isUnknown()) {
|
||||
if (this.isUnknown())
|
||||
return other.isUnknown() ? this.asVersionNumber().compareTo(other.asVersionNumber()) : 1;
|
||||
}
|
||||
|
||||
if (other.isUnknown()) {
|
||||
if (other.isUnknown())
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (this.value.equals(other.value)) {
|
||||
if (this.value.equals(other.value))
|
||||
return 0;
|
||||
}
|
||||
|
||||
int c = this.getPrevNormalVersion().compareTo(other.getPrevNormalVersion());
|
||||
if (c != 0) {
|
||||
if (c != 0)
|
||||
return c;
|
||||
}
|
||||
|
||||
GameVersionNumber v = prev;
|
||||
while (v instanceof Special) {
|
||||
if (v == other) {
|
||||
if (v == other)
|
||||
return 1;
|
||||
}
|
||||
|
||||
v = ((Special) v).prev;
|
||||
}
|
||||
@@ -524,17 +464,14 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
|
||||
|
||||
@Override
|
||||
int compareToImpl(@NotNull GameVersionNumber o) {
|
||||
if (o instanceof Release) {
|
||||
if (o instanceof Release)
|
||||
return compareToReleaseOrSnapshot(o);
|
||||
}
|
||||
|
||||
if (o instanceof Snapshot) {
|
||||
if (o instanceof Snapshot)
|
||||
return compareToReleaseOrSnapshot(o);
|
||||
}
|
||||
|
||||
if (o instanceof Special) {
|
||||
if (o instanceof Special)
|
||||
return compareToSpecial((Special) o);
|
||||
}
|
||||
|
||||
throw new AssertionError(o.getClass());
|
||||
}
|
||||
@@ -566,7 +503,6 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
|
||||
List<Snapshot> snapshots = new ArrayList<>(1024);
|
||||
List<Release> snapshotPrev = new ArrayList<>(1024);
|
||||
|
||||
// Convert it to dynamic resource after the website is repaired?
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(GameVersionNumber.class.getResourceAsStream("/assets/game/versions.txt"), StandardCharsets.US_ASCII))) {
|
||||
Release currentRelease = null;
|
||||
GameVersionNumber prev = null;
|
||||
|
||||
@@ -33,6 +33,8 @@ import java.util.Objects;
|
||||
*/
|
||||
public final class VersionNumber implements Comparable<VersionNumber> {
|
||||
|
||||
public static final VersionNumber ZERO = asVersion("0");
|
||||
|
||||
public static VersionNumber asVersion(String version) {
|
||||
Objects.requireNonNull(version);
|
||||
return new VersionNumber(version);
|
||||
|
||||
@@ -756,4 +756,15 @@
|
||||
1.20.5
|
||||
1.20.6-rc1
|
||||
1.20.6
|
||||
24w18a
|
||||
24w18a
|
||||
24w19a
|
||||
24w19b
|
||||
24w20a
|
||||
24w21a
|
||||
24w21b
|
||||
1.21-pre1
|
||||
1.21-pre2
|
||||
1.21-pre3
|
||||
1.21-pre4
|
||||
1.21-rc1
|
||||
1.21
|
||||
+67
-28
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2024 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.versioning;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -5,6 +22,7 @@ import org.junit.jupiter.api.Test;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
|
||||
@@ -13,18 +31,25 @@ import static org.junit.jupiter.api.Assertions.*;
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public class GameVersionNumberTest {
|
||||
public final class GameVersionNumberTest {
|
||||
|
||||
@Test
|
||||
public void testSortVersions() throws IOException {
|
||||
private static List<String> readVersions() {
|
||||
List<String> versions = new ArrayList<>();
|
||||
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(GameVersionNumber.class.getResourceAsStream("/assets/game/versions.txt"), StandardCharsets.UTF_8))) {
|
||||
for (String line; (line = reader.readLine()) != null && !line.isEmpty(); ) {
|
||||
versions.add(line);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
|
||||
return versions;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSortVersions() {
|
||||
List<String> versions = readVersions();
|
||||
List<String> copied = new ArrayList<>(versions);
|
||||
Collections.shuffle(copied, new Random(0));
|
||||
copied.sort(Comparator.comparing(GameVersionNumber::asGameVersion));
|
||||
@@ -42,52 +67,57 @@ public class GameVersionNumberTest {
|
||||
|
||||
private static void assertGameVersionEquals(String version1, String version2) {
|
||||
assertEquals(0, GameVersionNumber.asGameVersion(version1).compareTo(version2), errorMessage(version1, version2));
|
||||
assertEquals(GameVersionNumber.asGameVersion(version1), GameVersionNumber.asGameVersion(version2), errorMessage(version1, version2));
|
||||
}
|
||||
|
||||
private static void assertLessThan(String version1, String version2) {
|
||||
assertTrue(GameVersionNumber.asGameVersion(version1).compareTo(version2) < 0, errorMessage(version1, version2));
|
||||
private static String toString(GameVersionNumber gameVersionNumber) {
|
||||
return gameVersionNumber.getClass().getSimpleName();
|
||||
}
|
||||
|
||||
private static void assertOrder(String... versions) {
|
||||
for (int i = 0; i < versions.length - 1; i++) {
|
||||
GameVersionNumber version1 = GameVersionNumber.asGameVersion(versions[i]);
|
||||
|
||||
//noinspection EqualsWithItself
|
||||
assertEquals(0, version1.compareTo(version1), "version=" + versions[i]);
|
||||
assertGameVersionEquals(versions[i]);
|
||||
|
||||
for (int j = i + 1; j < versions.length; j++) {
|
||||
GameVersionNumber version2 = GameVersionNumber.asGameVersion(versions[j]);
|
||||
|
||||
assertEquals(-1, version1.compareTo(version2), String.format("version1=%s, version2=%s", versions[i], versions[j]));
|
||||
assertEquals(1, version2.compareTo(version1), String.format("version1=%s, version2=%s", versions[i], versions[j]));
|
||||
assertEquals(-1, version1.compareTo(version2), String.format("version1=%s (%s), version2=%s (%s)", versions[i], toString(version1), versions[j], toString(version2)));
|
||||
assertEquals(1, version2.compareTo(version1), String.format("version1=%s (%s), version2=%s (%s)", versions[i], toString(version1), versions[j], toString(version2)));
|
||||
}
|
||||
}
|
||||
|
||||
assertGameVersionEquals(versions[versions.length - 1]);
|
||||
}
|
||||
|
||||
private void assertOldVersion(String oldVersion, GameVersionNumber.Type type, String versionNumber) {
|
||||
GameVersionNumber version = GameVersionNumber.asGameVersion(oldVersion);
|
||||
assertInstanceOf(GameVersionNumber.Old.class, version);
|
||||
GameVersionNumber.Old old = (GameVersionNumber.Old) version;
|
||||
assertSame(type, old.type);
|
||||
assertEquals(VersionNumber.asVersion(versionNumber), old.versionNumber);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseOld() {
|
||||
{
|
||||
GameVersionNumber version = GameVersionNumber.asGameVersion("b1.0");
|
||||
assertInstanceOf(GameVersionNumber.Old.class, version);
|
||||
GameVersionNumber.Old old = (GameVersionNumber.Old) version;
|
||||
assertEquals(GameVersionNumber.Type.BETA, old.type);
|
||||
assertEquals(1, old.major);
|
||||
assertEquals(0, old.minor);
|
||||
assertEquals(0, old.patch);
|
||||
assertEquals(0, old.additional);
|
||||
}
|
||||
assertOldVersion("rd-132211", GameVersionNumber.Type.PRE_CLASSIC, "132211");
|
||||
assertOldVersion("inf-20100618", GameVersionNumber.Type.INFDEV, "20100618");
|
||||
assertOldVersion("inf-20100330-1", GameVersionNumber.Type.INFDEV, "20100330-1");
|
||||
assertOldVersion("a1.0.6", GameVersionNumber.Type.ALPHA, "1.0.6");
|
||||
assertOldVersion("a1.0.8_01", GameVersionNumber.Type.ALPHA, "1.0.8_01");
|
||||
assertOldVersion("a1.0.13_01-1", GameVersionNumber.Type.ALPHA, "1.0.13_01-1");
|
||||
assertOldVersion("b1.0", GameVersionNumber.Type.BETA, "1.0");
|
||||
assertOldVersion("b1.0_01", GameVersionNumber.Type.BETA, "1.0_01");
|
||||
assertOldVersion("b1.8-pre1-2", GameVersionNumber.Type.BETA, "1.8-pre1-2");
|
||||
assertOldVersion("b1.9-pre1", GameVersionNumber.Type.BETA, "1.9-pre1");
|
||||
}
|
||||
|
||||
{
|
||||
GameVersionNumber version = GameVersionNumber.asGameVersion("b1.0_01");
|
||||
assertInstanceOf(GameVersionNumber.Old.class, version);
|
||||
GameVersionNumber.Old old = (GameVersionNumber.Old) version;
|
||||
assertEquals(GameVersionNumber.Type.BETA, old.type);
|
||||
assertEquals(1, old.major);
|
||||
assertEquals(0, old.minor);
|
||||
assertEquals(0, old.patch);
|
||||
assertEquals(1, old.additional);
|
||||
@Test
|
||||
public void testParseNew() {
|
||||
List<String> versions = readVersions();
|
||||
for (String version : versions) {
|
||||
assertFalse(GameVersionNumber.asGameVersion(version) instanceof GameVersionNumber.Old, "version=" + version);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,15 +168,24 @@ public class GameVersionNumberTest {
|
||||
"inf-20100330-2",
|
||||
"inf-20100618",
|
||||
"a1.0.4",
|
||||
"a1.0.8_01",
|
||||
"a1.0.10",
|
||||
"a1.0.13_01-1",
|
||||
"a1.0.17_02",
|
||||
"a1.0.17_04",
|
||||
"a1.1.0",
|
||||
"a1.1.1",
|
||||
"b1.0",
|
||||
"b1.0_01",
|
||||
"b1.1_02",
|
||||
"b1.2",
|
||||
"b1.8-pre1-2",
|
||||
"b1.8.1",
|
||||
"0.0",
|
||||
"1.0.0-rc1",
|
||||
"1.0.0-rc2-1",
|
||||
"1.0.0-rc2-2",
|
||||
"1.0.0-rc2-3",
|
||||
"1.0",
|
||||
"11w47a",
|
||||
"1.1",
|
||||
|
||||
+11
-11
@@ -2,17 +2,17 @@
|
||||
|
||||
English | [简体中文](PLATFORM_cn.md)
|
||||
|
||||
| | Windows | Linux | Mac OS | FreeBSD |
|
||||
|----------------------------|:--------------------------------------------------|:---------------------------|:------------------------------------------------------------------------|:---------------------------|
|
||||
| x86-64 | ✅️ | ✅️ | ✅️ | 👌 (Minecraft 1.13~1.20.4) |
|
||||
| x86 | ✅️ | ✅️ | / | / |
|
||||
| ARM64 | 👌 (Minecraft 1.8~1.18.2)<br/>✅ (Minecraft 1.19+) | 👌 (Minecraft 1.8~1.20.4) | 👌 (Minecraft 1.6~1.18.2)<br/>✅ (Minecraft 1.19+)<br/>✅ (use Rosetta 2) | ❔ |
|
||||
| ARM32 | /️ | 👌 (Minecraft 1.8~1.20.1) | / | / |
|
||||
| MIPS64el | / | 👌 (Minecraft 1.8~1.20.1) | / | / |
|
||||
| RISC-V 64 | / | 👌 (Minecraft 1.13~1.20.4) | / | / |
|
||||
| LoongArch64 | / | 👌 (Minecraft 1.6~1.20.1) | / | / |
|
||||
| PowerPC-64 (Little-Endian) | / | ❔ | / | / |
|
||||
| S390x | / | ❔ | / | / |
|
||||
| | Windows | Linux | Mac OS | FreeBSD |
|
||||
|----------------------------|:--------------------------------------------------|:--------------------------|:------------------------------------------------------------------------|:-------------------------|
|
||||
| x86-64 | ✅️ | ✅️ | ✅️ | 👌 (Minecraft 1.13~1.21) |
|
||||
| x86 | ✅️ (~1.20.4) | ✅️ (~1.20.4) | / | / |
|
||||
| ARM64 | 👌 (Minecraft 1.8~1.18.2)<br/>✅ (Minecraft 1.19+) | 👌 (Minecraft 1.8~1.20.6) | 👌 (Minecraft 1.6~1.18.2)<br/>✅ (Minecraft 1.19+)<br/>✅ (use Rosetta 2) | ❔ |
|
||||
| ARM32 | /️ | 👌 (Minecraft 1.8~1.20.1) | / | / |
|
||||
| MIPS64el | / | 👌 (Minecraft 1.8~1.20.1) | / | / |
|
||||
| RISC-V 64 | / | 👌 (Minecraft 1.13~1.21) | / | / |
|
||||
| LoongArch64 | / | 👌 (Minecraft 1.6~1.20.1) | / | / |
|
||||
| PowerPC-64 (Little-Endian) | / | ❔ | / | / |
|
||||
| S390x | / | ❔ | / | / |
|
||||
|
||||
Legend:
|
||||
|
||||
|
||||
+11
-11
@@ -2,17 +2,17 @@
|
||||
|
||||
[English](PLATFORM.md) | 简体中文
|
||||
|
||||
| | Windows | Linux | Mac OS | FreeBSD |
|
||||
|------------------|:---------------------------------------------------|:---------------------------|:-----------------------------------------------------------------------|:--------------------------|
|
||||
| x86-64 | ✅️ | ✅️ | ✅️ | 👌(Minecraft 1.13~1.20.4) |
|
||||
| x86 | ✅️ | ✅️ | / | / |
|
||||
| ARM64 | 👌 (Minecraft 1.8~1.18.2)<br/>✅ (Minecraft 1.19+) | 👌 (Minecraft 1.8~1.20.4) | 👌 (Minecraft 1.6~1.18.2)<br/>✅ (Minecraft 1.19+)<br/>✅ (使用 Rosetta 2) | ❔ |
|
||||
| ARM32 | /️ | 👌 (Minecraft 1.6~1.20.1) | / | / |
|
||||
| MIPS64el | / | 👌 (Minecraft 1.6~1.20.1) | / | / |
|
||||
| RISC-V 64 | / | 👌 (Minecraft 1.13~1.20.4) | / | / |
|
||||
| LoongArch64 | / | 👌 (Minecraft 1.6~1.20.1) | / | / |
|
||||
| PowerPC-64 (小端序) | / | ❔ | / | / |
|
||||
| S390x | / | ❔ | / | / |
|
||||
| | Windows | Linux | Mac OS | FreeBSD |
|
||||
|----------------------------|:--------------------------------------------------|:---------------------------|:-----------------------------------------------------------------------|:------------------------|
|
||||
| x86-64 | ✅️ | ✅️ | ✅️ | 👌(Minecraft 1.13~1.21) |
|
||||
| x86 | ✅️ (~1.20.4) | ✅️ (~1.20.4) | / | / |
|
||||
| ARM64 | 👌 (Minecraft 1.8~1.18.2)<br/>✅ (Minecraft 1.19+) | 👌 (Minecraft 1.8~1.21) | 👌 (Minecraft 1.6~1.18.2)<br/>✅ (Minecraft 1.19+)<br/>✅ (使用 Rosetta 2) | ❔ |
|
||||
| ARM32 | /️ | 👌 (Minecraft 1.6~1.20.1) | / | / |
|
||||
| MIPS64el | / | 👌 (Minecraft 1.6~1.20.1) | / | / |
|
||||
| RISC-V 64 | / | 👌 (Minecraft 1.13~1.21) | / | / |
|
||||
| LoongArch64 | / | 👌 (Minecraft 1.6~1.20.1) | / | / |
|
||||
| PowerPC-64 (Little-Endian) | / | ❔ | / | / |
|
||||
| S390x | / | ❔ | / | / |
|
||||
|
||||
图例:
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||

|
||||

|
||||
[](https://discord.gg/jVvC7HfM6U)
|
||||
[](https://kook.top/Kx7n3t)
|
||||
[](https://docs.hmcl.net/groups.html)
|
||||
|
||||
English | [中文](README_cn.md)
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||

|
||||

|
||||
[](https://discord.gg/jVvC7HfM6U)
|
||||
[](https://kook.top/Kx7n3t)
|
||||
[](https://docs.hmcl.net/groups.html)
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
<module name="ThrowsCount"> <!-- max 5 throws definitions per method: http://checkstyle.sourceforge.net/config_design.html#ThrowsCount -->
|
||||
<property name="max" value="5"/>
|
||||
</module>
|
||||
<module name="InterfaceIsType"/> <!-- interface must contain methods, should not be used for const only: http://checkstyle.sourceforge.net/config_design.html#InterfaceIsType -->
|
||||
<!--<module name="InterfaceIsType"/> interface must contain methods, should not be used for const only: http://checkstyle.sourceforge.net/config_design.html#InterfaceIsType -->
|
||||
<module name="OuterTypeFilename"/> <!-- class Foo must be in Foo.java: http://checkstyle.sourceforge.net/config_misc.html#OuterTypeFilename -->
|
||||
|
||||
<module name="HideUtilityClassConstructor"/> <!-- utility class constructor must be private: http://checkstyle.sourceforge.net/config_design.html#HideUtilityClassConstructor -->
|
||||
|
||||
Reference in new issue
Block a user