Compare commits

..
52 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
huanghongxun 295499c17b fix: missing translations. 2021-12-18 16:55:33 +08:00
Glavo 043d1c88c8 Fix #1096: Feedback item cannot be clicked 2021-12-18 16:49:42 +08:00
bling-yshs 55e9b93b1c fix curse category 2021-12-18 16:49:23 +08:00
Glavo aa5e3e9697 Fix #1148: Specify console encoding explicitly 2021-12-18 15:59:24 +08:00
Glavo fbf7ff6ebd Lifting GB2312 to GB18030 2021-12-18 15:59:24 +08:00
Glavo 3a5e8a827c update README 2021-12-18 15:59:24 +08:00
Glavo 6176dfaaf7 Lifting encoding to be compatible with more Unicode characters 2021-12-18 15:59:24 +08:00
Glavo 76c361e9bc Fix #1201: Download version jar if the jar is empty 2021-12-18 15:58:33 +08:00
Glavo ccb8981634 Remove invalid Appender 2021-12-18 15:47:41 +08:00
Glavo 99b55d03c1 Allows user to enable message lookup 2021-12-18 15:47:41 +08:00
Glavo 651fa6e2d1 fix #663: Incorrect judgment of WebView loading status 2021-12-18 15:46:24 +08:00
Glavo 4b258da0a0 Fix code format 2021-12-18 15:45:35 +08:00
Glavo 9a66605616 Disable JndiLookup using log4j-patch 2021-12-18 15:45:35 +08:00
Glavo da13895b72 fix: InputStream not closed 2021-12-18 15:45:35 +08:00
Glavo c657a1d2cc Introducing log4j-patch 2021-12-18 15:45:35 +08:00
Glavo eac8896f3e fix NPE in MultiplayerManager 2021-12-18 15:39:57 +08:00
Gamom 113f4f948b Update I18N_zh_CN.properties
翻译问题
解决 https://github.com/huanghongxun/HMCL/issues/1203#issuecomment-997155654
2021-12-18 15:39:26 +08:00
huanghongxun 1b8dc1f75e fix(mcbbs): NPE when fileApi is not empty. Closes #1197. 2021-12-18 01:40:25 +08:00
huanghongxun 001236e8e6 feat(download): refresh button 2021-12-18 01:40:25 +08:00
Glavo 8025b8e24b Support offline installation of OptiFine 1.17.1 H1 and later 2021-12-18 01:26:03 +08:00
Glavo e48bf012a6 Safely save config 2021-12-18 01:21:45 +08:00
Glavo 94e32694b2 fix: Game should use global settings by default 2021-12-12 16:51:01 +08:00
Glavo cbcf033b15 Delete unnecessary properties 2021-12-11 23:52:03 +08:00
Glavo 7da2671ec5 Use custom log4j2.xml 2021-12-11 23:52:03 +08:00
Glavo b6ea4c7003 Fix the problem that you cannot continue after canceling the prompt 2021-12-11 23:52:03 +08:00
Glavo 49378061bf Always issue security warning 2021-12-11 23:52:03 +08:00
Glavo 38013a9298 Issue security warning for CVE-2021-44228 2021-12-11 23:52:03 +08:00
Glavo 5ffbd16800 Prohibit JNDI remote invoke 2021-12-11 23:52:03 +08:00
Glavo bcb29ef792 Update GitHub workflow configuration 2021-12-11 23:50:37 +08:00
Glavo d20adb20d3 fix: Logs should be encoded in UTF-8 2021-12-11 23:49:29 +08:00
Kylepoops 4f28cdd58f Fix CVE-2021-44228 again
https://github.com/apache/logging-log4j2/pull/614
2021-12-11 23:49:09 +08:00
huanghongxun 4b6b5c2f0e fix(game): fix remote code execution from log4j. 2021-12-10 14:22:16 +08:00
60 changed files with 934 additions and 324 deletions

No files matched your search

