Compare commits

...
Author SHA1 Message Date
huanghongxun 31568a5806 fix(download): we can check hash of fabric-api jar. 2021-12-20 01:43:41 +08:00
huanghongxun 9aa73f5a5c fix(download): download fabric-api from curseforge. 2021-12-20 01:22:54 +08:00
Glavo 35943feb08 Use GitHub action sync to gitee 2021-12-20 01:21:00 +08:00
huanghongxun 73de21f936 feat(game): select last renamed game version. Closes #1185. 2021-12-19 20:35:15 +08:00
huanghongxun 4f1af87ba0 fix(download): 1.18 released. 2021-12-19 20:31:05 +08:00
huanghongxun d129bd3e6b fix(multiplayer): allow updating cato at server. 2021-12-19 20:30:03 +08:00
Glavo c2c52e8747 fix: Bootstrap launcher version does not need to be displayed on GameCrashWindow 2021-12-19 19:55:00 +08:00
huanghongxun 7e2ab49f89 fix(about): missing JSTUN. Mentioned in #1203. 2021-12-19 19:50:02 +08:00
huanghongxun 3192d41fe3 fix(download): cannot refresh mod download list. Mentioned in #1203. 2021-12-19 19:47:55 +08:00
huanghongxun ad5b260460 fix(download): cannot refresh game list. Mentioned in #1203. 2021-12-19 19:45:01 +08:00
huanghongxun e57cb0ce51 fix(launch): should not patch offline account skin when generating launch script or stopping the launcher immediately after game launched. 2021-12-19 18:45:26 +08:00
Glavo 9514357391 Temporarily disabled adjust process priority on Windows 2021-12-19 17:59:13 +08:00
Glavo 6fd0a40cc6 Delete redundant exception catches 2021-12-19 17:17:40 +08:00
Glavo 350d63231f Improved the logging message of log4j-patch 2021-12-19 17:17:40 +08:00
Glavo cc28f1d5b5 Fix #1241: Use javaagent patch log4j 2021-12-19 17:17:40 +08:00
Glavo 4b73af9250 update check-update.yml 2021-12-19 01:52:48 +08:00
zhixuan 6e0efe521d Change the wrong translation for ja 2021-12-19 01:18:24 +08:00
huanghongxun 041ab30778 fix(multiplayer): incorrect cato option name. 2021-12-18 19:54:51 +08:00
huanghongxun 678be4b92f feat(multiplayer): upgrade cato to 1.2.0 2021-12-18 17:15:18 +08:00
Glavo 74d759a45b Update I18N text 2021-12-18 17:08:16 +08:00
30 changed files with 429 additions and 262 deletions

No files matched your search