+9 -13
View File
@@ -1,16 +1,14 @@
name: Check Update
on:
push:
branches:
- javafx
workflow_dispatch:
schedule:
- cron: '30 * * * *'
jobs:
check-update:
if: ${{ github.repository_owner == 'huanghongxun' }}
runs-on: ubuntu-latest
env:
HMCL_BASE_VERSION: '3.4'
steps:
- uses: actions/checkout@v2
- name: Fetch tags
@@ -20,15 +18,15 @@ jobs:
- name: Fetch last version
run: |
wget -O ci.json https://ci.huangyuhui.net/job/HMCL/lastSuccessfulBuild/api/json
export HMCL_BUILD_NUMBER=`cat ci.json | jq -M -r '.id'`
export HMCL_VERSION="$HMCL_BASE_VERSION.$HMCL_BUILD_NUMBER"
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 -E "^HMCL-[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?\.exe\$"` ]; then exit 1; fi
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_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
@@ -42,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: |
@@ -63,5 +61,3 @@ jobs:
target_commitish: ${{ env.HMCL_COMMIT_SHA }}
name: ${{ env.HMCL_TAG_NAME }}
tag_name: ${{ env.HMCL_TAG_NAME }}
+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
-44
View File
@@ -1,44 +0,0 @@
name: Create Release
on:
push:
tags:
- "v*.*.*"
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Check and set HMCL version
run: |
export HMCL_VERSION=`echo ${{ github.ref }} | awk '{print substr($0,12)}'`
echo "HMCL_VERSION=$HMCL_VERSION" >> $GITHUB_ENV
if [ -z `echo $HMCL_VERSION | grep "^[0-9]\.[0-9]\.[0-9]\{3\}\$"` ]; then
exit 1
fi
echo "HMCL_BUILD_NUMBER=`echo $HMCL_VERSION | awk '{print substr($0,5)}'`" >> $GITHUB_ENV
- name: Download artifacts
run: |
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.exe"
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.exe.sha1"
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
- name: Generate release note
run: |
echo "HMCL v$HMCL_VERSION" >> RELEASE_NOTE
echo "" >> RELEASE_NOTE
echo "The full changelogs can be found on the website: https://hmcl.huangyuhui.net/changelog/dev.html" >> RELEASE_NOTE
echo "Notice: changelogs are written in Chinese." >> RELEASE_NOTE
- name: Create release
uses: softprops/action-gh-release@v1
with:
body_path: RELEASE_NOTE
files: |
HMCL-${{ env.HMCL_VERSION }}.exe
HMCL-${{ env.HMCL_VERSION }}.exe.sha1
HMCL-${{ env.HMCL_VERSION }}.jar
HMCL-${{ env.HMCL_VERSION }}.jar.sha1
+6 -3
View File
@@ -17,20 +17,23 @@ NVIDIA
/build/
/HMCL/build/
/HMCLCore/build/
/HMCLTransformerDiscoveryService/build/
/minecraft/libraries/HMCLTransformerDiscoveryService/build/
/minecraft/libraries/log4j-patch/build/
# idea
.idea
/out/
/HMCL/out/
/HMCLCore/out/
/HMCLTransformerDiscoveryService/out/
/minecraft/libraries/HMCLTransformerDiscoveryService/out/
/minecraft/libraries/log4j-patch/out/
# eclipse
/bin/
/HMCL/bin/
/HMCLCore/bin/
/HMCLTransformerDiscoveryService/bin/
/minecraft/libraries/HMCLTransformerDiscoveryService/bin/
/minecraft/libraries/log4j-patch/bin/
.classpath
.project
.settings
-1
View File
@@ -129,7 +129,6 @@ shadowJar {
exclude(dependency('com.google.code.gson:.*:.*'))
exclude(dependency('com.github.steveice10:.*:.*'))
exclude(dependency('libs:JFoenix:.*'))
exclude(project(":HMCLTransformerDiscoveryService"))
}
manifest {
@@ -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,
@@ -494,7 +494,7 @@ public final class LauncherHelper {
// Cannot allocate too much memory exceeding free space.
if (!suggested && OperatingSystem.TOTAL_MEMORY > 0 && OperatingSystem.TOTAL_MEMORY < setting.getMaxMemory()) {
Controllers.confirm(i18n("launch.advice.not_enough_space", OperatingSystem.TOTAL_MEMORY), i18n("message.error"), continueAction, null);
Controllers.confirm(i18n("launch.advice.not_enough_space", OperatingSystem.TOTAL_MEMORY), i18n("message.error"), continueAction, breakAction);
suggested = true;
}
@@ -506,7 +506,7 @@ public final class LauncherHelper {
VersionNumber.VERSION_COMPARATOR.compare(it.getVersion(), "1.12.2-14.23.5.2773") < 0);
boolean hasLiteLoader = version.getLibraries().stream().anyMatch(it -> it.is("com.mumfrey", "liteloader"));
if (hasForge2760 && hasLiteLoader && gameVersion.compareTo(VersionNumber.asVersion("1.12.2")) == 0) {
Controllers.confirm(i18n("launch.advice.forge2760_liteloader"), i18n("message.error"), continueAction, null);
Controllers.confirm(i18n("launch.advice.forge2760_liteloader"), i18n("message.error"), continueAction, breakAction);
suggested = true;
}
}
@@ -518,12 +518,12 @@ public final class LauncherHelper {
VersionNumber.VERSION_COMPARATOR.compare("1.14.4-28.2.2", it.getVersion()) <= 0);
boolean hasOptiFine = version.getLibraries().stream().anyMatch(it -> it.is("optifine", "OptiFine"));
if (hasForge28_2_2 && hasOptiFine && gameVersion.compareTo(VersionNumber.asVersion("1.14.4")) == 0) {
Controllers.confirm(i18n("launch.advice.forge28_2_2_optifine"), i18n("message.error"), continueAction, null);
Controllers.confirm(i18n("launch.advice.forge28_2_2_optifine"), i18n("message.error"), continueAction, breakAction);
suggested = true;
}
}
if (!suggested) {
if (!future.isDone()) {
future.complete(javaVersion);
}
@@ -30,7 +30,6 @@ import java.nio.file.*;
import java.util.Map;
import java.util.logging.Level;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.jackhuang.hmcl.util.Logging.LOG;
public final class ConfigHolder {
@@ -175,7 +174,7 @@ public final class ConfigHolder {
private static void writeToConfig(String content) throws IOException {
LOG.info("Saving config");
synchronized (configLocation) {
Files.write(configLocation, content.getBytes(UTF_8));
FileUtils.saveSafely(configLocation, content);
}
}
@@ -219,7 +218,7 @@ public final class ConfigHolder {
private static void writeToGlobalConfig(String content) throws IOException {
LOG.info("Saving global config");
synchronized (GLOBAL_CONFIG_PATH) {
Files.write(GLOBAL_CONFIG_PATH, content.getBytes(UTF_8));
FileUtils.saveSafely(GLOBAL_CONFIG_PATH, content);
}
}
@@ -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;
}
}
@@ -60,7 +60,7 @@ public final class VersionSetting implements Cloneable {
this.global = global;
}
private final BooleanProperty usesGlobalProperty = new SimpleBooleanProperty(this, "usesGlobal", false);
private final BooleanProperty usesGlobalProperty = new SimpleBooleanProperty(this, "usesGlobal", true);
public BooleanProperty usesGlobalProperty() {
return usesGlobalProperty;
@@ -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);
});
}
}
}
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialogLayout;
import javafx.concurrent.Worker;
import javafx.scene.control.Label;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
@@ -41,23 +42,23 @@ public class UpgradeDialog extends JFXDialogLayout {
{
String url = CHANGELOG_URL + remoteVersion.getChannel().channelName + ".html";
WebView webView = new WebView();
webView.getEngine().setUserDataDirectory(Metadata.HMCL_DIRECTORY.toFile());
try {
WebView webView = new WebView();
webView.getEngine().setUserDataDirectory(Metadata.HMCL_DIRECTORY.toFile());
WebEngine engine = webView.getEngine();
engine.load(CHANGELOG_URL + remoteVersion.getChannel().channelName);
engine.load(url);
engine.getLoadWorker().stateProperty().addListener((observable, oldValue, newValue) -> {
String viewURL = engine.getLoadWorker().getMessage().trim();
if (!viewURL.startsWith(CHANGELOG_URL)) {
engine.getLoadWorker().cancel();
FXUtils.openLink(viewURL);
if (newValue == Worker.State.FAILED) {
LOG.warning("Failed to load update log, trying to open it in browser");
FXUtils.openLink(url);
setBody();
}
});
setBody(webView);
} catch (NoClassDefFoundError | UnsatisfiedLinkError e) {
LOG.log(Level.WARNING, "WebView is missing or initialization failed", e);
FXUtils.openLink(url);
}
setBody(webView);
}
{
@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.ui.download;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXCheckBox;
import com.jfoenix.controls.JFXListView;
import com.jfoenix.controls.JFXSpinner;
@@ -39,7 +40,9 @@ import org.jackhuang.hmcl.download.forge.ForgeRemoteVersion;
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
import org.jackhuang.hmcl.download.liteloader.LiteLoaderRemoteVersion;
import org.jackhuang.hmcl.download.optifine.OptiFineRemoteVersion;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.ComponentList;
@@ -58,6 +61,7 @@ import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.wrap;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -84,6 +88,8 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
@FXML
private JFXCheckBox chkOld;
@FXML
private JFXButton btnRefresh;
@FXML
private HBox checkPane;
@FXML
private ComponentList centrePane;
@@ -114,6 +120,8 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
chkSnapshot.selectedProperty().addListener(listener);
chkOld.selectedProperty().addListener(listener);
btnRefresh.setGraphic(wrap(SVG.refresh(Theme.blackFillBinding(), -1, -1)));
list.setCellFactory(listView -> new ListCell<RemoteVersion>() {
IconedTwoLineListItem content = new IconedTwoLineListItem();
RipplerContainer ripplerContainer = new RipplerContainer(content);
@@ -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);
}
@@ -128,7 +128,7 @@ public class FeedbackPage extends VBox implements PageAware {
container.setPickOnBounds(false);
container.setAlignment(Pos.CENTER_LEFT);
HBox.setHgrow(content, Priority.ALWAYS);
content.setMouseTransparent(true);
content.setMouseTransparent(false);
setSelectable();
likeButton.getStyleClass().add("toggle-icon4");
@@ -152,7 +152,7 @@ public class FeedbackPage extends VBox implements PageAware {
"#" + feedback.getId(),
i18n("feedback.state." + feedback.getState().name().toLowerCase(Locale.US)),
i18n("feedback.type." + feedback.getType().name().toLowerCase(Locale.US)));
container.setOnMouseClicked(e -> {
content.setOnMouseClicked(e -> {
getFeedback(feedback.getId())
.thenAcceptAsync(Schedulers.javafx(), f -> {
Controllers.dialog(new ViewFeedbackDialog(f));
@@ -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();
@@ -400,7 +401,8 @@ public final class MultiplayerManager {
onExit.fireEvent(new CatoExitEvent(this, CatoExitEvent.EXIT_CODE_INTERRUPTED));
} finally {
try {
writer.close();
if (writer != null)
writer.close();
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to close cato stdin writer", e);
}
@@ -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);
@@ -112,7 +112,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
private final StringProperty selectedVersion = new SimpleStringProperty();
private final BooleanProperty navigateToSpecificSettings = new SimpleBooleanProperty(false);
private final BooleanProperty enableSpecificSettings = new SimpleBooleanProperty(true);
private final BooleanProperty enableSpecificSettings = new SimpleBooleanProperty(false);
private final IntegerProperty maxMemory = new SimpleIntegerProperty();
private final ObjectProperty<OperatingSystem.PhysicalMemoryStatus> memoryStatus = new SimpleObjectProperty<>(OperatingSystem.PhysicalMemoryStatus.INVALID);
private final BooleanProperty modpack = new SimpleBooleanProperty();
@@ -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"));
}
@@ -23,10 +23,26 @@
<JFXSpinner fx:id="spinner" styleClass="first-spinner" />
<StackPane fx:id="center" style="-fx-padding: 10;">
<ComponentList fx:id="centrePane" styleClass="no-padding">
<HBox fx:id="checkPane" spacing="10" style="-fx-padding: 10;">
<JFXCheckBox fx:id="chkRelease" text="%version.game.releases" selected="true" />
<JFXCheckBox fx:id="chkSnapshot" text="%version.game.snapshots" />
<JFXCheckBox fx:id="chkOld" text="%version.game.old" />
<HBox fx:id="checkPane" spacing="10">
<JFXCheckBox fx:id="chkRelease" text="%version.game.releases" selected="true">
<HBox.margin>
<Insets top="10" bottom="10" />
</HBox.margin>
</JFXCheckBox>
<JFXCheckBox fx:id="chkSnapshot" text="%version.game.snapshots">
<HBox.margin>
<Insets top="10" bottom="10" />
</HBox.margin>
</JFXCheckBox>
<JFXCheckBox fx:id="chkOld" text="%version.game.old">
<HBox.margin>
<Insets top="10" bottom="10" />
</HBox.margin>
</JFXCheckBox>
<HBox HBox.hgrow="ALWAYS" />
<JFXButton fx:id="btnRefresh" styleClass="jfx-tool-bar-button" onMouseClicked="#onRefresh" text="%button.refresh" />
</HBox>
<JFXListView fx:id="list" styleClass="jfx-list-view-float" VBox.vgrow="ALWAYS">
</JFXListView>
@@ -182,6 +182,11 @@ curse.category.4472=Tech
curse.category.4473=Magic
# 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=Map and Information
curse.category.426=Addons
curse.category.434=Armor, Tools, and Weapons
@@ -223,7 +228,8 @@ curse.category.4558=Redstone
curse.category.4843=Automation
curse.category.4906=MCreator
# https://addons-ecs.forgesvc.net/api/v2/category/section/6
# https://addons-ecs.forgesvc.net/api/v2/category/section/12
curse.category.5193=Data Packs
curse.category.399=Steampunk
curse.category.396=128x
curse.category.398=512x and Higher
@@ -943,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=次へ >
@@ -182,6 +182,11 @@ curse.category.4472=科技
curse.category.4473=魔法
# https://addons-ecs.forgesvc.net/api/v2/category/section/6
curse.category.5129=原版增強
curse.category.5189=實用與QOL
curse.category.5190=QoL
curse.category.5191=實用與QoL
curse.category.5192=夢幻菜單
curse.category.423=訊息展示
curse.category.426=模組擴展
curse.category.434=裝備武器
@@ -224,6 +229,7 @@ curse.category.4843=自動化
curse.category.4906=MCreator
# https://addons-ecs.forgesvc.net/api/v2/category/section/6
curse.category.5193=封包
curse.category.399=蒸汽朋克
curse.category.396=128x
curse.category.398=512x 及更高
@@ -943,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=更新
@@ -182,6 +182,11 @@ curse.category.4472=科技
curse.category.4473=魔法
# https://addons-ecs.forgesvc.net/api/v2/category/section/6
curse.category.5129=原版增强
curse.category.5189=实用与QOL
curse.category.5190=QoL
curse.category.5191=实用与QoL
curse.category.5192=梦幻菜单
curse.category.423=信息展示
curse.category.426=模组扩展
curse.category.434=装备武器
@@ -223,7 +228,8 @@ curse.category.4558=红石
curse.category.4843=自动化
curse.category.4906=MCreator
# https://addons-ecs.forgesvc.net/api/v2/category/section/6
# https://addons-ecs.forgesvc.net/api/v2/category/section/12
curse.category.5193=数据包
curse.category.399=蒸汽朋克
curse.category.396=128x
curse.category.398=512x 及更高
@@ -943,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=更新
+8
View File
@@ -17,3 +17,11 @@ dependencies {
api group: 'org.apache.commons', name: 'commons-lang3', version: '3.12.0'
compileOnlyApi group: 'org.jetbrains', name: 'annotations', version: '16.0.3'
}
tasks.processResources {
dependsOn ':log4j-patch:jar'
into('assets/game') {
from project(':log4j-patch').file("build/libs")
}
}
@@ -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());
@@ -27,6 +27,7 @@ import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.Task;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
@@ -78,7 +79,8 @@ public class DefaultDependencyManager extends AbstractDependencyManager {
Version version = original.resolve(repository);
return Task.allOf(
Task.composeAsync(() -> {
if (!repository.getVersionJar(version).exists())
File versionJar = repository.getVersionJar(version);
if (!versionJar.exists() || versionJar.length() == 0)
return new GameDownloadTask(this, null, version);
else
return null;
@@ -27,6 +27,7 @@ import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -62,18 +63,31 @@ public class MaintainTask extends Task<Version> {
String mainClass = version.resolve(null).getMainClass();
if (mainClass != null && mainClass.equals(LibraryAnalyzer.LAUNCH_WRAPPER_MAIN)) {
return maintainOptiFineLibrary(repository, maintainGameWithLaunchWrapper(unique(version), true), false);
version = maintainOptiFineLibrary(repository, maintainGameWithLaunchWrapper(unique(version), true), false);
} else if (mainClass != null && mainClass.equals(LibraryAnalyzer.MOD_LAUNCHER_MAIN)) {
// Forge 1.13 and OptiFine
return maintainOptiFineLibrary(repository, maintainGameWithCpwModLauncher(repository, unique(version)), true);
version = maintainOptiFineLibrary(repository, maintainGameWithCpwModLauncher(repository, unique(version)), true);
} else if (mainClass != null && mainClass.equals(LibraryAnalyzer.BOOTSTRAP_LAUNCHER_MAIN)) {
// Forge 1.17
return maintainGameWithCpwBoostrapLauncher(repository, unique(version));
version = maintainGameWithCpwBoostrapLauncher(repository, unique(version));
} else {
// Vanilla Minecraft does not need maintain
// Fabric does not need maintain, nothing compatible with fabric now.
return maintainOptiFineLibrary(repository, unique(version), false);
version = maintainOptiFineLibrary(repository, unique(version), false);
}
List<Library> libraries = version.getLibraries();
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()));
}
}
return version;
}
public static Version maintainPreservingPatches(GameRepository repository, Version version) {
@@ -136,10 +150,9 @@ public class MaintainTask extends Task<Version> {
builder.addJvmArgument("-Dhmcl.transformer.candidates=${library_directory}/" + library.getPath());
if (!libraryExisting) builder.addLibrary(hmclTransformerDiscoveryService);
Path libraryPath = repository.getLibraryFile(version, hmclTransformerDiscoveryService).toPath();
try {
try (InputStream input = MaintainTask.class.getResourceAsStream("/assets/game/HMCLTransformerDiscoveryService-1.0.jar")) {
Files.createDirectories(libraryPath.getParent());
Files.copy(MaintainTask.class.getResourceAsStream("/assets/game/HMCLTransformerDiscoveryService-1.0.jar"),
libraryPath, StandardCopyOption.REPLACE_EXISTING);
Files.copy(input, libraryPath, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
Logging.LOG.log(Level.WARNING, "Unable to unpack HMCLTransformerDiscoveryService", e);
}
@@ -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())));
}
}
}));
@@ -229,6 +229,7 @@ public final class OptiFineInstallTask extends Task<Version> {
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(installer)) {
Path configClass = fs.getPath("Config.class");
if (!Files.exists(configClass)) configClass = fs.getPath("net/optifine/Config.class");
if (!Files.exists(configClass)) configClass = fs.getPath("notch/net/optifine/Config.class");
if (!Files.exists(configClass)) throw new IOException("Unrecognized installer");
ConstantPool pool = ConstantPoolScanner.parse(Files.readAllBytes(configClass), ConstantType.UTF8);
List<String> constants = new ArrayList<>();
@@ -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,24 +19,20 @@ 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;
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.io.Unzipper;
import org.jackhuang.hmcl.util.platform.CommandBuilder;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.util.platform.ManagedProcess;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Bits;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.io.*;
import java.net.InetSocketAddress;
@@ -46,10 +42,13 @@ 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;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.Pair.pair;
/**
@@ -75,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");
}
@@ -92,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");
}
@@ -114,6 +113,21 @@ public class DefaultLauncher extends Launcher {
res.addAllWithoutParsing(options.getJavaArguments());
Charset encoding = OperatingSystem.NATIVE_CHARSET;
// After Java 17, file.encoding does not affect console encoding
if (options.getJava().getParsedVersion() <= JavaVersion.JAVA_17) {
try {
String fileEncoding = res.addDefault("-Dfile.encoding=", encoding.name());
if (fileEncoding != null)
encoding = Charset.forName(fileEncoding.substring("-Dfile.encoding=".length()));
} catch (Throwable ex) {
LOG.log(Level.WARNING, "Bad file encoding", ex);
}
}
Optional<Library> log4j = findLog4j();
// JVM Args
if (!options.isNoGeneratedJVMArgs()) {
appendJvmArgs(res);
@@ -179,8 +193,22 @@ public class DefaultLauncher extends Launcher {
res.addDefault("-Dfml.ignoreInvalidMinecraftCertificates=", "true");
res.addDefault("-Dfml.ignorePatchDiscrepancies=", "true");
// Fix RCE vulnerability of log4j2
res.addDefault("-Dlog4j2.formatMsgNoLookups=", "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
res.addDefault("-Djava.rmi.server.useCodebaseOnly=", "true");
res.addDefault("-Dcom.sun.jndi.rmi.object.trustURLCodebase=", "false");
res.addDefault("-Dcom.sun.jndi.cosnaming.object.trustURLCodebase=", "false");
String formatMsgNoLookups = res.addDefault("-Dlog4j2.formatMsgNoLookups=", "true");
if (!"-Dlog4j2.formatMsgNoLookups=false".equals(formatMsgNoLookups) && log4j.isPresent()) {
res.addDefault("-Dlog4j.configurationFile=", getLog4jConfigurationFile().getAbsolutePath());
}
Proxy proxy = options.getProxy();
@@ -227,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());
@@ -239,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(":");
@@ -272,7 +301,7 @@ public class DefaultLauncher extends Launcher {
res.addAllWithoutParsing(Arguments.parseStringArguments(options.getGameArguments(), configuration));
res.removeIf(it -> getForbiddens().containsKey(it) && getForbiddens().get(it).get());
return new Command(res, tempNativeFolder);
return new Command(res, tempNativeFolder, encoding);
}
public Map<String, Boolean> getFeatures() {
@@ -336,6 +365,48 @@ public class DefaultLauncher extends Launcher {
}
}
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() {
return new File(repository.getVersionRoot(version.getId()), "log4j2.xml");
}
public void extractLog4jConfigurationFile() throws IOException {
File targetFile = getLog4jConfigurationFile();
InputStream source;
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)) {
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
@@ -380,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);
@@ -403,6 +466,13 @@ public class DefaultLauncher extends Launcher {
decompressNatives(nativeFolder);
}
Optional<Library> log4j = findLog4j();
if (log4j.isPresent()) {
extractLog4jConfigurationFile();
if (isLog4jUnsafe(log4j.get()))
extractLog4jAgent();
}
File runDirectory = repository.getRunDirectory(version.getId());
if (StringUtils.isNotBlank(options.getPreLaunchCommand())) {
@@ -419,21 +489,16 @@ 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, daemon);
startMonitors(p, listener, command.encoding, daemon);
return p;
}
@@ -477,6 +542,13 @@ public class DefaultLauncher extends Launcher {
decompressNatives(nativeFolder);
}
Optional<Library> log4j = findLog4j();
if (log4j.isPresent()) {
extractLog4jConfigurationFile();
if (isLog4jUnsafe(log4j.get()))
extractLog4jAgent();
}
String scriptExtension = FileUtils.getExtension(scriptFile);
boolean usePowerShell = "ps1".equals(scriptExtension);
@@ -594,17 +666,17 @@ public class DefaultLauncher extends Launcher {
throw new ExecutionPolicyLimitException();
}
private void startMonitors(ManagedProcess managedProcess, ProcessListener processListener, boolean isDaemon) {
private void startMonitors(ManagedProcess managedProcess, ProcessListener processListener, Charset encoding, boolean isDaemon) {
processListener.setProcess(managedProcess);
Thread stdout = Lang.thread(new StreamPump(managedProcess.getProcess().getInputStream(), it -> {
processListener.onLog(it, Optional.ofNullable(Log4jLevel.guessLevel(it)).orElse(Log4jLevel.INFO));
managedProcess.addLine(it);
}, OperatingSystem.NATIVE_CHARSET), "stdout-pump", isDaemon);
}, encoding), "stdout-pump", isDaemon);
managedProcess.addRelatedThread(stdout);
Thread stderr = Lang.thread(new StreamPump(managedProcess.getProcess().getErrorStream(), it -> {
processListener.onLog(it, Log4jLevel.ERROR);
managedProcess.addLine(it);
}, OperatingSystem.NATIVE_CHARSET), "stderr-pump", isDaemon);
}, encoding), "stderr-pump", isDaemon);
managedProcess.addRelatedThread(stderr);
managedProcess.addRelatedThread(Lang.thread(new ExitWaiter(managedProcess, Arrays.asList(stdout, stderr), processListener::onExit), "exit-waiter", isDaemon));
}
@@ -612,10 +684,12 @@ public class DefaultLauncher extends Launcher {
private static final class Command {
final CommandBuilder commandLine;
final Path tempNativeFolder;
final Charset encoding;
Command(CommandBuilder commandBuilder, Path tempNativeFolder) {
Command(CommandBuilder commandBuilder, Path tempNativeFolder, Charset encoding) {
this.commandLine = commandBuilder;
this.tempNativeFolder = tempNativeFolder;
this.encoding = encoding;
}
}
}
@@ -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",
@@ -62,7 +62,6 @@ public class McbbsModpackCompletionTask extends CompletableFutureTask<Void> {
private final String version;
private final File configurationFile;
private ModpackConfiguration<McbbsModpackManifest> configuration;
private GetTask dependent;
private McbbsModpackManifest manifest;
private final List<Task<?>> dependencies = new LinkedList<>();
@@ -108,11 +107,11 @@ public class McbbsModpackCompletionTask extends CompletableFutureTask<Void> {
}
})).thenComposeAsync(wrap(unused1 -> {
return executor.one(new GetTask(new URL(manifest.getFileApi() + "/manifest.json")));
})).thenComposeAsync(wrap(unused1 -> {
})).thenComposeAsync(wrap(remoteManifestJson -> {
McbbsModpackManifest remoteManifest;
// We needs to update modpack from online server.
try {
remoteManifest = JsonUtils.fromNonNullJson(dependent.getResult(), McbbsModpackManifest.class);
remoteManifest = JsonUtils.fromNonNullJson(remoteManifestJson, McbbsModpackManifest.class);
} catch (JsonParseException e) {
throw new IOException("Unable to parse server manifest.json from " + manifest.getFileApi(), e);
}
@@ -47,6 +47,7 @@ public final class Logging {
FileHandler fileHandler = new FileHandler(logFolder.resolve("hmcl.log").toAbsolutePath().toString());
fileHandler.setLevel(Level.FINEST);
fileHandler.setFormatter(DefaultFormatter.INSTANCE);
fileHandler.setEncoding("UTF-8");
LOG.addHandler(fileHandler);
} catch (IOException e) {
System.err.println("Unable to create hmcl.log, " + e.getMessage());
@@ -88,7 +89,11 @@ public final class Logging {
}
public static String getLogs() {
return storedLogs.toString();
try {
return storedLogs.toString("UTF-8");
} catch (UnsupportedEncodingException e) {
throw new InternalError(e);
}
}
private static final class DefaultFormatter extends Formatter {
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.util.io;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
@@ -36,7 +37,6 @@ import java.util.function.Predicate;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
*
* @author huang
*/
public final class FileUtils {
@@ -425,4 +425,24 @@ public final class FileUtils {
return Optional.empty();
}
}
public static Path tmpSaveFile(Path file) {
return file.toAbsolutePath().resolveSibling("." + file.getFileName().toString() + ".tmp");
}
public static void saveSafely(Path file, String content) throws IOException {
Path tmpFile = tmpSaveFile(file);
try (BufferedWriter writer = Files.newBufferedWriter(tmpFile, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE)) {
writer.write(content);
}
try {
if (Files.exists(file) && Files.getAttribute(file, "dos:hidden") == Boolean.TRUE) {
Files.setAttribute(tmpFile, "dos:hidden", true);
}
} catch (Throwable ignored) {
}
Files.move(tmpFile, file, StandardCopyOption.REPLACE_EXISTING);
}
}
@@ -83,33 +83,33 @@ public final class CommandBuilder {
return this;
}
public CommandBuilder addDefault(String opt) {
public String addDefault(String opt) {
for (Item item : raw) {
if (item.arg.equals(opt)) {
return this;
return item.arg;
}
}
raw.add(new Item(opt, true));
return this;
return null;
}
public CommandBuilder addDefault(String opt, String value) {
public String addDefault(String opt, String value) {
for (Item item : raw) {
if (item.arg.startsWith(opt)) {
LOG.info("Default option '" + opt + value + "' is suppressed by '" + item.arg + "'");
return this;
return item.arg;
}
}
raw.add(new Item(opt + value, true));
return this;
return null;
}
public CommandBuilder addUnstableDefault(String opt, boolean value) {
public String addUnstableDefault(String opt, boolean value) {
for (Item item : raw) {
final Matcher matcher = UNSTABLE_BOOLEAN_OPTION_PATTERN.matcher(item.arg);
if (matcher.matches()) {
if (matcher.group("key").equals(opt)) {
return this;
return item.arg;
}
}
}
@@ -119,21 +119,21 @@ public final class CommandBuilder {
} else {
raw.add(new Item("-XX:-" + opt, true));
}
return this;
return null;
}
public CommandBuilder addUnstableDefault(String opt, String value) {
public String addUnstableDefault(String opt, String value) {
for (Item item : raw) {
final Matcher matcher = UNSTABLE_OPTION_PATTERN.matcher(item.arg);
if (matcher.matches()) {
if (matcher.group("key").equals(opt)) {
return this;
return item.arg;
}
}
}
raw.add(new Item("-XX:" + opt + "=" + value, true));
return this;
return null;
}
public boolean removeIf(Predicate<String> pred) {
@@ -112,6 +112,7 @@ public final class JavaVersion {
public static final int JAVA_8 = 8;
public static final int JAVA_9 = 9;
public static final int JAVA_16 = 16;
public static final int JAVA_17 = 17;
private static int parseVersion(String version) {
Matcher matcher = VERSION.matcher(version);
@@ -22,6 +22,7 @@ import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.charset.UnsupportedCharsetException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -112,15 +113,26 @@ public enum OperatingSystem {
private static final Pattern MEMINFO_PATTERN = Pattern.compile("^(?<key>.*?):\\s+(?<value>\\d+) kB?$");
static {
String nativeEncoding = System.getProperty("native.encoding", System.getProperty("sun.jnu.encoding"));
String nativeEncoding = System.getProperty("native.encoding");
String hmclNativeEncoding = System.getProperty("hmcl.native.encoding");
Charset nativeCharset = Charset.defaultCharset();
if (nativeEncoding != null) {
try {
nativeCharset = Charset.forName(nativeEncoding);
} catch (UnsupportedCharsetException e) {
e.printStackTrace();
try {
if (hmclNativeEncoding != null) {
nativeCharset = Charset.forName(hmclNativeEncoding);
} else {
if (nativeEncoding != null && !nativeEncoding.equalsIgnoreCase(nativeCharset.name())) {
nativeCharset = Charset.forName(nativeEncoding);
}
if (nativeCharset == StandardCharsets.UTF_8 || nativeCharset == StandardCharsets.US_ASCII) {
nativeCharset = StandardCharsets.UTF_8;
} else if ("GBK".equalsIgnoreCase(nativeCharset.name()) || "GB2312".equalsIgnoreCase(nativeCharset.name())) {
nativeCharset = Charset.forName("GB18030");
}
}
} catch (UnsupportedCharsetException e) {
e.printStackTrace();
}
NATIVE_CHARSET = nativeCharset;
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Appenders>
<Console name="SysOut" target="SYSTEM_OUT">
<PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level]: %msg{nolookups}%n" />
</Console>
<Queue name="ServerGuiConsole">
<PatternLayout pattern="[%d{HH:mm:ss} %level]: %msg{nolookups}%n" />
</Queue>
<RollingRandomAccessFile name="File" fileName="logs/latest.log" filePattern="logs/%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level]: %msg{nolookups}%n" />
<Policies>
<TimeBasedTriggeringPolicy />
<OnStartupTriggeringPolicy />
</Policies>
</RollingRandomAccessFile>
</Appenders>
<Loggers>
<Root level="info">
<filters>
<MarkerFilter marker="NETWORK_PACKETS" onMatch="DENY" onMismatch="NEUTRAL" />
</filters>
<AppenderRef ref="SysOut"/>
<AppenderRef ref="File"/>
</Root>
</Loggers>
</Configuration>
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Appenders>
<Console name="SysOut" target="SYSTEM_OUT">
<PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level]: %msg%n" />
</Console>
<Queue name="ServerGuiConsole">
<PatternLayout pattern="[%d{HH:mm:ss} %level]: %msg%n" />
</Queue>
<RollingRandomAccessFile name="File" fileName="logs/latest.log" filePattern="logs/%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level]: %msg%n" />
<Policies>
<TimeBasedTriggeringPolicy />
<OnStartupTriggeringPolicy />
</Policies>
</RollingRandomAccessFile>
</Appenders>
<Loggers>
<Root level="info">
<filters>
<MarkerFilter marker="NETWORK_PACKETS" onMatch="DENY" onMismatch="NEUTRAL" />
<RegexFilter regex=".*\$\{[^}]*\}.*" onMatch="DENY" onMismatch="NEUTRAL"/>
</filters>
<AppenderRef ref="SysOut"/>
<AppenderRef ref="File"/>
</Root>
</Loggers>
</Configuration>
@@ -1 +0,0 @@
org.jackhuang.hmcl.HMCLTransformerDiscoveryService
+1
View File
@@ -58,5 +58,6 @@ Make sure you have Java installed with JavaFX 8 at least. Liberica full JDK 8 or
|`-Dhmcl.update_source.override=<url>`|Override the update source.|
|`-Dhmcl.authlibinjector.location=<path>`|Use specified authlib-injector (instead of downloading one).|
|`-Dhmcl.openjfx.repo=<maven repository url>`|Add custom maven repository for download OpenJFX.|
|`-Dhmcl.native.encoding=<encoding>`|Override the native encoding.|
|`-Dhmcl.microsoft.auth.id=<App ID>`|Override Microsoft OAuth App ID.|
|`-Dhmcl.microsoft.auth.secret=<App Secret>`|Override Microsoft OAuth App secret.|
@@ -4,6 +4,11 @@ dependencies {
compileOnly project.files("lib/modlauncher-4.1.0.jar")
}
compileJava {
sourceCompatibility = 8
targetCompatibility = 8
}
jar {
manifest {
attributes 'Created-By': 'Copyright(c) 2013-2020 huangyuhui.',
+13
View File
@@ -0,0 +1,13 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.
@@ -0,0 +1,6 @@
# Log4j Patch
Copy from [Glavo/log4j-patch](https://github.com/Glavo/log4j-patch/).
`org.apache.logging.log4j.core.lookup.JndiLookup` is licensed under the WTFPL 2.0 license,
and `org.apache.logging.log4j.core.lookup.Interpolator` is licensed under the Apache 2.0 license.
@@ -0,0 +1,52 @@
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.withType(JavaCompile) {
sourceCompatibility = 8
targetCompatibility = 8
doLast {
FileTree tree = fileTree('build/classes/java')
tree.include '**/*.class'
tree.each {
RandomAccessFile rf = new RandomAccessFile(it, "rw")
rf.seek(7) // major version
rf.write(50) // java 6
rf.close()
}
}
}
task agentJar(type: Jar) {
dependsOn(tasks.compileJava)
baseName = 'log4j-patch-agent'
manifest {
attributes 'Premain-Class': 'org.glavo.log4j.patch.agent.Log4jAgent'
}
from(sourceSets.agent.output)
from(sourceSets.main.output) {
includeEmptyDirs = false
eachFile {
it.path = "org/glavo/log4j/patch/agent/${it.name}.bin"
}
}
}
tasks.jar {
enabled = false
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;
}
}
}
@@ -0,0 +1,155 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache license, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the license for the specific language governing permissions and
* limitations under the license.
*/
package org.apache.logging.log4j.core.lookup;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.config.plugins.PluginManager;
import org.apache.logging.log4j.core.config.plugins.PluginType;
import org.apache.logging.log4j.status.StatusLogger;
import java.util.HashMap;
import java.util.Map;
/**
* The Interpolator is a StrLookup that acts as a proxy for all the other StrLookups.
*/
public class Interpolator implements StrLookup {
private static final Logger LOGGER = StatusLogger.getLogger();
/** Constant for the prefix separator. */
private static final char PREFIX_SEPARATOR = ':';
private final Map<String, StrLookup> lookups = new HashMap<String, StrLookup>();
private final StrLookup defaultLookup;
@SuppressWarnings("deprecation")
public Interpolator(final StrLookup defaultLookup) {
this.defaultLookup = defaultLookup == null ? new MapLookup(new HashMap<String, String>()) : defaultLookup;
final PluginManager manager = new PluginManager("Lookup");
manager.collectPlugins();
final Map<String, PluginType<?>> plugins = manager.getPlugins();
for (final Map.Entry<String, PluginType<?>> entry : plugins.entrySet()) {
@SuppressWarnings("unchecked")
final Class<? extends StrLookup> clazz = (Class<? extends StrLookup>) entry.getValue().getPluginClass();
try {
lookups.put(entry.getKey(), clazz.newInstance());
} catch (final Exception ex) {
LOGGER.error("Unable to create Lookup for " + entry.getKey(), ex);
}
}
}
/**
* Create the default Interpolator using only Lookups that work without an event.
*/
public Interpolator() {
this.defaultLookup = new MapLookup(new HashMap<String, String>());
lookups.put("sys", new SystemPropertiesLookup());
lookups.put("env", new EnvironmentLookup());
// lookups.put("jndi", new JndiLookup());
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) {
lookups.put("web", new WebLookup());
}
} catch (ClassNotFoundException ex) {
LOGGER.debug("ServletContext not present - WebLookup not added");
} catch (Exception ex) {
LOGGER.error("Unable to locate ServletContext", ex);
}
}
/**
* Resolves the specified variable. This implementation will try to extract
* a variable prefix from the given variable name (the first colon (':') is
* used as prefix separator). It then passes the name of the variable with
* the prefix stripped to the lookup object registered for this prefix. If
* no prefix can be found or if the associated lookup object cannot resolve
* this variable, the default lookup object will be used.
*
* @param var the name of the variable whose value is to be looked up
* @return the value of this variable or <b>null</b> if it cannot be
* resolved
*/
@Override
public String lookup(final String var) {
return lookup(null, var);
}
/**
* Resolves the specified variable. This implementation will try to extract
* a variable prefix from the given variable name (the first colon (':') is
* used as prefix separator). It then passes the name of the variable with
* the prefix stripped to the lookup object registered for this prefix. If
* no prefix can be found or if the associated lookup object cannot resolve
* this variable, the default lookup object will be used.
*
* @param event The current LogEvent or null.
* @param var the name of the variable whose value is to be looked up
* @return the value of this variable or <b>null</b> if it cannot be
* resolved
*/
@Override
public String lookup(final LogEvent event, String var) {
if (var == null) {
return null;
}
final int prefixPos = var.indexOf(PREFIX_SEPARATOR);
if (prefixPos >= 0) {
final String prefix = var.substring(0, prefixPos);
final String name = var.substring(prefixPos + 1);
final StrLookup lookup = lookups.get(prefix);
String value = null;
if (lookup != null) {
value = event == null ? lookup.lookup(name) : lookup.lookup(event, name);
}
if (value != null) {
return value;
}
var = var.substring(prefixPos + 1);
}
if (defaultLookup != null) {
return event == null ? defaultLookup.lookup(var) : defaultLookup.lookup(event, var);
}
return null;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
for (final String name : lookups.keySet()) {
if (sb.length() == 0) {
sb.append("{");
} else {
sb.append(", ");
}
sb.append(name);
}
if (sb.length() > 0) {
sb.append("}");
}
return sb.toString();
}
}
@@ -0,0 +1,13 @@
/*
* Copyright © 2021 Glavo <zjx001202@gmail.com>
* This work is free. You can redistribute it and/or modify it under the
* terms of the Do What The Fuck You Want To Public License, Version 2,
* as published by Sam Hocevar. See http://www.wtfpl.net/ for more details.
*/
package org.apache.logging.log4j.core.lookup;
public class JndiLookup {
public JndiLookup() {
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.");
}
}
+4
View File
@@ -2,3 +2,7 @@ rootProject.name = 'HMCL3'
include ':HMCL'
include ':HMCLCore'
include ':HMCLTransformerDiscoveryService'
include ':log4j-patch'
project(':HMCLTransformerDiscoveryService').projectDir = file('minecraft/libraries/HMCLTransformerDiscoveryService')
project(':log4j-patch').projectDir = file('minecraft/libraries/log4j-patch')