+3 -8
View File
@@ -20,18 +20,13 @@ jobs:
wget -O ci.json https://ci.huangyuhui.net/job/HMCL/lastSuccessfulBuild/api/json
export HMCL_EXE_FILE_NAME=`cat ci.json | jq -M -r '.artifacts[] | select(.fileName | endswith(".exe")) | .fileName'`
if [ -z `echo $HMCL_EXE_FILE_NAME | grep "^HMCL-[0-9]\.[0-9]\.[0-9]\{3\}\.exe\$"` ]; then exit 1; fi
if [ -z `echo $HMCL_EXE_FILE_NAME | grep -E "^HMCL-[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?\.exe\$"` ]; then exit 1; fi
export HMCL_BASE_VERSION=${HMCL_EXE_FILE_NAME:5:3}
export HMCL_BUILD_NUMBER=`cat ci.json | jq -M -r '.id'`
export HMCL_VERSION="$HMCL_BASE_VERSION.$HMCL_BUILD_NUMBER"
export HMCL_VERSION=`echo "${HMCL_EXE_FILE_NAME%.exe}" | tail -c +6`
export HMCL_COMMIT_SHA=`cat ci.json | jq -M -r '.actions[] | select(._class == "hudson.plugins.git.util.BuildData") | .lastBuiltRevision.SHA1'`
if [ "${#HMCL_BUILD_NUMBER}" != 3 ]; then exit 1; fi
if [ "${#HMCL_COMMIT_SHA}" != 40 ]; then exit 1; fi
echo "HMCL_BASE_VERSION=$HMCL_BASE_VERSION" >> $GITHUB_ENV
echo "HMCL_BUILD_NUMBER=$HMCL_BUILD_NUMBER" >> $GITHUB_ENV
echo "HMCL_VERSION=$HMCL_VERSION" >> $GITHUB_ENV
echo "HMCL_COMMIT_SHA=$HMCL_COMMIT_SHA" >> $GITHUB_ENV
echo "HMCL_TAG_NAME=v$HMCL_VERSION" >> $GITHUB_ENV
@@ -45,7 +40,7 @@ jobs:
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.jar"
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.jar.sha1"
env:
DOWNLOAD_BASE_URL: https://ci.huangyuhui.net/job/HMCL/${{ env.HMCL_BUILD_NUMBER }}/artifact/HMCL/build/libs
DOWNLOAD_BASE_URL: https://ci.huangyuhui.net/job/HMCL/lastSuccessfulBuild/artifact/HMCL/build/libs
- name: Generate release note
if: ${{ env.continue == 'true' }}
run: |
+20
View File
@@ -0,0 +1,20 @@
name: Sync to Gitee
on:
push
jobs:
run:
runs-on: ubuntu-latest
steps:
- name: Mirror Github to Gitee
uses: Yikun/hub-mirror-action@v1.1
with:
src: github/huanghongxun
dst: gitee/huanghongxun
static_list: 'HMCL'
force_update: true
debug: true
dst_key: ${{ secrets.GITEE_SYNC_BOT_PRIVATE_KEY }}
dst_token: ${{ secrets.GITEE_SYNC_BOT_TOKEN }}
cache_path: /github/workspace/hub-mirror-cache
@@ -328,7 +328,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
vs.setUsesGlobal(true);
}
public LaunchOptions getLaunchOptions(String version, JavaVersion javaVersion, File gameDir) {
public LaunchOptions getLaunchOptions(String version, JavaVersion javaVersion, File gameDir, boolean makeLaunchScript) {
VersionSetting vs = getVersionSetting(version);
LaunchOptions.Builder builder = new LaunchOptions.Builder()
@@ -358,7 +358,8 @@ public class HMCLGameRepository extends DefaultGameRepository {
.setNativesDir(vs.getNativesDir())
.setProcessPriority(vs.getProcessPriority())
.setUseNativeGLFW(vs.isUseNativeGLFW())
.setUseNativeOpenAL(vs.isUseNativeOpenAL());
.setUseNativeOpenAL(vs.isUseNativeOpenAL())
.setDaemon(!makeLaunchScript && vs.getLauncherVisibility().isDaemon());
if (config().hasProxy()) {
builder.setProxy(ProxyManager.getProxy());
if (config().hasProxyAuth()) {
@@ -172,7 +172,7 @@ public final class LauncherHelper {
}
}).withStage("launch.state.logging_in"))
.thenComposeAsync(authInfo -> Task.supplyAsync(() -> {
LaunchOptions launchOptions = repository.getLaunchOptions(selectedVersion, javaVersionRef.get(), profile.getGameDir());
LaunchOptions launchOptions = repository.getLaunchOptions(selectedVersion, javaVersionRef.get(), profile.getGameDir(), scriptFile != null);
return new HMCLGameLauncher(
repository,
version,
@@ -43,5 +43,9 @@ public enum LauncherVisibility {
/**
* Hide the launcher and reopen it when game closes.
*/
HIDE_AND_REOPEN
HIDE_AND_REOPEN;
public boolean isDaemon() {
return this != CLOSE;
}
}
@@ -221,7 +221,7 @@ public class GameCrashWindow extends Stage {
LogWindow logWindow = new LogWindow();
logWindow.logLine("Command: " + new CommandBuilder().addAll(managedProcess.getCommands()).toString(), Log4jLevel.INFO);
logWindow.logLine("ClassPath: " + managedProcess.getClasspath(), Log4jLevel.INFO);
if (managedProcess.getClasspath() != null) logWindow.logLine("ClassPath: " + managedProcess.getClasspath(), Log4jLevel.INFO);
for (Map.Entry<String, Log4jLevel> entry : logs)
logWindow.logLine(entry.getKey(), entry.getValue());
@@ -315,13 +315,15 @@ public class GameCrashWindow extends Stage {
moddedPane.setAlignment(Pos.CENTER_LEFT);
for (LibraryAnalyzer.LibraryType type : LibraryAnalyzer.LibraryType.values()) {
analyzer.getVersion(type).ifPresent(ver -> {
TwoLineListItem item = new TwoLineListItem();
item.getStyleClass().setAll("two-line-item-second-large");
item.setTitle(i18n("install.installer." + type.getPatchId()));
item.setSubtitle(ver);
moddedPane.getChildren().add(item);
});
if (!type.getPatchId().isEmpty()) {
analyzer.getVersion(type).ifPresent(ver -> {
TwoLineListItem item = new TwoLineListItem();
item.getStyleClass().setAll("two-line-item-second-large");
item.setTitle(i18n("install.installer." + type.getPatchId()));
item.setSubtitle(ver);
moddedPane.getChildren().add(item);
});
}
}
}
@@ -167,9 +167,9 @@ public class AboutPage extends StackPane {
jstun.setExternalLink("https://github.com/tking/JSTUN");
IconedTwoLineListItem minecraftJFXSkin = new IconedTwoLineListItem();
jstun.setTitle("minecraft-jfx-skin");
jstun.setSubtitle("Copyright (c) 2016 InfinityStudio.\nLicensed under the GPL 3.");
jstun.setExternalLink("https://github.com/InfinityStudio/minecraft-jfx-skin");
minecraftJFXSkin.setTitle("minecraft-jfx-skin");
minecraftJFXSkin.setSubtitle("Copyright (c) 2016 InfinityStudio.\nLicensed under the GPL 3.");
minecraftJFXSkin.setExternalLink("https://github.com/InfinityStudio/minecraft-jfx-skin");
dep.getContent().setAll(javafx, jfoenix, gson, xz, fxgson, constantPoolScanner, openNBT, jstun, minecraftJFXSkin);
}
@@ -25,9 +25,11 @@ import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.launch.StreamPump;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
import org.jackhuang.hmcl.util.io.HttpRequest;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.CommandBuilder;
@@ -59,56 +61,52 @@ import static org.jackhuang.hmcl.util.Pair.pair;
* Cato Management.
*/
public final class MultiplayerManager {
static final String CATO_VERSION = "1.1.1-3f36a56";
// private static final String CATO_DOWNLOAD_URL = "https://files.huangyuhui.net/maven/cato/cato/" + MultiplayerManager.CATO_VERSION;
private static final String CATO_DOWNLOAD_URL = "https://codechina.csdn.net/huanghongxun1/ioi_bin/-/raw/3f36a567d9381e5fa404b96bcecd6d6a555f235c/client/";
static final String CATO_VERSION = "1.2.0-202112171527";
private static final String CATO_DOWNLOAD_URL = "https://gitcode.net/huanghongxun1/ioi_bin/-/raw/3.5.3/client/";
private static final String CATO_HASH_URL = CATO_DOWNLOAD_URL + "cato-all-files.sha1";
private static final String CATO_PATH = getCatoPath();
public static final int CATO_AGREEMENT_VERSION = 2;
private static final String REMOTE_ADDRESS = "127.0.0.1";
private static final String LOCAL_ADDRESS = "0.0.0.0";
private static final Map<String, String> HASH = mapOf(
pair("cato-client-darwin-amd64", "e733fa4b72be24b99026249c31c4ae8b52ad3906"),
pair("cato-client-darwin-arm64", "19ab41b7f78263cad81009ae1a391b0d0367d95f"),
pair("cato-client-freebsd-amd64", "6de8439ab9267e444d45bb9f02394023ce0bfe5e"),
pair("cato-client-freebsd-arm64", "5cd8930e7cd17d72386b085950e63467e4ce72ef"),
pair("cato-client-freebsd-arm7", "d0e54a4a1b0f8c2f769d74a6a600eec4a9c4e0da"),
pair("cato-client-freebsd-i386", "d9d22faf85e01e77b18f28f1609e87623ac9b1a4"),
pair("cato-client-js.wasm", "acb366a4d6a3c6cd464ef5919ce7d2d58a52964e"),
pair("cato-client-linux-amd64", "04e94a3adff1e4d865dead2c6c5da584b74bff0a"),
pair("cato-client-linux-arm64", "ba662bc461995dae5e78d21b9fea35c2db81b652"),
pair("cato-client-linux-arm7", "28021eff56e1e273c051853808e70e7ab3af80d9"),
pair("cato-client-linux-i386", "4f524a9cfbcea560bdf07d3468b883f946c431ed"),
pair("cato-client-linux-mips", "91c3108b046511c2aafbe88f82b38a06c03d88e1"),
pair("cato-client-linux-mips64", "04d1b66494dbdb0443eaae85bda6b61408e300ed"),
pair("cato-client-linux-mips64le", "d838ac71052f7873b28f4b4a2f23470b918ef85d"),
pair("cato-client-linux-mipsle", "54918ed9cdbe48f7adf11680947281f5fc7c5a7d"),
pair("cato-client-linux-ppc64", "f2679259516b0db0422f85afcce35bfa6f7b3192"),
pair("cato-client-linux-ppc64le", "463d9b86316dfc83e668ddfae96e51181a9e3ffa"),
pair("cato-client-openbsd-amd64", "b776155e955340485faf62c3b2aad3a788c57e2b"),
pair("cato-client-openbsd-arm64", "fd6ec21248fd9c8be3d9d2876faf65b95c63b3a9"),
pair("cato-client-openbsd-arm7", "19eabe71530bb4eac7a32f6b8aeef72f6f9382e9"),
pair("cato-client-openbsd-i386", "3e755798628a379997dbc43dfd71765d62263ebe"),
pair("cato-client-windows-amd64.exe", "21a862f455c909c9fb786cd308e969629eb3c8d9"),
pair("cato-client-windows-arm64.exe", "7f2fe5d054ebc3265edb046badafdd6ac94bf7eb"),
pair("cato-client-windows-i386.exe", "4e03e640c6cabfd7e1ddf60bba1726353bc3afee")
);
private static CompletableFuture<Map<String, String>> HASH;
private MultiplayerManager() {
}
private static CompletableFuture<Map<String, String>> getCatoHash() {
FXUtils.checkFxUserThread();
if (HASH == null) {
HASH = CompletableFuture.supplyAsync(wrap(() -> {
String hashList = HttpRequest.GET(CATO_HASH_URL).getString();
Map<String, String> hashes = new HashMap<>();
for (String line : hashList.split("\n")) {
String[] items = line.trim().split(" ");
if (items.length == 2 && items[0].length() == 40) {
hashes.put(items[1], items[0]);
} else {
LOG.warning("Failed to parse cato hash file, hash line " + line);
}
}
return hashes;
}));
}
return HASH;
}
public static Task<Void> downloadCato() {
return new FileDownloadTask(
NetworkUtils.toURL(CATO_DOWNLOAD_URL + getCatoFileName()),
getCatoExecutable().toFile(),
new FileDownloadTask.IntegrityCheck("SHA-1", HASH.get(getCatoFileName()))
).thenRunAsync(() -> {
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX || OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
Set<PosixFilePermission> perm = Files.getPosixFilePermissions(getCatoExecutable());
perm.add(PosixFilePermission.OWNER_EXECUTE);
Files.setPosixFilePermissions(getCatoExecutable(), perm);
}
});
return Task.fromCompletableFuture(getCatoHash()).thenComposeAsync(catoHashes ->
new FileDownloadTask(
NetworkUtils.toURL(CATO_DOWNLOAD_URL + getCatoFileName()),
getCatoExecutable().toFile(),
catoHashes.get(getCatoFileName()) == null ? null : new FileDownloadTask.IntegrityCheck("SHA-1", catoHashes.get(getCatoFileName()))
).thenRunAsync(() -> {
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX || OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
Set<PosixFilePermission> perm = Files.getPosixFilePermissions(getCatoExecutable());
perm.add(PosixFilePermission.OWNER_EXECUTE);
Files.setPosixFilePermissions(getCatoExecutable(), perm);
}
}));
}
public static Path getCatoExecutable() {
@@ -116,7 +114,7 @@ public final class MultiplayerManager {
}
private static CompletableFuture<CatoSession> startCato(String token, State state) {
return CompletableFuture.completedFuture(null).thenApplyAsync(wrap(unused -> {
return getCatoHash().thenApplyAsync(wrap(catoHashes -> {
Path exe = getCatoExecutable();
if (!Files.isRegularFile(exe)) {
throw new CatoNotExistsException(exe);
@@ -127,13 +125,16 @@ public final class MultiplayerManager {
}
try {
ChecksumMismatchException.verifyChecksum(exe, "SHA-1", HASH.get(getCatoFileName()));
String hash = catoHashes.get(getCatoFileName());
if (hash != null) {
ChecksumMismatchException.verifyChecksum(exe, "SHA-1", hash);
}
} catch (IOException e) {
Files.deleteIfExists(exe);
throw e;
}
String[] commands = new String[]{exe.toString(), "--token", StringUtils.isBlank(token) ? "new" : token};
String[] commands = new String[]{exe.toString(), "-client.token", StringUtils.isBlank(token) ? "new" : token};
Process process = new ProcessBuilder()
.command(commands)
.start();
@@ -88,6 +88,8 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
private TaskExecutor executor;
protected RemoteModRepository repository;
private Runnable retrySearch;
public DownloadListPage(RemoteModRepository repository) {
this(repository, null);
}
@@ -151,6 +153,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
}
public void search(String userGameVersion, RemoteModRepository.Category category, int pageOffset, String searchFilter, int sort) {
retrySearch = null;
setLoading(true);
setFailed(false);
File versionJar = StringUtils.isNotBlank(version.get().getVersion())
@@ -177,6 +180,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
failed.set(false);
} else {
failed.set(true);
retrySearch = () -> search(userGameVersion, category, pageOffset, searchFilter, sort);
}
}).executor(true);
}
@@ -360,6 +364,11 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
return null;
}
}, getSkinnable().failedProperty()));
spinnerPane.setOnFailedAction(e -> {
if (getSkinnable().retrySearch != null) {
getSkinnable().retrySearch.run();
}
});
JFXListView<RemoteMod> listView = new JFXListView<>();
spinnerPane.setContent(listView);
@@ -84,7 +84,12 @@ public class DownloadPage extends Control implements DecoratorPage {
this.mod = ModTranslations.getModByCurseForgeId(addon.getSlug());
this.version = version;
this.callback = callback;
loadModVersions();
this.state.set(State.fromTitle(addon.getTitle()));
}
private void loadModVersions() {
File versionJar = StringUtils.isNotBlank(version.getVersion())
? version.getProfile().getRepository().getVersionJar(version.getVersion())
: null;
@@ -93,9 +98,9 @@ public class DownloadPage extends Control implements DecoratorPage {
setFailed(false);
Task.allOf(
Task.supplyAsync(() -> addon.getData().loadDependencies()),
Task.supplyAsync(() -> {
Stream<RemoteMod.Version> versions = addon.getData().loadVersions();
Task.supplyAsync(() -> addon.getData().loadDependencies()),
Task.supplyAsync(() -> {
Stream<RemoteMod.Version> versions = addon.getData().loadVersions();
// if (StringUtils.isNotBlank(version.getVersion())) {
// Optional<String> gameVersion = GameVersion.minecraftVersion(versionJar);
// if (gameVersion.isPresent()) {
@@ -103,8 +108,8 @@ public class DownloadPage extends Control implements DecoratorPage {
// .filter(file -> file.getGameVersions().contains(gameVersion.get())));
// }
// }
return sortVersions(versions);
}))
return sortVersions(versions);
}))
.whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception == null) {
@SuppressWarnings("unchecked")
@@ -122,8 +127,6 @@ public class DownloadPage extends Control implements DecoratorPage {
}
setLoading(false);
}).start();
this.state.set(State.fromTitle(addon.getTitle()));
}
private SimpleMultimap<String, RemoteMod.Version> sortVersions(Stream<RemoteMod.Version> versions) {
@@ -195,7 +198,7 @@ public class DownloadPage extends Control implements DecoratorPage {
}
Controllers.taskDialog(
new FileDownloadTask(NetworkUtils.toURL(file.getFile().getUrl()), dest).executor(true),
new FileDownloadTask(NetworkUtils.toURL(file.getFile().getUrl()), dest, file.getFile().getIntegrityCheck()).executor(true),
i18n("message.downloading")
);
}
@@ -297,6 +300,7 @@ public class DownloadPage extends Control implements DecoratorPage {
return null;
}
}, getSkinnable().failedProperty()));
spinnerPane.setOnFailedAction(e -> getSkinnable().loadModVersions());
ComponentList list = new ComponentList();
StackPane.setAlignment(list, Pos.TOP_CENTER);
@@ -123,7 +123,12 @@ public final class Versions {
}
if (profile.getRepository().renameVersion(version, newName)) {
resolve.run();
profile.getRepository().refreshVersionsAsync().start();
profile.getRepository().refreshVersionsAsync()
.thenRunAsync(Schedulers.javafx(), () -> {
if (profile.getRepository().hasVersion(newName)) {
profile.setSelectedVersion(newName);
}
}).start();
} else {
reject.accept(i18n("version.manage.rename.fail"));
}
@@ -949,7 +949,7 @@ update.found=Update Available!
update.newest_version=Latest version: %s
update.bubble.title=Update Available: %s
update.bubble.subtitle=Click here to update
update.note=Warning: Beta versions may have more functionality and bug fixes but also more possible errors.
update.note=Warning: Beta versions and nightly versions may have more functionality and bug fixes but also more possible errors.
update.latest=This is the latest version.
update.no_browser=Cannot open a browser. The link has been copied to the clipboard. Paste it into the address bar of browser to update.
update.tooltip=Update
@@ -148,7 +148,7 @@ button.install=インストール
button.export=エクスポート
button.no=いいえ
button.ok=OK
button.refresh=リフレッシュ
button.refresh=更新
button.remove=削除
button.remove.confirm=削除してもよろしいですか?この操作をロールバックすることはできません!
button.save=保存
@@ -182,6 +182,11 @@ curse.category.4472=Tech
curse.category.4473=魔法
# https://addons-ecs.forgesvc.net/api/v2/category/section/6
curse.category.5129=Vanilla+
curse.category.5189=Utility & QOL
curse.category.5190=QoL
curse.category.5191=Utility & QoL
curse.category.5192=FancyMenu
curse.category.423=地図と情報
curse.category.426=Addons
curse.category.434=武器、道具、武器
@@ -289,6 +294,12 @@ download.javafx.notes=ネットワークを介したHMCLに必要なコンポー
download.javafx.component=モジュール%sをダウンロードしています
download.javafx.prepare=ダウンロードする準備ができました
exception.access_denied=It's denied by operating system to access file %s. Maybe we don't have permission to access this file, or this file has already been opened by other program.\n\
Please check if current operating system user has permission to access that file.\n\
For Windows users, you can also try the Resource Monitor, find if some programs is holding the file, try to close related program, or restart your computer.
exception.artifact_malformed=The file cannot pass verification.
exception.ssl_handshake=Cannot establish SSL connection because of missing SSL certifcate in current Java installation. Try to start HMCL on another Java VM.
extension.bat=WindowsBatファイル
extension.mod=Modファイル
extension.png=画像ファイル
@@ -300,7 +311,8 @@ fatal.javafx.missing=JavaFXがありません。\nJava11以降を使用してい
fatal.missing_dst_root_ca_x3=現在のJavaプラットフォームにDSTルートCAX3証明書がありません。\n引き続きHelloMinecraftを使用できます。ランチャーですが、一部のサイト(Let's Encryptによって発行された証明書を使用するサイトなど)に接続できず、ランチャーが正しく機能しない可能性があります。\n問題を解決するには、Javaランタイムを8u101以降に更新してください。
fatal.config_loading_failure=構成にアクセスできません。\nHelloMinecraftを確認してください。 Launcherには、「%s」とその中のファイルへの読み取りおよび書き込みアクセス権があります。
fatal.migration_requires_manual_reboot=更新が完了しました。 Hello Minecraftを再開してください!ランチャー。
fatal.apply_update_failure=ごめんなさい、こんにちはMinecraft!何か問題が発生したため、ランチャーは更新を完了できませんでした。\nただし、Hello Minecraftをダウンロードすることで、手動で更新を終了できます。 %sからのランチャー。\nこの問題を報告することを検討してください。
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.
feedback=フィードバック
Feedback.add=フィードバックを追加
@@ -392,12 +404,11 @@ input.number=数値である必要があります。
input.not_empty=必須フィールド
input.url=有効なURLである必要があります。
install=新しい構成を作成
install=新規作成
install.change_version=バージョンの変更
install.change_version.confirm=%sをバージョン%sから%sに更新してもよろしいですか?
install.failed=バージョンのインストールに失敗しました
install.failed.downloading=一部のファイルが正常にダウンロードされなかったため、インストールに失敗しました
install.failed.downloading.access_denied=ファイルのダウンロードに失敗しました:%1$s。これは、オペレーティングシステムによってファイル%2 $ sへのアクセスが拒否されているためです。このファイルにアクセスする権限がないか、このファイルが他のプログラムによって既に開かれている可能性があります。関連するプログラムを閉じるか、コンピュータを再起動してみてください。
install.failed.downloading.detail=ファイルのダウンロードに失敗しました:%s
install.failed.downloading.timeout=ファイルのダウンロード中にタイムアウトしました:%s
install.failed.install_online=提供されたインストーラーファイルを認識できません。 modをインストールする場合は、「Mods」ページに移動します。
@@ -425,7 +436,7 @@ install.installer.not_installed=インストールされていません
install.installer.optifine=OptiFine
install.installer.version=%s
install.modpack=modpackを導入
install.new_game=新しいゲームをインストールする
install.new_game=新規作成
install.new_game.already_exists=このバージョンはすでに存在します。
install.new_game.current_game_version=現在のゲームバージョン
install.new_game.malformed=無効な名前
@@ -482,6 +493,7 @@ launcher.background.classic=クラシック
launcher.background.choose=背景画像ファイルを選択してください
launcher.background.default=標準
launcher.background.network=ネットワーク
launcher.background.translucent=半透明
launcher.cache_directory=キャッシュ用のディレクトリ
launcher.cache_directory.clean=Clear
launcher.cache_directory.choose=キャッシュするディレクトリを選択します
@@ -641,8 +653,10 @@ multiplayer.download.unsupported=現在のオペレーティングシステム
multiplayer.exit.after_ready=マルチプレイヤーセッションが壊れています。 cato exitcode%d
multiplayer.exit.before_ready=マルチプレイヤーセッションの作成に失敗しました。 cato exitcode%d
multiplayer.exit.timeout=マルチプレイヤーサーバーへの接続に失敗しました。
multiplayer.exit.timeout.dynamic_token=マルチプレイヤーサーバーへの接続に失敗しました。
multiplayer.hint=マルチプレイヤー機能は実験的なものです。フィードバックをお願いします。
multiplayer.nat=ネットワークタイプの検出
multiplayer.nat.failed=ネットワークタイプの検出に失敗しました。
multiplayer.nat.hint=ネットワークタイプの検出により、ネットワークがマルチプレイヤーモードの要件を満たしているかどうかが明確になります。
multiplayer.nat.latency=レイテンシー
multiplayer.nat.not_yet_tested=まだテストされていません
@@ -750,7 +764,7 @@ profile.instance_directory=ゲームディレクトリ
profile.instance_directory.choose=ゲームディレクトリを選択してください
profile.manage=ゲームディレクトリリスト
profile.name=名前
profile.new=新しい構成
profile.new=新規作成
profile.title=ゲームディレクトリ
profile.selected=選択済み
profile.use_relative_path=可能であれば、ゲームディレクトリの相対パスを使用します
@@ -799,7 +813,7 @@ settings.advanced.launcher_visibility.close=ゲームの起動時にランチャ
settings.advanced.launcher_visibility.hide=ゲームの起動時にランチャーを非表示にします。
settings.advanced.launcher_visibility.hide_and_reopen=ランチャーを非表示にし、ゲームが終了したら再度開きます。
settings.advanced.launcher_visibility.keep=ランチャーを表示したままにします。
settings.advanced.launcher_visible=ランチャーの可視性
settings.advanced.launcher_visible=ランチャーの設定
settings.advanced.minecraft_arguments=Minecraftの引数
settings.advanced.minecraft_arguments.prompt=デフォルト
settings.advanced.natives_directory=ネイティブライブラリパス(例: LWJGL)
@@ -883,7 +897,7 @@ settings.memory.allocate.auto=最小%1 $ .1f GB /実際の%2 $ .1f GB
settings.memory.allocate.auto.exceeded=最小%1 $ .1f GB /実際の%2 $ .1f GB(%3 $ .1f GB使用可能)
settings.memory.allocate.manual=割り当て%1 $ .1f GB
settings.memory.allocate.manual.exceeded=割り当て%1 $ .1f GB(%3 $ .1f GB使用可能)
settings.memory.auto_allocate=メモリサイズは自動的に決定されます
settings.memory.auto_allocate=メモリサイズの自動割り当て
settings.memory.lower_bound=最小メモリ
settings.memory.used_per_total=%1 $ .1f GB使用済み/%2 $ .1fGB合計
settings.physical_memory=物理メモリサイズ
@@ -969,9 +983,9 @@ version.manage.rename=このバージョンの名前を変更します
version.manage.rename.message=このバージョンの新しい名前を入力してください
version.manage.rename.fail=バージョンの名前を変更できませんでした。
version.settings=設定
version.update=modpackを更新します
version.update=modpackを更新する
wizard.prev=<前へ
wizard.prev=< 前へ
wizard.failed=失敗
wizard.finish=終了
wizard.next=次へ>
wizard.next=次へ >
@@ -949,7 +949,7 @@ update.found=發現到更新
update.newest_version=最新版本為: %s
update.bubble.title=發現更新: %s
update.bubble.subtitle=點選此處進行升級
update.note=開發版包含更多的功能以及錯誤修復,但也可能會包含其他的問題。
update.note=測試版與開發版包含更多的功能以及錯誤修復,但也可能會包含其他的問題。
update.latest=目前版本為最新版本
update.no_browser=無法開啟瀏覽器,網址已經複製到剪貼簿了,您可以手動複製網址開啟頁面
update.tooltip=更新
@@ -949,7 +949,7 @@ update.found=发现更新
update.newest_version=最新版本为:%s
update.bubble.title=发现更新:%s
update.bubble.subtitle=点击此处进行升级
update.note=开发版包含更多的功能以及错误修复,但也可能会包含其他的问题。
update.note=测试版与开发版包含更多的功能以及错误修复,但也可能会包含其他的问题。
update.latest=当前版本为最新版本
update.no_browser=无法打开浏览器,网址已经复制到剪贴板了,您可以手动粘贴网址打开页面
update.tooltip=更新
@@ -18,8 +18,10 @@
package org.jackhuang.hmcl.auth;
import org.jackhuang.hmcl.game.Arguments;
import org.jackhuang.hmcl.game.LaunchOptions;
import org.jackhuang.hmcl.util.Immutable;
import java.io.IOException;
import java.util.UUID;
/**
@@ -27,26 +29,18 @@ import java.util.UUID;
* @author huangyuhui
*/
@Immutable
public final class AuthInfo implements AutoCloseable {
public class AuthInfo implements AutoCloseable {
private final String username;
private final UUID uuid;
private final String accessToken;
private final String userProperties;
private final Arguments arguments;
private final AutoCloseable closeable;
public AuthInfo(String username, UUID uuid, String accessToken, String userProperties) {
this(username, uuid, accessToken, userProperties, null, null);
}
public AuthInfo(String username, UUID uuid, String accessToken, String userProperties, Arguments arguments, AutoCloseable closeable) {
this.username = username;
this.uuid = uuid;
this.accessToken = accessToken;
this.userProperties = userProperties;
this.arguments = arguments;
this.closeable = closeable;
}
public String getUsername() {
@@ -72,24 +66,14 @@ public final class AuthInfo implements AutoCloseable {
}
/**
* Called when launching game.
* @return null if no argument is specified
*/
public Arguments getArguments() {
return arguments;
}
public AuthInfo withArguments(Arguments arguments) {
return new AuthInfo(username, uuid, accessToken, userProperties, arguments, closeable);
}
public AuthInfo withCloseable(AutoCloseable closeable) {
return new AuthInfo(username, uuid, accessToken, userProperties, arguments, closeable);
public Arguments getLaunchArguments(LaunchOptions options) throws IOException {
return null;
}
@Override
public void close() throws Exception {
if (closeable != null) {
closeable.close();
}
}
}
@@ -26,6 +26,7 @@ import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilSession;
import org.jackhuang.hmcl.game.Arguments;
import org.jackhuang.hmcl.game.LaunchOptions;
import org.jackhuang.hmcl.util.ToStringBuilder;
import org.jackhuang.hmcl.util.function.ExceptionalSupplier;
import java.io.IOException;
@@ -71,7 +72,7 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
Optional<String> prefetchedMeta = server.getMetadataResponse();
if (auth.isPresent() && artifact.isPresent() && prefetchedMeta.isPresent()) {
return Optional.of(auth.get().withArguments(generateArguments(artifact.get(), server, prefetchedMeta.get())));
return Optional.of(new AuthlibInjectorAuthInfo(auth.get(), artifact.get(), server, prefetchedMeta.get()));
} else {
return Optional.empty();
}
@@ -112,14 +113,30 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
}
}
return auth.withArguments(generateArguments(artifact, server, prefetchedMeta));
return new AuthlibInjectorAuthInfo(auth, artifact, server, prefetchedMeta);
}
private static Arguments generateArguments(AuthlibInjectorArtifactInfo artifact, AuthlibInjectorServer server, String prefetchedMeta) {
return new Arguments().addJVMArguments(
"-javaagent:" + artifact.getLocation().toString() + "=" + server.getUrl(),
"-Dauthlibinjector.side=client",
"-Dauthlibinjector.yggdrasil.prefetched=" + Base64.getEncoder().encodeToString(prefetchedMeta.getBytes(UTF_8)));
private static class AuthlibInjectorAuthInfo extends AuthInfo {
private final AuthlibInjectorArtifactInfo artifact;
private final AuthlibInjectorServer server;
private final String prefetchedMeta;
public AuthlibInjectorAuthInfo(AuthInfo authInfo, AuthlibInjectorArtifactInfo artifact, AuthlibInjectorServer server, String prefetchedMeta) {
super(authInfo.getUsername(), authInfo.getUUID(), authInfo.getAccessToken(), authInfo.getUserProperties());
this.artifact = artifact;
this.server = server;
this.prefetchedMeta = prefetchedMeta;
}
@Override
public Arguments getLaunchArguments(LaunchOptions options) {
return new Arguments().addJVMArguments(
"-javaagent:" + artifact.getLocation().toString() + "=" + server.getUrl(),
"-Dauthlibinjector.side=client",
"-Dauthlibinjector.yggdrasil.prefetched=" + Base64.getEncoder().encodeToString(prefetchedMeta.getBytes(UTF_8)));
}
}
@Override
@@ -28,6 +28,7 @@ import org.jackhuang.hmcl.auth.yggdrasil.Texture;
import org.jackhuang.hmcl.auth.yggdrasil.TextureModel;
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
import org.jackhuang.hmcl.game.Arguments;
import org.jackhuang.hmcl.game.LaunchOptions;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.ToStringBuilder;
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
@@ -129,15 +130,7 @@ public class OfflineAccount extends Account {
}
try {
YggdrasilServer server = new YggdrasilServer(0);
server.start();
server.addCharacter(new YggdrasilServer.Character(uuid, username, skin.load(username).run()));
return authInfo.withArguments(new Arguments().addJVMArguments(
"-javaagent:" + artifact.getLocation().toString() + "=" + "http://localhost:" + server.getListeningPort(),
"-Dauthlibinjector.side=client"
))
.withCloseable(server::stop);
return new OfflineAuthInfo(authInfo, artifact);
} catch (Exception e) {
throw new AuthenticationException(e);
}
@@ -146,6 +139,46 @@ public class OfflineAccount extends Account {
}
}
private class OfflineAuthInfo extends AuthInfo {
private final AuthlibInjectorArtifactInfo artifact;
private YggdrasilServer server;
public OfflineAuthInfo(AuthInfo authInfo, AuthlibInjectorArtifactInfo artifact) {
super(authInfo.getUsername(), authInfo.getUUID(), authInfo.getAccessToken(), authInfo.getUserProperties());
this.artifact = artifact;
}
@Override
public Arguments getLaunchArguments(LaunchOptions options) throws IOException {
if (!options.isDaemon()) return null;
server = new YggdrasilServer(0);
server.start();
try {
server.addCharacter(new YggdrasilServer.Character(uuid, username, skin.load(username).run()));
} catch (IOException e) {
throw e;
} catch (Exception e) {
throw new IOException(e);
}
return new Arguments().addJVMArguments(
"-javaagent:" + artifact.getLocation().toString() + "=" + "http://localhost:" + server.getListeningPort(),
"-Dauthlibinjector.side=client"
);
}
@Override
public void close() throws Exception {
super.close();
if (server != null)
server.stop();
}
}
@Override
public Optional<AuthInfo> playOffline() throws AuthenticationException {
return Optional.of(logIn());
@@ -76,43 +76,17 @@ public class MaintainTask extends Task<Version> {
version = maintainOptiFineLibrary(repository, unique(version), false);
}
Library log4jPatch = null;
List<Library> libraries = version.getLibraries();
for (Library library : libraries) {
if (library.is("org.apache.logging.log4j", "log4j-core")) {
if (library.getVersion().startsWith("2.0-beta")) {
if ("2.0-beta9".equals(library.getVersion())) {
log4jPatch = new Library(new Artifact("org.glavo", "log4j-patch-beta9", "1.0"));
} else {
Logging.LOG.warning("Log4j " + library.getVersion() + " cannot be patched");
}
} else if (VersionNumber.VERSION_COMPARATOR.compare(library.getVersion(), "2.16") < 0) {
log4jPatch = new Library(new Artifact("org.glavo", "log4j-patch", "1.0"));
}
break;
if (libraries.size() > 0) {
Library library = libraries.get(0);
if ("org.glavo".equals(library.getGroupId())
&& ("log4j-patch".equals(library.getArtifactId()) || "log4j-patch-beta9".equals(library.getArtifactId()))
&& "1.0".equals(library.getVersion())
&& library.getDownload() == null) {
version = version.setLibraries(libraries.subList(1, libraries.size()));
}
}
if (log4jPatch != null) {
ArrayList<Library> patchedLibraries = new ArrayList<>(libraries.size() + 1);
patchedLibraries.add(log4jPatch);
patchedLibraries.addAll(libraries);
version = version.setLibraries(patchedLibraries);
Path log4jPatchPath = repository.getLibraryFile(version, log4jPatch).toPath();
String patchName = log4jPatch.getArtifactId() + "-" + log4jPatch.getVersion();
if (Files.notExists(log4jPatchPath)) {
try (InputStream input = MaintainTask.class.getResourceAsStream("/assets/game/" + patchName + ".jar")) {
Files.createDirectories(log4jPatchPath.getParent());
Files.copy(input, log4jPatchPath, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
Logging.LOG.log(Level.WARNING, "Unable to unpack " + patchName, e);
}
}
Logging.LOG.info("Apply patch " + patchName + " to log4j");
}
return version;
}
@@ -20,13 +20,11 @@ package org.jackhuang.hmcl.download.fabric;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Task;
import java.io.IOException;
import java.net.URL;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
@@ -37,7 +35,6 @@ import java.util.List;
*/
public final class FabricAPIInstallTask extends Task<Version> {
private GetTask dependent;
private final DefaultDependencyManager dependencyManager;
private final Version version;
private final FabricAPIRemoteVersion remote;
@@ -47,22 +44,6 @@ public final class FabricAPIInstallTask extends Task<Version> {
this.dependencyManager = dependencyManager;
this.version = version;
this.remote = remoteVersion;
}
@Override
public boolean doPreExecute() {
return true;
}
@Override
public void preExecute() throws Exception {
this.dependent = new GetTask(new URL(remote.getUrls().get(0) + ".sha1"));
}
@Override
public Collection<? extends Task<?>> getDependents() {
return Collections.singleton(dependent);
}
@Override
@@ -78,9 +59,9 @@ public final class FabricAPIInstallTask extends Task<Version> {
@Override
public void execute() throws IOException {
dependencies.add(new FileDownloadTask(
new URL(remote.getUrls().get(0)),
dependencyManager.getGameRepository().getRunDirectory(version.getId()).toPath().resolve("mods").resolve("fabric-api-" + remote.getFullVersion() + ".jar").toFile(),
new FileDownloadTask.IntegrityCheck("SHA-1", dependent.getResult().trim()))
new URL(remote.getVersion().getFile().getUrl()),
dependencyManager.getGameRepository().getRunDirectory(version.getId()).toPath().resolve("mods").resolve("fabric-api-" + remote.getVersion().getVersion() + ".jar").toFile(),
remote.getVersion().getFile().getIntegrityCheck())
);
}
}
@@ -19,12 +19,15 @@ package org.jackhuang.hmcl.download.fabric;
import org.jackhuang.hmcl.download.*;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.mod.RemoteMod;
import org.jackhuang.hmcl.task.Task;
import java.time.Instant;
import java.util.List;
public class FabricAPIRemoteVersion extends RemoteVersion {
private final String fullVersion;
private final RemoteMod.Version version;
/**
* Constructor.
@@ -33,10 +36,11 @@ public class FabricAPIRemoteVersion extends RemoteVersion {
* @param selfVersion the version string of the remote version.
* @param urls the installer or universal jar original URL.
*/
FabricAPIRemoteVersion(String gameVersion, String selfVersion, String fullVersion, List<String> urls) {
super(LibraryAnalyzer.LibraryType.FABRIC_API.getPatchId(), gameVersion, selfVersion, null, urls);
FabricAPIRemoteVersion(String gameVersion, String selfVersion, String fullVersion, Instant datePublished, RemoteMod.Version version, List<String> urls) {
super(LibraryAnalyzer.LibraryType.FABRIC_API.getPatchId(), gameVersion, selfVersion, datePublished, urls);
this.fullVersion = fullVersion;
this.version = version;
}
@Override
@@ -44,9 +48,18 @@ public class FabricAPIRemoteVersion extends RemoteVersion {
return fullVersion;
}
public RemoteMod.Version getVersion() {
return version;
}
@Override
public Task<Version> getInstallTask(DefaultDependencyManager dependencyManager, Version baseVersion) {
return new FabricAPIInstallTask(dependencyManager, baseVersion, this);
}
@Override
public int compareTo(RemoteVersion o) {
if (!(o instanceof FabricAPIRemoteVersion)) return 0;
return -this.getReleaseDate().compareTo(o.getReleaseDate());
}
}
@@ -19,12 +19,10 @@ package org.jackhuang.hmcl.download.fabric;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.VersionList;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.jackhuang.hmcl.mod.RemoteMod;
import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository;
import org.jackhuang.hmcl.util.Lang;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.CompletableFuture;
@@ -49,24 +47,10 @@ public class FabricAPIVersionList extends VersionList<FabricAPIRemoteVersion> {
@Override
public CompletableFuture<?> refreshAsync() {
return CompletableFuture.runAsync(wrap(() -> {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse("https://maven.fabricmc.net/net/fabricmc/fabric-api/fabric-api/maven-metadata.xml");
Element r = doc.getDocumentElement();
NodeList versionElements = r.getElementsByTagName("version");
for (int i = 0; i < versionElements.getLength(); i++) {
String versionName = versionElements.item(i).getTextContent();
Matcher matcher = FABRIC_VERSION_PATTERN.matcher(versionName);
if (matcher.find()) {
String fabricVersion = matcher.group("version");
if (matcher.group("build") != null) {
fabricVersion += "." + matcher.group("build");
}
String gameVersion = matcher.group("mcversion");
versions.put(gameVersion, new FabricAPIRemoteVersion(gameVersion, fabricVersion, versionName,
Collections.singletonList(String.format(
"https://maven.fabricmc.net/net/fabricmc/fabric-api/fabric-api/%1$s/fabric-api-%1$s.jar", versionName))));
for (RemoteMod.Version modVersion : Lang.toIterable(ModrinthRemoteModRepository.INSTANCE.getRemoteVersionsById("P7dR8mSH"))) {
for (String gameVersion : modVersion.getGameVersions()) {
versions.put(gameVersion, new FabricAPIRemoteVersion(gameVersion, modVersion.getVersion(), modVersion.getName(), modVersion.getDatePublished(), modVersion,
Collections.singletonList(modVersion.getFile().getUrl())));
}
}
}));
@@ -59,6 +59,7 @@ public class LaunchOptions implements Serializable {
private ProcessPriority processPriority = ProcessPriority.NORMAL;
private boolean useNativeGLFW;
private boolean useNativeOpenAL;
private boolean daemon;
/**
* The game directory
@@ -243,6 +244,13 @@ public class LaunchOptions implements Serializable {
return useNativeOpenAL;
}
/**
* Will launcher keeps alive after game launched or not.
*/
public boolean isDaemon() {
return daemon;
}
public static class Builder {
private final LaunchOptions options = new LaunchOptions();
@@ -411,6 +419,10 @@ public class LaunchOptions implements Serializable {
return options.useNativeOpenAL;
}
public boolean isDaemon() {
return options.daemon;
}
public Builder setGameDir(File gameDir) {
options.gameDir = gameDir;
return this;
@@ -543,5 +555,10 @@ public class LaunchOptions implements Serializable {
return this;
}
public Builder setDaemon(boolean daemon) {
options.daemon = daemon;
return this;
}
}
}
@@ -19,13 +19,7 @@ package org.jackhuang.hmcl.launch;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.game.Argument;
import org.jackhuang.hmcl.game.Arguments;
import org.jackhuang.hmcl.game.GameRepository;
import org.jackhuang.hmcl.game.LaunchOptions;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.NativesDirectoryType;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Log4jLevel;
import org.jackhuang.hmcl.util.StringUtils;
@@ -48,6 +42,7 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.*;
import java.util.function.Supplier;
import java.util.logging.Level;
@@ -79,14 +74,14 @@ public class DefaultLauncher extends Launcher {
switch (options.getProcessPriority()) {
case HIGH:
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
res.add("cmd", "/C", "start", "unused title", "/B", "/high");
// res.add("cmd", "/C", "start", "unused title", "/B", "/high");
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX || OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
res.add("nice", "-n", "-5");
}
break;
case ABOVE_NORMAL:
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
res.add("cmd", "/C", "start", "unused title", "/B", "/abovenormal");
// res.add("cmd", "/C", "start", "unused title", "/B", "/abovenormal");
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX || OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
res.add("nice", "-n", "-1");
}
@@ -96,14 +91,14 @@ public class DefaultLauncher extends Launcher {
break;
case BELOW_NORMAL:
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
res.add("cmd", "/C", "start", "unused title", "/B", "/belownormal");
// res.add("cmd", "/C", "start", "unused title", "/B", "/belownormal");
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX || OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
res.add("nice", "-n", "1");
}
break;
case LOW:
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
res.add("cmd", "/C", "start", "unused title", "/B", "/low");
// res.add("cmd", "/C", "start", "unused title", "/B", "/low");
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX || OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
res.add("nice", "-n", "5");
}
@@ -131,6 +126,8 @@ public class DefaultLauncher extends Launcher {
}
}
Optional<Library> log4j = findLog4j();
// JVM Args
if (!options.isNoGeneratedJVMArgs()) {
appendJvmArgs(res);
@@ -195,6 +192,13 @@ public class DefaultLauncher extends Launcher {
res.addDefault("-Dfml.ignoreInvalidMinecraftCertificates=", "true");
res.addDefault("-Dfml.ignorePatchDiscrepancies=", "true");
if (log4j.isPresent()) {
String enableJndi = res.addDefault("-Dlog4j2.enableJndi=", "false");
if (!"-Dlog4j2.enableJndi=true".equals(enableJndi)) {
res.add("-javaagent:" + repository.getLibraryFile(version, LOG4J_PATCH_AGENT).getAbsolutePath() + "=" + log4j.get().getVersion().startsWith("2.0-beta"));
}
}
}
// Fix RCE vulnerability of log4j2
@@ -203,7 +207,7 @@ public class DefaultLauncher extends Launcher {
res.addDefault("-Dcom.sun.jndi.cosnaming.object.trustURLCodebase=", "false");
String formatMsgNoLookups = res.addDefault("-Dlog4j2.formatMsgNoLookups=", "true");
if (!"-Dlog4j2.formatMsgNoLookups=false".equals(formatMsgNoLookups) && isUsingLog4j()) {
if (!"-Dlog4j2.formatMsgNoLookups=false".equals(formatMsgNoLookups) && log4j.isPresent()) {
res.addDefault("-Dlog4j.configurationFile=", getLog4jConfigurationFile().getAbsolutePath());
}
@@ -251,8 +255,9 @@ public class DefaultLauncher extends Launcher {
configuration.put("${natives_directory}", nativeFolderPath);
res.addAll(Arguments.parseArguments(version.getArguments().map(Arguments::getJvm).orElseGet(this::getDefaultJVMArguments), configuration));
if (authInfo.getArguments() != null && authInfo.getArguments().getJvm() != null && !authInfo.getArguments().getJvm().isEmpty())
res.addAll(Arguments.parseArguments(authInfo.getArguments().getJvm(), configuration));
Arguments argumentsFromAuthInfo = authInfo.getLaunchArguments(options);
if (argumentsFromAuthInfo != null && argumentsFromAuthInfo.getJvm() != null && !argumentsFromAuthInfo.getJvm().isEmpty())
res.addAll(Arguments.parseArguments(argumentsFromAuthInfo.getJvm(), configuration));
res.add(version.getMainClass());
@@ -263,8 +268,8 @@ public class DefaultLauncher extends Launcher {
if (version.getMinecraftArguments().isPresent()) {
res.addAll(Arguments.parseArguments(this.getDefaultGameArguments(), configuration, features));
}
if (authInfo.getArguments() != null && authInfo.getArguments().getGame() != null && !authInfo.getArguments().getGame().isEmpty())
res.addAll(Arguments.parseArguments(authInfo.getArguments().getGame(), configuration, features));
if (argumentsFromAuthInfo != null && argumentsFromAuthInfo.getGame() != null && !argumentsFromAuthInfo.getGame().isEmpty())
res.addAll(Arguments.parseArguments(argumentsFromAuthInfo.getGame(), configuration, features));
if (StringUtils.isNotBlank(options.getServerIp())) {
String[] args = options.getServerIp().split(":");
@@ -360,8 +365,17 @@ public class DefaultLauncher extends Launcher {
}
}
private boolean isUsingLog4j() {
return VersionNumber.VERSION_COMPARATOR.compare(repository.getGameVersion(version).orElse("Unknown"), "1.7") >= 0;
private static final Library LOG4J_PATCH_AGENT = new Library(new Artifact("org.glavo", "log4j-patch-agent", "1.0"));
private Optional<Library> findLog4j() {
return version.getLibraries().stream()
.filter(it -> it.is("org.apache.logging.log4j", "log4j-core")
&& (VersionNumber.VERSION_COMPARATOR.compare(it.getVersion(), "2.17") < 0 || "2.0-beta9".equals(it.getVersion())))
.findFirst();
}
private boolean isLog4jUnsafe(Library log4j) {
return VersionNumber.VERSION_COMPARATOR.compare(log4j.getVersion(), "2.17") < 0;
}
public File getLog4jConfigurationFile() {
@@ -371,18 +385,28 @@ public class DefaultLauncher extends Launcher {
public void extractLog4jConfigurationFile() throws IOException {
File targetFile = getLog4jConfigurationFile();
InputStream source;
if (VersionNumber.VERSION_COMPARATOR.compare(repository.getGameVersion(version).orElse("Unknown"), "1.12") < 0) {
if (VersionNumber.VERSION_COMPARATOR.compare(repository.getGameVersion(version).orElse("0.0"), "1.12") < 0) {
source = DefaultLauncher.class.getResourceAsStream("/assets/game/log4j2-1.7.xml");
} else {
source = DefaultLauncher.class.getResourceAsStream("/assets/game/log4j2-1.12.xml");
}
try (InputStream input = source;
OutputStream output = new FileOutputStream(targetFile)) {
try (InputStream input = source; OutputStream output = new FileOutputStream(targetFile)) {
IOUtils.copyTo(input, output);
}
}
public void extractLog4jAgent() throws IOException {
Path log4jPatchPath = repository.getLibraryFile(version, LOG4J_PATCH_AGENT).toPath();
String patchName = LOG4J_PATCH_AGENT.getArtifactId() + "-" + LOG4J_PATCH_AGENT.getVersion();
if (Files.notExists(log4jPatchPath)) {
try (InputStream input = DefaultLauncher.class.getResourceAsStream("/assets/game/" + patchName + ".jar")) {
Files.createDirectories(log4jPatchPath.getParent());
Files.copy(input, log4jPatchPath, StandardCopyOption.REPLACE_EXISTING);
}
}
}
protected Map<String, String> getConfigurations() {
return mapOf(
// defined by Minecraft official launcher
@@ -427,15 +451,7 @@ public class DefaultLauncher extends Launcher {
final Command command = generateCommandLine(nativeFolder);
// To guarantee that when failed to generate launch command line, we will not call pre-launch command
List<String> rawCommandLine = command.commandLine.asMutableList();
// Pass classpath using the environment variable, to reduce the command length
String classpath = null;
final int cpIndex = rawCommandLine.indexOf("-cp");
if (cpIndex >= 0 && cpIndex < rawCommandLine.size() - 1) {
rawCommandLine.remove(cpIndex); // remove "-cp"
classpath = rawCommandLine.remove(cpIndex);
}
List<String> rawCommandLine = command.commandLine.asList();
if (command.tempNativeFolder != null) {
Files.deleteIfExists(command.tempNativeFolder);
@@ -450,8 +466,11 @@ public class DefaultLauncher extends Launcher {
decompressNatives(nativeFolder);
}
if (isUsingLog4j()) {
Optional<Library> log4j = findLog4j();
if (log4j.isPresent()) {
extractLog4jConfigurationFile();
if (isLog4jUnsafe(log4j.get()))
extractLog4jAgent();
}
File runDirectory = repository.getRunDirectory(version.getId());
@@ -470,19 +489,14 @@ public class DefaultLauncher extends Launcher {
}
String appdata = options.getGameDir().getAbsoluteFile().getParent();
if (appdata != null) builder.environment().put("APPDATA", appdata);
if (classpath != null) {
builder.environment().put("CLASSPATH", classpath);
// Fix #1153: On Windows, the 'classpath' environment variable in the context overrides the 'CLASSPATH'
// Environment variables on Windows are not case-sensitive; The lowercase 'classpath' overwrites any other case.
builder.environment().put("classpath", classpath);
}
builder.environment().putAll(getEnvVars());
process = builder.start();
} catch (IOException e) {
throw new ProcessCreationException(e);
}
ManagedProcess p = new ManagedProcess(process, rawCommandLine, classpath);
ManagedProcess p = new ManagedProcess(process, rawCommandLine);
if (listener != null)
startMonitors(p, listener, command.encoding, daemon);
return p;
@@ -528,8 +542,11 @@ public class DefaultLauncher extends Launcher {
decompressNatives(nativeFolder);
}
if (isUsingLog4j()) {
Optional<Library> log4j = findLog4j();
if (log4j.isPresent()) {
extractLog4jConfigurationFile();
if (isLog4jUnsafe(log4j.get()))
extractLog4jAgent();
}
String scriptExtension = FileUtils.getExtension(scriptFile);
@@ -17,6 +17,8 @@
*/
package org.jackhuang.hmcl.mod;
import org.jackhuang.hmcl.task.FileDownloadTask;
import java.io.IOException;
import java.time.Instant;
import java.util.List;
@@ -184,6 +186,18 @@ public class RemoteMod {
return hashes;
}
public FileDownloadTask.IntegrityCheck getIntegrityCheck() {
if (hashes.containsKey("md5")) {
return new FileDownloadTask.IntegrityCheck("MD5", hashes.get("sha1"));
} else if (hashes.containsKey("sha1")) {
return new FileDownloadTask.IntegrityCheck("SHA-1", hashes.get("sha1"));
} else if (hashes.containsKey("sha512")) {
return new FileDownloadTask.IntegrityCheck("SHA-256", hashes.get("sha1"));
} else {
return null;
}
}
public String getUrl() {
return url;
}
@@ -61,6 +61,7 @@ public interface RemoteModRepository {
}
String[] DEFAULT_GAME_VERSIONS = new String[]{
"1.18.1", "1.18",
"1.17.1", "1.17",
"1.16.5", "1.16.4", "1.16.3", "1.16.2", "1.16.1", "1.16",
"1.15.2", "1.15.1", "1.15",
+20 -14
View File
@@ -1,15 +1,21 @@
version '1.0'
sourceSets.create("agent") {
java {
srcDir 'src/main/agent'
}
}
dependencies {
compileOnly group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.0-beta9'
}
tasks.compileJava {
tasks.withType(JavaCompile) {
sourceCompatibility = 8
targetCompatibility = 8
doLast {
FileTree tree = fileTree('build/classes/java/main')
FileTree tree = fileTree('build/classes/java')
tree.include '**/*.class'
tree.each {
RandomAccessFile rf = new RandomAccessFile(it, "rw")
@@ -20,27 +26,27 @@ tasks.compileJava {
}
}
task patchJar(type: Jar) {
task agentJar(type: Jar) {
dependsOn(tasks.compileJava)
baseName = 'log4j-patch'
baseName = 'log4j-patch-agent'
from(sourceSets.main.output) {
include('**/JndiLookup.class')
manifest {
attributes 'Premain-Class': 'org.glavo.log4j.patch.agent.Log4jAgent'
}
}
task patchBeta9Jar(type: Jar) {
dependsOn(tasks.compileJava)
baseName = 'log4j-patch-beta9'
from(sourceSets.agent.output)
from(sourceSets.main.output) {
include '**/Interpolator.class'
includeEmptyDirs = false
eachFile {
it.path = "org/glavo/log4j/patch/agent/${it.name}.bin"
}
}
}
tasks.jar {
enabled = false
dependsOn patchJar, patchBeta9Jar
dependsOn agentJar
}
@@ -0,0 +1,71 @@
package org.glavo.log4j.patch.agent;
import java.io.InputStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.ProtectionDomain;
import java.util.Arrays;
public final class Log4jAgent {
static final String JNDI_LOOKUP_CLASS_NAME = "org/apache/logging/log4j/core/lookup/JndiLookup";
static final String INTERPOLATOR_CLASS_NAME = "org/apache/logging/log4j/core/lookup/Interpolator";
static final byte[] INTERPOLATOR_CLASS_SHA = {53, 103, 16, 123, 51, 29, 65, -70, -32, 71, -11, 7, 114, -15, 72, 127, 40, -38, 35, 18};
static boolean isBeta = false;
private static byte[] loadResource(String name) {
try {
try (InputStream input = Log4jAgent.class.getResourceAsStream(name)) {
if (input == null) {
throw new AssertionError(name + " not found");
}
int available = input.available();
if (available <= 0) {
throw new AssertionError();
}
byte[] res = new byte[available];
if (input.read(res) != available) {
throw new AssertionError();
}
return res;
}
} catch (Exception ex) {
throw new InternalError(ex);
}
}
public static void premain(String agentArgs, Instrumentation inst) throws Exception {
if ("true".equals(agentArgs)) {
isBeta = true;
}
inst.addTransformer(new Transformer());
}
private static final class Transformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if (!isBeta && JNDI_LOOKUP_CLASS_NAME.equals(className)) {
return loadResource("JndiLookup.class.bin");
}
if (isBeta && INTERPOLATOR_CLASS_NAME.equals(className)) {
try {
MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
sha1.update(classfileBuffer);
if (Arrays.equals(INTERPOLATOR_CLASS_SHA, sha1.digest())) {
return loadResource("Interpolator.class.bin");
}
} catch (NoSuchAlgorithmException e) {
throw new InternalError(e);
}
}
return null;
}
}
}
@@ -65,7 +65,7 @@ public class Interpolator implements StrLookup {
lookups.put("sys", new SystemPropertiesLookup());
lookups.put("env", new EnvironmentLookup());
// lookups.put("jndi", new JndiLookup());
LOGGER.warn("log4j-patch works, JNDI lookup is disabled");
LOGGER.warn("Log4j patch works, JNDI lookup is disabled. This message is not a warning, it represents the successful elimination of some security risks. You should be able to play safely.");
try {
if (Class.forName("javax.servlet.ServletContext") != null) {
@@ -8,6 +8,6 @@ package org.apache.logging.log4j.core.lookup;
public class JndiLookup {
public JndiLookup() {
throw new NoClassDefFoundError("JNDI lookup is disabled");
throw new NoClassDefFoundError("JNDI lookup is disabled. This is not an error. We are blocking some vulnerabilities through it. You should be able to play safely.");
}
}