Compare commits
76
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4549b6f68 | ||
|
|
4cf5dbdf66 | ||
|
|
b179c44bec | ||
|
|
7dfc654d5d | ||
|
|
a4b930ded5 | ||
|
|
00cd20a6e9 | ||
|
|
9ebf29d060 | ||
|
|
d7d5e99185 | ||
|
|
5e9ed84605 | ||
|
|
2e01874e95 | ||
|
|
efb475879b | ||
|
|
cb69387151 | ||
|
|
70104cb9e5 | ||
|
|
931fbf5181 | ||
|
|
1d5ecb9236 | ||
|
|
b65104e38b | ||
|
|
8629a2bbe3 | ||
|
|
3142b2f894 | ||
|
|
b1759acb62 | ||
|
|
1363f0e70d | ||
|
|
45c4913b82 | ||
|
|
0d47ff95e1 | ||
|
|
4064c5c188 | ||
|
|
56a2f75c8b | ||
|
|
c4ebdeb77e | ||
|
|
13b87fff89 | ||
|
|
50072799d0 | ||
|
|
16d45890ad | ||
|
|
e0dacd0265 | ||
|
|
72071a41c9 | ||
|
|
c86c19a7f9 | ||
|
|
5475acdc9e | ||
|
|
6daeeb0b04 | ||
|
|
1a4ccd355d | ||
|
|
204a08342d | ||
|
|
ced06cb48c | ||
|
|
80b3883d4f | ||
|
|
60c7ea5a01 | ||
|
|
3323c6a50a | ||
|
|
7530f08ab1 | ||
|
|
15a983de28 | ||
|
|
5c0d4a7c7b | ||
|
|
1532247f2a | ||
|
|
587e92543a | ||
|
|
f0527e12b4 | ||
|
|
b0ac4fb5d0 | ||
|
|
083dbb18ad | ||
|
|
c9585198e1 | ||
|
|
0fd83e5177 | ||
|
|
5a017ecab6 | ||
|
|
da77f52492 | ||
|
|
dccfa7c9a9 | ||
|
|
f7bf2d468d | ||
|
|
a10c501d4f | ||
|
|
116629743e | ||
|
|
cfee31d339 | ||
|
|
3ac08d7f62 | ||
|
|
de643becf8 | ||
|
|
edbcc1f343 | ||
|
|
91fcc4e5ef | ||
|
|
34aeba1b1a | ||
|
|
9a36b86cfb | ||
|
|
71b4c02474 | ||
|
|
45f49d079b | ||
|
|
71d9daed16 | ||
|
|
5ae019ba17 | ||
|
|
770c58c2e7 | ||
|
|
a03c11be42 | ||
|
|
ca3746319f | ||
|
|
56629892f8 | ||
|
|
5238a361cd | ||
|
|
0323a7f321 | ||
|
|
a3c5e8e412 | ||
|
|
5c364f624a | ||
|
|
22275f2c57 | ||
|
|
11e54d199c |
No files matched your search
@@ -4,6 +4,7 @@
|
||||
|
||||
# ─── Explicitly text – source & build ────────────────────────────────────────
|
||||
*.java text eol=lf
|
||||
*.js text eol=lf
|
||||
*.kt text eol=lf
|
||||
*.kts text eol=lf
|
||||
*.groovy text eol=lf
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
const fs = require("fs");
|
||||
|
||||
module.exports = async ({ github, context, core }) => {
|
||||
const prNumberPath = process.env.PULL_REQUEST_NUMBER_PATH;
|
||||
if (!prNumberPath || !fs.existsSync(prNumberPath) || !fs.statSync(prNumberPath).isFile()) {
|
||||
core.setFailed("Pull request number file not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
const reportsPath = process.env.REPORTS_PATH;
|
||||
if (!reportsPath || !fs.existsSync(reportsPath) || !fs.statSync(reportsPath).isFile()) {
|
||||
core.setFailed("Reports file not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
const PULL_REQUEST_NUMBER = parseInt(fs.readFileSync(prNumberPath, "utf8").trim(), 10);
|
||||
if (isNaN(PULL_REQUEST_NUMBER)) {
|
||||
core.setFailed("Failed to parse the pull request number.");
|
||||
return;
|
||||
}
|
||||
|
||||
const { owner, repo } = context.repo;
|
||||
|
||||
let pr;
|
||||
try {
|
||||
const response = await github.rest.pulls.get({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: PULL_REQUEST_NUMBER,
|
||||
});
|
||||
pr = response.data;
|
||||
} catch (error) {
|
||||
core.setFailed(`Failed to fetch PR #${PULL_REQUEST_NUMBER}: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
core.info(`Current PR state: ${pr.state}`);
|
||||
if (pr.state !== "open") {
|
||||
core.setFailed("The pull request is not open. Skipping comment creation.");
|
||||
return;
|
||||
}
|
||||
|
||||
const run = context.payload.workflow_run;
|
||||
if (!run) {
|
||||
core.setFailed("context.payload.workflow_run is undefined. Ensure this script runs on the 'workflow_run' event.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (pr.head.sha !== run.head_sha) {
|
||||
core.setFailed("PR head SHA does not match the workflow run head SHA. Skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (pr.head.ref !== run.head_branch) {
|
||||
core.setFailed("PR head branch does not match the workflow run head branch. Skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (pr.head.repo.full_name !== run.head_repository.full_name) {
|
||||
core.setFailed("PR head repository fullname does not match the workflow run head repository fullname. Skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
const comments = [];
|
||||
const maxCommentCount = 10;
|
||||
const reports = fs.readFileSync(reportsPath, "utf8").trim().split("\n").filter(Boolean);
|
||||
for (const report of reports) {
|
||||
if (comments.length >= maxCommentCount) break;
|
||||
try {
|
||||
const data = JSON.parse(report);
|
||||
if (typeof data !== "object" || Array.isArray(data)) continue;
|
||||
|
||||
const { location, message, severity, code } = data;
|
||||
if (typeof location !== "object" || Array.isArray(location)) continue;
|
||||
if (typeof message !== "string") continue;
|
||||
const severities = ["UNKNOWN_SEVERITY", "ERROR", "WARNING", "INFO", 0, 1, 2, 3];
|
||||
if (!severities.includes(severity)) continue;
|
||||
const severityText = typeof severity === "string" ? severity : severities[severity];
|
||||
if (typeof code !== "object" || Array.isArray(code)) continue;
|
||||
|
||||
const path = location.path;
|
||||
const line = location.range?.start?.line;
|
||||
const codeText = code.value ? `[${code.value}] ` : "";
|
||||
|
||||
if (path && line) {
|
||||
comments.push({
|
||||
path,
|
||||
line,
|
||||
side: "RIGHT",
|
||||
body: `**[${severityText}]** ${codeText}\n\n${message}`
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
core.warning(`Failed to parse report: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (comments.length === 0) {
|
||||
core.info("No diagnostics found to report as comments.");
|
||||
return;
|
||||
}
|
||||
|
||||
core.info(`Successfully prepared ${comments.length} comment${comments.length > 1 ? "s" : ""} to post.`);
|
||||
|
||||
try {
|
||||
await github.rest.pulls.createReview({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: PULL_REQUEST_NUMBER,
|
||||
commit_id: run.head_sha,
|
||||
body: "🤖 Static analysis found the following issues (max count 10):",
|
||||
event: "COMMENT",
|
||||
comments: comments
|
||||
});
|
||||
core.info("Review comments successfully posted to the pull request.");
|
||||
} catch (error) {
|
||||
core.setFailed(`Failed to post review comments to GitHub: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
name: Check Codes Comment
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Check Codes"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'failure'
|
||||
steps:
|
||||
- name: Checkout Script
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
sparse-checkout: |
|
||||
.github/scripts/check-codes-comment.js
|
||||
sparse-checkout-cone-mode: false
|
||||
- name: Download Artifact
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: checkstyle-artifact
|
||||
path: ${{ runner.temp }}/checkstyle-artifact
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Generate Review Comment
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
REPORTS_PATH: ${{ runner.temp }}/checkstyle-artifact/ALL_REPORTS.jsonl
|
||||
PULL_REQUEST_NUMBER_PATH: ${{ runner.temp }}/checkstyle-artifact/PULL_REQUEST_NUMBER
|
||||
with:
|
||||
script: |
|
||||
const script = require("./.github/scripts/check-codes-comment.js");
|
||||
await script({ github, context, core });
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up JDK 17
|
||||
- name: Setup JDK 17
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
@@ -27,3 +27,29 @@ jobs:
|
||||
cache-cleanup: never
|
||||
- name: Check Codes
|
||||
run: ./gradlew checkstyle checkTranslations --no-daemon --parallel --stacktrace
|
||||
- if: failure() && github.event_name == 'pull_request'
|
||||
name: Setup reviewdog
|
||||
uses: reviewdog/action-setup@d8edfce3dd5e1ec6978745e801f9c50b5ef80252 # v1.4.0
|
||||
with:
|
||||
reviewdog_version: v0.21.0
|
||||
- if: failure() && github.event_name == 'pull_request'
|
||||
name: Generate Artifact
|
||||
run: |
|
||||
echo "${{ github.event.number }}" > PULL_REQUEST_NUMBER
|
||||
> ALL_REPORTS.jsonl
|
||||
find . -path "*/build/reports/checkstyle/*.xml" | while read -r xml_file; do
|
||||
if [ -s "$xml_file" ]; then
|
||||
echo "Processing $xml_file..."
|
||||
reviewdog -f=checkstyle -name="checkstyle" -reporter=rdjsonl < "$xml_file" >> ALL_REPORTS.jsonl
|
||||
else
|
||||
echo "Skipping empty or missing report: $xml_file"
|
||||
fi
|
||||
done
|
||||
- if: failure() && github.event_name == 'pull_request'
|
||||
name: Upload Artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: checkstyle-artifact
|
||||
path: |
|
||||
ALL_REPORTS.jsonl
|
||||
PULL_REQUEST_NUMBER
|
||||
@@ -21,8 +21,8 @@ jobs:
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
sparse-checkout: |
|
||||
.github/scripts
|
||||
sparse-checkout-cone-mode: true
|
||||
.github/scripts/pr-size-label.js
|
||||
sparse-checkout-cone-mode: false
|
||||
- name: Label PR by filtered changed lines
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
|
||||
@@ -17,6 +17,7 @@ NVIDIA
|
||||
minecraft-exported-crash-info*
|
||||
hmcl-exported-logs-*
|
||||
terracotta-log-*
|
||||
hmcl-mod-update-list-*
|
||||
*.hmcl-theme
|
||||
/.java/
|
||||
/.local/
|
||||
|
||||
@@ -70,7 +70,7 @@ public class JFXComboBox<T> extends ComboBox<T> {
|
||||
|
||||
private void initialize() {
|
||||
getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
this.setCellFactory(listView -> new JFXListCell<T>() {
|
||||
this.setCellFactory(listView -> new JFXListCell<>() {
|
||||
@Override
|
||||
public void updateItem(T item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
@@ -80,7 +80,7 @@ public class JFXComboBox<T> extends ComboBox<T> {
|
||||
|
||||
// had to refactor the code out of the skin class to allow
|
||||
// customization of the button cell
|
||||
this.setButtonCell(new ListCell<T>() {
|
||||
this.setButtonCell(new ListCell<>() {
|
||||
{
|
||||
// fixed clearing the combo box value is causing
|
||||
// java prompt text to be shown because the button cell is not updated
|
||||
|
||||
@@ -199,6 +199,9 @@ public class JFXListCell<T> extends ListCell<T> {
|
||||
*/
|
||||
@Override
|
||||
protected void updateItem(T item, boolean empty) {
|
||||
T oldItem = getItem();
|
||||
boolean oldEmpty = isEmpty();
|
||||
if (oldItem != item || oldEmpty != empty) cellRippler.releaseRippleImmediately();
|
||||
super.updateItem(item, empty);
|
||||
if (empty) {
|
||||
setText(null);
|
||||
|
||||
@@ -278,6 +278,10 @@ public class JFXRippler extends StackPane {
|
||||
rippler.releaseRipple();
|
||||
}
|
||||
|
||||
public void releaseRippleImmediately() {
|
||||
rippler.releaseRippleImmediately();
|
||||
}
|
||||
|
||||
/**
|
||||
* creates Ripple effect in the center of the control
|
||||
*
|
||||
@@ -405,6 +409,21 @@ public class JFXRippler extends StackPane {
|
||||
}
|
||||
}
|
||||
|
||||
private void releaseRippleImmediately() {
|
||||
Ripple ripple = ripplesQueue.poll();
|
||||
if (ripple != null) {
|
||||
getChildren().remove(ripple);
|
||||
if (generating.getAndSet(false)) {
|
||||
if (overlayRect != null) {
|
||||
overlayRect.inAnimation.stop();
|
||||
if (!forceOverlay) {
|
||||
overlayRect.setOpacity(0D);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void cacheRippleClip(boolean cached) {
|
||||
cacheRipplerClip = cached;
|
||||
}
|
||||
|
||||
@@ -57,8 +57,10 @@ import org.jetbrains.annotations.Nullable;
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.datatransfer.StringSelection;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.lang.management.MemoryPoolMXBean;
|
||||
import java.net.CookieHandler;
|
||||
@@ -68,9 +70,11 @@ import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.SettingsManager.settings;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
@@ -78,6 +82,7 @@ import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/// The main JavaFX application entry point.
|
||||
public final class Launcher extends Application {
|
||||
public static final CookieManager COOKIE_MANAGER = new CookieManager();
|
||||
|
||||
@@ -308,8 +313,8 @@ public final class Launcher extends Application {
|
||||
LOG.info("Current Directory: " + Metadata.CURRENT_DIRECTORY);
|
||||
LOG.info("HMCL User Home: " + Metadata.HMCL_USER_HOME);
|
||||
LOG.info("HMCL Local Home: " + Metadata.HMCL_LOCAL_HOME);
|
||||
LOG.info("HMCL Jar Path: " + Lang.requireNonNullElse(JarUtils.thisJarPath(), "Not Found"));
|
||||
LOG.info("HMCL Log File: " + Lang.requireNonNullElse(LOG.getLogFile(), "In Memory"));
|
||||
LOG.info("HMCL Jar Path: " + Objects.requireNonNullElse(JarUtils.thisJarPath(), "Not Found"));
|
||||
LOG.info("HMCL Log File: " + Objects.requireNonNullElse(LOG.getLogFile(), "In Memory"));
|
||||
LOG.info("JVM Max Memory: " + MEGABYTES.formatBytes(Runtime.getRuntime().maxMemory()));
|
||||
try {
|
||||
for (MemoryPoolMXBean bean : ManagementFactory.getMemoryPoolMXBeans()) {
|
||||
@@ -396,6 +401,8 @@ public final class Launcher extends Application {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets JavaFX VM options before the toolkit starts.
|
||||
/// On Wayland, the UI scale is auto-detected when the user did not configure one explicitly.
|
||||
private static void setupJavaFXVMOptions() {
|
||||
if ("true".equalsIgnoreCase(System.getenv("HMCL_FORCE_GPU"))) {
|
||||
LOG.info("HMCL_FORCE_GPU: true");
|
||||
@@ -451,14 +458,25 @@ public final class Launcher extends Application {
|
||||
}
|
||||
}
|
||||
|
||||
String uiScale = System.getProperty("hmcl.uiScale", System.getenv("HMCL_UI_SCALE"));
|
||||
if (uiScale != null) {
|
||||
uiScale = uiScale.trim();
|
||||
float uiScaleLowerBound;
|
||||
float uiScaleUpperBound;
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
// JavaFX behavior may be abnormal when the DPI scaling factor is too high
|
||||
uiScaleLowerBound = 0.25f;
|
||||
uiScaleUpperBound = 4f;
|
||||
} else {
|
||||
uiScaleLowerBound = 0.01f;
|
||||
uiScaleUpperBound = 10f;
|
||||
}
|
||||
|
||||
String uiScale = System.getProperty("hmcl.uiScale", System.getenv("HMCL_UI_SCALE"));
|
||||
|
||||
float scaleValue = Float.NaN;
|
||||
if (uiScale != null && !(uiScale = uiScale.trim()).isEmpty()) {
|
||||
LOG.info("HMCL_UI_SCALE: " + uiScale);
|
||||
|
||||
try {
|
||||
float scaleValue;
|
||||
if (uiScale.endsWith("%")) {
|
||||
scaleValue = Integer.parseInt(uiScale.substring(0, uiScale.length() - 1)) / 100.0f;
|
||||
} else if (uiScale.endsWith("dpi") || uiScale.endsWith("DPI")) {
|
||||
@@ -466,33 +484,60 @@ public final class Launcher extends Application {
|
||||
} else {
|
||||
scaleValue = Float.parseFloat(uiScale);
|
||||
}
|
||||
|
||||
float lowerBound;
|
||||
float upperBound;
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
// JavaFX behavior may be abnormal when the DPI scaling factor is too high
|
||||
lowerBound = 0.25f;
|
||||
upperBound = 4f;
|
||||
} else {
|
||||
lowerBound = 0.01f;
|
||||
upperBound = 10f;
|
||||
}
|
||||
|
||||
if (scaleValue >= lowerBound && scaleValue <= upperBound) {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
System.getProperties().putIfAbsent("glass.win.uiScale", uiScale);
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
|
||||
LOG.warning("macOS does not support setting UI scale, so it will be ignored");
|
||||
} else {
|
||||
System.getProperties().putIfAbsent("glass.gtk.uiScale", uiScale);
|
||||
}
|
||||
} else {
|
||||
LOG.warning("UI scale out of range: " + uiScale);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Invalid UI scale: " + uiScale);
|
||||
}
|
||||
} else if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
|
||||
String xdgSessionType = Objects.requireNonNullElse(System.getenv("XDG_SESSION_TYPE"), "");
|
||||
String xdgCurrentDesktop = Objects.requireNonNullElse(System.getenv("XDG_CURRENT_DESKTOP"), "");
|
||||
|
||||
if ("wayland".equals(xdgSessionType)
|
||||
&& StringUtils.startsWithIgnoreCase(xdgCurrentDesktop, "KDE")) {
|
||||
try {
|
||||
@Nullable Path xrdb = SystemUtils.which("xrdb");
|
||||
@Nullable String xftDpi = xrdb != null ? SystemUtils.run(List.of(FileUtils.getAbsolutePath(xrdb), "-query"), inputStream -> {
|
||||
@Nullable String dpiLine;
|
||||
|
||||
try (var reader = new InputStreamReader(inputStream, OperatingSystem.NATIVE_CHARSET);
|
||||
BufferedReader bufferedReader = new BufferedReader(reader);
|
||||
Stream<String> lines = bufferedReader.lines()) {
|
||||
dpiLine = lines.filter(line -> line.trim().startsWith("Xft.dpi:"))
|
||||
.findFirst().orElse(null);
|
||||
}
|
||||
|
||||
return dpiLine != null
|
||||
? dpiLine.substring("Xft.dpi:".length()).trim()
|
||||
: null;
|
||||
}, java.time.Duration.ofSeconds(1)) : null;
|
||||
|
||||
if (xftDpi != null) {
|
||||
float dpiValue = Float.parseFloat(xftDpi);
|
||||
float scale = dpiValue / 96f;
|
||||
|
||||
if (scale > 1 && scale <= 10) {
|
||||
LOG.info("Detected Xft.dpi: %s (%.1fx)".formatted(dpiValue, scale));
|
||||
scaleValue = scale;
|
||||
uiScale = Float.toString(scale);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to read Xft.dpi from xrdb", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Float.isFinite(scaleValue)) {
|
||||
if (scaleValue >= uiScaleLowerBound && scaleValue <= uiScaleUpperBound) {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
System.getProperties().putIfAbsent("glass.win.uiScale", uiScale);
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
|
||||
LOG.warning("macOS does not support setting UI scale, so it will be ignored");
|
||||
} else {
|
||||
System.getProperties().putIfAbsent("glass.gtk.uiScale", uiScale);
|
||||
}
|
||||
} else {
|
||||
LOG.warning("UI scale out of range: " + uiScale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.EnumSet;
|
||||
|
||||
@@ -133,4 +134,32 @@ public final class Metadata {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Directory under [HMCL_LOCAL_HOME] that holds a launcher-bundled modpack for automatic install.
|
||||
public static final String BUNDLED_MODPACK_DIRECTORY_NAME = "modpack";
|
||||
|
||||
/// Returns the directory for a launcher-bundled modpack (`[HMCL_LOCAL_HOME]/modpack`).
|
||||
public static Path getBundledModpackDirectory() {
|
||||
return HMCL_LOCAL_HOME.resolve(BUNDLED_MODPACK_DIRECTORY_NAME);
|
||||
}
|
||||
|
||||
/// Returns the bundled modpack package under [getBundledModpackDirectory], if present.
|
||||
///
|
||||
/// Prefers `modpack.zip` over `modpack.mrpack` when both exist. Presence of the package is the
|
||||
/// signal to offer automatic install; the file is removed after a successful install.
|
||||
///
|
||||
/// @return the modpack path, or `null` when no package is present
|
||||
public static @Nullable Path findBundledModpackFile() {
|
||||
Path directory = getBundledModpackDirectory();
|
||||
Path zipModpack = directory.resolve("modpack.zip");
|
||||
if (Files.isRegularFile(zipModpack)) {
|
||||
return zipModpack;
|
||||
}
|
||||
Path mrpackModpack = directory.resolve("modpack.mrpack");
|
||||
if (Files.isRegularFile(mrpackModpack)) {
|
||||
return mrpackModpack;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.jackhuang.hmcl.download.DefaultCacheRepository;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
|
||||
/// Provides HMCL-specific game builders for an HMCL game repository.
|
||||
@NotNullByDefault
|
||||
public class HMCLDependencyManager extends DefaultDependencyManager {
|
||||
/// Creates a dependency manager for a repository and download context.
|
||||
///
|
||||
/// @param repository the associated game repository
|
||||
/// @param downloadProvider the remote download provider
|
||||
/// @param cacheRepository the artifact cache
|
||||
public HMCLDependencyManager(
|
||||
HMCLGameRepository repository,
|
||||
DownloadProvider downloadProvider,
|
||||
DefaultCacheRepository cacheRepository) {
|
||||
super(repository, downloadProvider, cacheRepository);
|
||||
}
|
||||
|
||||
/// {@inheritDoc}
|
||||
@Override
|
||||
public HMCLGameRepository getGameRepository() {
|
||||
return (HMCLGameRepository) super.getGameRepository();
|
||||
}
|
||||
|
||||
/// {@inheritDoc}
|
||||
@Override
|
||||
public HMCLGameBuilder newGameBuilder(GameInstanceID instanceId) {
|
||||
GameInstanceManifest initialManifest = new GameInstanceManifest(instanceId);
|
||||
DefaultGameRepositoryDraft draft = openGameBuilderDraft(initialManifest, null);
|
||||
return new HMCLGameBuilder(this, instanceId, null, draft, initialManifest);
|
||||
}
|
||||
|
||||
/// {@inheritDoc}
|
||||
@Override
|
||||
public HMCLGameBuilder newGameBuilder(GameInstance instance) {
|
||||
validateGameInstance(instance);
|
||||
HMCLGameInstance updateTarget = (HMCLGameInstance) instance;
|
||||
|
||||
GameInstanceManifest initialManifest = new GameInstanceManifest(updateTarget.getId());
|
||||
DefaultGameRepositoryDraft draft = openGameBuilderDraft(initialManifest, updateTarget);
|
||||
return new HMCLGameBuilder(
|
||||
this, updateTarget.getId(), updateTarget, draft, initialManifest);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.jackhuang.hmcl.download.DefaultGameBuilder;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/// Builds game instances and applies HMCL-specific post-installation settings.
|
||||
@NotNullByDefault
|
||||
public class HMCLGameBuilder extends DefaultGameBuilder {
|
||||
/// Creates a builder around an HMCL target already reserved by its dependency manager.
|
||||
///
|
||||
/// @param dependencyManager the dependency manager for the target repository
|
||||
/// @param instanceId the id of the reserved instance
|
||||
/// @param updateTarget the exact update target retained by the draft, or `null` for install
|
||||
/// @param draft the open draft containing `initialManifest`
|
||||
/// @param initialManifest the empty target manifest retained by `draft`
|
||||
HMCLGameBuilder(
|
||||
HMCLDependencyManager dependencyManager,
|
||||
GameInstanceID instanceId,
|
||||
@Nullable HMCLGameInstance updateTarget,
|
||||
DefaultGameRepositoryDraft draft,
|
||||
GameInstanceManifest initialManifest) {
|
||||
super(dependencyManager, instanceId, updateTarget, draft, initialManifest);
|
||||
}
|
||||
|
||||
/// {@inheritDoc}
|
||||
///
|
||||
/// When isolation was enabled, persists the corresponding HMCL instance setting so subsequent
|
||||
/// launches use the instance root. A read-only instance retains its existing setting.
|
||||
@Override
|
||||
protected void onInstanceCommitted(DefaultGameInstance instance) {
|
||||
if (isolationEnabled) {
|
||||
((HMCLGameInstance) instance).enableIsolation();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,856 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectPropertyBase;
|
||||
import javafx.scene.image.Image;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.addon.mod.ModLoaderType;
|
||||
import org.jackhuang.hmcl.java.JavaRuntime;
|
||||
import org.jackhuang.hmcl.modpack.ModpackConfiguration;
|
||||
import org.jackhuang.hmcl.modpack.ModpackProvider;
|
||||
import org.jackhuang.hmcl.setting.*;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.FileSaver;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonSchema;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.SystemInfo;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.SettingsManager.settings;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/// HMCL-specific game instance that owns instance-local settings and run-directory policy.
|
||||
@NotNullByDefault
|
||||
public class HMCLGameInstance extends DefaultGameInstance {
|
||||
|
||||
/// Whether the instance-local game settings file has already been inspected.
|
||||
private boolean gameSettingsLoaded;
|
||||
|
||||
/// Whether the instance-local game settings file cannot be overwritten safely.
|
||||
private boolean gameSettingsReadOnly;
|
||||
|
||||
/// Cached instance-local game settings, or `null` when none exist after loading.
|
||||
private GameSettings.@Nullable Instance gameSettings;
|
||||
|
||||
/// Creates a registered instance bound to the given repository snapshot.
|
||||
///
|
||||
/// @param snapshot the repository snapshot that owns this instance
|
||||
/// @param id the instance id
|
||||
/// @param manifest the stored instance manifest
|
||||
protected HMCLGameInstance(DefaultGameRepositorySnapshot snapshot, GameInstanceID id, GameInstanceManifest manifest) {
|
||||
this(snapshot, id, manifest, (Path) null);
|
||||
}
|
||||
|
||||
/// Creates a registered instance with an optional non-conventional manifest path.
|
||||
///
|
||||
/// @param snapshot the repository snapshot that owns this instance
|
||||
/// @param id the instance id
|
||||
/// @param manifest the stored instance manifest
|
||||
/// @param manifestFile the actual manifest JSON path, or `null` for the layout default
|
||||
protected HMCLGameInstance(
|
||||
DefaultGameRepositorySnapshot snapshot,
|
||||
GameInstanceID id,
|
||||
GameInstanceManifest manifest,
|
||||
@Nullable Path manifestFile) {
|
||||
super(snapshot, id, manifest, manifestFile);
|
||||
}
|
||||
|
||||
/// Creates an instance that shares mutable instance-local state with another instance.
|
||||
///
|
||||
/// Used when the repository clones a snapshot so that settings and the icon property remain
|
||||
/// available on the new wrapper.
|
||||
private HMCLGameInstance(
|
||||
DefaultGameRepositorySnapshot snapshot,
|
||||
GameInstanceID id,
|
||||
GameInstanceManifest manifest,
|
||||
HMCLGameInstance shareState) {
|
||||
super(snapshot, id, manifest, shareState);
|
||||
this.gameSettingsLoaded = shareState.gameSettingsLoaded;
|
||||
this.gameSettingsReadOnly = shareState.gameSettingsReadOnly;
|
||||
this.gameSettings = shareState.gameSettings;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HMCLGameInstance withNewSnapshot(DefaultGameRepositorySnapshot newSnapshot) {
|
||||
return new HMCLGameInstance(newSnapshot, id, manifest, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HMCLGameInstance withManifest(DefaultGameRepositorySnapshot newSnapshot, GameInstanceManifest manifest) {
|
||||
return new HMCLGameInstance(newSnapshot, id, manifest, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HMCLGameRepository getRepository() {
|
||||
return (HMCLGameRepository) super.getRepository();
|
||||
}
|
||||
|
||||
@Override
|
||||
public HMCLGameRepositoryLayout getLayout() {
|
||||
return (HMCLGameRepositoryLayout) super.getLayout();
|
||||
}
|
||||
|
||||
/// Returns the HMCL modpack configuration file for this instance.
|
||||
///
|
||||
/// @return the `modpack.cfg` path in the instance root
|
||||
@Override
|
||||
public Path getModpackConfigurationFile() {
|
||||
return getLayout().getModpackConfigurationFile(getId());
|
||||
}
|
||||
|
||||
/// Returns whether this instance has an HMCL modpack configuration file.
|
||||
///
|
||||
/// @return whether [#getModpackConfigurationFile()] exists
|
||||
public boolean isModpack() {
|
||||
return Files.exists(getModpackConfigurationFile());
|
||||
}
|
||||
|
||||
/// Reads this instance's HMCL modpack configuration.
|
||||
///
|
||||
/// @return the parsed configuration, or `null` when the file does not exist
|
||||
/// @throws IOException if the configuration cannot be read
|
||||
public @Nullable ModpackConfiguration<?> readModpackConfiguration() throws IOException {
|
||||
Path file = getModpackConfigurationFile();
|
||||
if (Files.notExists(file)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JsonUtils.fromJsonFile(file, ModpackConfiguration.class);
|
||||
} catch (JsonParseException e) {
|
||||
throw new IOException("Malformed modpack configuration: " + file, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path getRunDirectory() {
|
||||
return getRepository().computeRunDirectory(getId(), isModpack(), getSettings());
|
||||
}
|
||||
|
||||
/// Returns the loaded instance-local game settings, loading them while this instance is current.
|
||||
///
|
||||
/// @return the settings, or `null` when no local settings exist after loading
|
||||
public @Nullable GameSettings.Instance getSettings() {
|
||||
ensureGameSettingsLoaded();
|
||||
return gameSettings;
|
||||
}
|
||||
|
||||
/// Returns the instance-local game settings, creating an empty settings object when absent.
|
||||
///
|
||||
/// @return the settings, or `null` when the settings file is read-only and no settings are loaded
|
||||
public @Nullable GameSettings.Instance getSettingsOrCreate() {
|
||||
GameSettings.Instance setting = getSettings();
|
||||
if (setting == null) {
|
||||
setting = createSettings();
|
||||
}
|
||||
return setting;
|
||||
}
|
||||
|
||||
/// Resolves this instance's effective settings against its selected parent preset.
|
||||
///
|
||||
/// @return the effective settings
|
||||
public GameSettings.Effective getEffectiveSettings() {
|
||||
@Nullable GameSettings.Instance setting = getSettings();
|
||||
return GameSettings.resolve(getRepository().getParentGameSettings(setting), setting);
|
||||
}
|
||||
|
||||
/// Creates empty instance-local game settings when none are loaded.
|
||||
///
|
||||
/// @return the settings, or `null` when settings are read-only or already present in a non-creatable state
|
||||
public @Nullable GameSettings.Instance createSettings() {
|
||||
ensureGameSettingsLoaded();
|
||||
if (gameSettingsReadOnly) {
|
||||
return null;
|
||||
}
|
||||
if (gameSettings != null) {
|
||||
return gameSettings;
|
||||
}
|
||||
return initSettings(new GameSettings.Instance(), true);
|
||||
}
|
||||
|
||||
/// Returns whether the instance-local game settings file cannot be overwritten safely.
|
||||
///
|
||||
/// @return whether the settings are loaded in read-only mode
|
||||
public boolean isSettingsReadOnly() {
|
||||
ensureGameSettingsLoaded();
|
||||
return gameSettingsReadOnly;
|
||||
}
|
||||
|
||||
/// Enables instance-local running-directory selection for this instance.
|
||||
///
|
||||
/// A blank local running directory resolves to the instance root. This operation is idempotent
|
||||
/// and schedules a settings save only when it adds the override. It leaves the instance
|
||||
/// unchanged when its local settings cannot be written safely.
|
||||
public void enableIsolation() {
|
||||
if (isSettingsReadOnly()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@Nullable GameSettings.Instance setting = getSettingsOrCreate();
|
||||
if (setting != null
|
||||
&& setting.getOverrideProperties().add(GameSettings.PROPERTY_RUNNING_DIRECTORY)) {
|
||||
saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
/// Backs up and overwrites the settings file when this instance still owns its settings.
|
||||
public void forceOverwriteSettings() {
|
||||
ensureGameSettingsLoaded();
|
||||
if (!ownsGameSettings()) {
|
||||
return;
|
||||
}
|
||||
|
||||
GameSettings.Instance setting = gameSettings;
|
||||
if (setting == null) {
|
||||
setting = new GameSettings.Instance();
|
||||
gameSettings = setting;
|
||||
gameSettingsLoaded = true;
|
||||
}
|
||||
|
||||
boolean installAutoSave = !setting.isSavable();
|
||||
Path file = getGameSettingsFile().toAbsolutePath().normalize();
|
||||
SettingFileUtils.backupInvalidConfig(file);
|
||||
setting.setSchema(GameSettings.Instance.CURRENT_SCHEMA);
|
||||
setting.setSavable(true);
|
||||
setting.setBackupOnNextSave(false);
|
||||
gameSettingsReadOnly = false;
|
||||
saveSettings();
|
||||
if (installAutoSave) {
|
||||
setting.addListener(a -> saveSettings());
|
||||
}
|
||||
}
|
||||
|
||||
/// Saves the settings asynchronously when writable and still owned by the current instance.
|
||||
public void saveSettings() {
|
||||
if (gameSettings == null || gameSettingsReadOnly || !ownsGameSettings()) {
|
||||
return;
|
||||
}
|
||||
|
||||
GameSettings.Instance setting = gameSettings;
|
||||
Path file = getGameSettingsFile().toAbsolutePath().normalize();
|
||||
try {
|
||||
Files.createDirectories(file.getParent());
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to create directory: " + file.getParent(), e);
|
||||
}
|
||||
|
||||
if (setting.isBackupOnNextSave()) {
|
||||
setting.setBackupOnNextSave(false);
|
||||
SettingFileUtils.backupInvalidConfig(file);
|
||||
}
|
||||
FileSaver.save(file, LauncherSettings.SETTINGS_GSON.toJson(setting));
|
||||
}
|
||||
|
||||
/// Saves the settings synchronously when writable and still owned by the current instance.
|
||||
///
|
||||
/// @throws IOException if saving the file fails
|
||||
public void saveSettingsSync() throws IOException {
|
||||
if (gameSettings == null || gameSettingsReadOnly || !ownsGameSettings()) {
|
||||
return;
|
||||
}
|
||||
|
||||
GameSettings.Instance setting = gameSettings;
|
||||
Path file = getGameSettingsFile().toAbsolutePath().normalize();
|
||||
Files.createDirectories(file.getParent());
|
||||
if (setting.isBackupOnNextSave()) {
|
||||
setting.setBackupOnNextSave(false);
|
||||
SettingFileUtils.backupInvalidConfig(file);
|
||||
}
|
||||
FileUtils.saveSafely(file, LauncherSettings.SETTINGS_GSON.toJson(setting));
|
||||
}
|
||||
|
||||
/// Initializes this instance with the given settings object.
|
||||
///
|
||||
/// @param setting the settings to install
|
||||
/// @param allowSave whether the settings may be written back to disk
|
||||
/// @return the installed settings
|
||||
public GameSettings.Instance initSettings(GameSettings.Instance setting, boolean allowSave) {
|
||||
normalizeRunningDirectoryOverride(setting);
|
||||
setting.setSavable(allowSave);
|
||||
gameSettingsLoaded = true;
|
||||
gameSettings = setting;
|
||||
setting.iconProperty().addListener(observable -> invalidateIconImage());
|
||||
if (allowSave) {
|
||||
gameSettingsReadOnly = false;
|
||||
setting.addListener(a -> saveSettings());
|
||||
} else {
|
||||
gameSettingsReadOnly = true;
|
||||
}
|
||||
return setting;
|
||||
}
|
||||
|
||||
/// Returns a deep copy of the currently loaded settings, or a new settings object that inherits
|
||||
/// the effective parent preset when no local settings exist.
|
||||
///
|
||||
/// @return a detached copy suitable for installing into another instance
|
||||
public GameSettings.Instance copySettings() {
|
||||
@Nullable GameSettings.Instance setting = getSettings();
|
||||
if (setting != null) {
|
||||
return JsonUtils.clone(LauncherSettings.SETTINGS_GSON, setting, TypeToken.get(GameSettings.Instance.class));
|
||||
}
|
||||
|
||||
GameSettings.Instance copied = new GameSettings.Instance();
|
||||
copied.parentProperty().setValue(
|
||||
getEffectiveSettings().getPreset().idProperty().getValue());
|
||||
return copied;
|
||||
}
|
||||
|
||||
/// Returns the first custom icon file found in this instance's root directory.
|
||||
///
|
||||
/// @return the icon file, or empty when no supported icon file exists
|
||||
public @Nullable Path getIconFile() {
|
||||
for (String extension : FXUtils.IMAGE_EXTENSIONS) {
|
||||
Path file = getInstanceRoot().resolve("icon." + extension);
|
||||
if (Files.exists(file)) {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Replaces this instance's custom icon file.
|
||||
///
|
||||
/// Existing supported icon files are removed before `iconFile` is copied.
|
||||
///
|
||||
/// @param iconFile the source icon file
|
||||
/// @throws IOException if the icon cannot be copied
|
||||
/// @throws IllegalArgumentException if the file extension is unsupported
|
||||
public void setIconFile(Path iconFile) throws IOException {
|
||||
String extension = FileUtils.getExtension(iconFile).toLowerCase(Locale.ROOT);
|
||||
if (!FXUtils.IMAGE_EXTENSIONS.contains(extension)) {
|
||||
throw new IllegalArgumentException("Unsupported icon file: " + extension);
|
||||
}
|
||||
|
||||
clearIconFiles();
|
||||
FileUtils.copyFile(iconFile, getInstanceRoot().resolve("icon." + extension));
|
||||
invalidateIconImage();
|
||||
}
|
||||
|
||||
/// Deletes all supported custom icon files for this instance.
|
||||
///
|
||||
/// Individual deletion failures are logged and do not stop later files from being attempted.
|
||||
public void deleteIconFile() {
|
||||
clearIconFiles();
|
||||
invalidateIconImage();
|
||||
}
|
||||
|
||||
private void clearIconFiles() {
|
||||
for (String extension : FXUtils.IMAGE_EXTENSIONS) {
|
||||
Path file = getInstanceRoot().resolve("icon." + extension);
|
||||
try {
|
||||
Files.deleteIfExists(file);
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to delete instance icon file: " + file, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Soft-cached icon image for this instance id.
|
||||
///
|
||||
/// Shared across COW snapshot wrappers. The computed [Image] is retained only via a
|
||||
/// [WeakReference], so it can be reclaimed under memory pressure when nothing else holds it.
|
||||
private @Nullable WeakCachedIconImageProperty iconImage;
|
||||
|
||||
/// Returns the observable icon image for this instance.
|
||||
///
|
||||
/// The image is stored in a [WeakReference] cache: when nothing else strongly references it
|
||||
/// (for example no UI node is displaying it), the JVM may reclaim the [Image] under memory
|
||||
/// pressure. The next [#getIconImage] reloads it.
|
||||
///
|
||||
/// @return the icon image property
|
||||
public ReadOnlyObjectProperty<Image> iconImageProperty() {
|
||||
if (iconImage == null) {
|
||||
iconImage = new WeakCachedIconImageProperty();
|
||||
}
|
||||
return iconImage;
|
||||
}
|
||||
|
||||
/// Returns the icon image selected for this instance.
|
||||
///
|
||||
/// Equivalent to [ReadOnlyObjectProperty#get()] on [#iconImageProperty].
|
||||
///
|
||||
/// @return the selected or derived icon image
|
||||
public Image getIconImage() {
|
||||
return iconImageProperty().get();
|
||||
}
|
||||
|
||||
/// Drops the soft-cached icon image and notifies observers.
|
||||
public void invalidateIconImage() {
|
||||
((WeakCachedIconImageProperty) iconImageProperty()).invalidate();
|
||||
}
|
||||
|
||||
/// Soft-cached read-only icon property compatible with JavaFX versions before 19.
|
||||
private final class WeakCachedIconImageProperty extends ReadOnlyObjectPropertyBase<Image> {
|
||||
private @Nullable WeakReference<Image> cache;
|
||||
|
||||
@Override
|
||||
public Object getBean() {
|
||||
return HMCLGameInstance.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "iconImage";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Image get() {
|
||||
WeakReference<Image> current = cache;
|
||||
Image image = current != null ? current.get() : null;
|
||||
if (image != null) {
|
||||
return image;
|
||||
}
|
||||
|
||||
image = computeIconImage();
|
||||
cache = new WeakReference<>(image);
|
||||
return image;
|
||||
}
|
||||
|
||||
/// Computes the icon image from settings, custom files, and the launch manifest.
|
||||
///
|
||||
/// @return the selected or derived icon image
|
||||
private Image computeIconImage() {
|
||||
@Nullable GameSettings.Instance setting = getSettings();
|
||||
GameInstanceIconType iconType = setting != null
|
||||
? Lang.requireNonNullElse(setting.iconProperty().getValue(), GameInstanceIconType.DEFAULT)
|
||||
: GameInstanceIconType.DEFAULT;
|
||||
if (iconType != GameInstanceIconType.DEFAULT) {
|
||||
return iconType.getIcon();
|
||||
}
|
||||
|
||||
@Nullable Path iconFile = getIconFile();
|
||||
if (iconFile != null) {
|
||||
try {
|
||||
return FXUtils.loadImage(iconFile, 64, 64, true, true);
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to load instance icon for " + getId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
for (ModLoaderType modLoader : getModLoaders()) {
|
||||
return GameInstanceIconType.getIconType(modLoader).getIcon();
|
||||
}
|
||||
|
||||
if (hasComponent(GameComponentType.OPTIFINE))
|
||||
return GameInstanceIconType.OPTIFINE.getIcon();
|
||||
|
||||
GameVersionNumber version = getVersion();
|
||||
if (version.isAprilFools())
|
||||
return GameInstanceIconType.APRIL_FOOLS.getIcon();
|
||||
else if (version instanceof GameVersionNumber.LegacySnapshot || version instanceof GameVersionNumber.Release release && release.getEaType() != GameVersionNumber.Release.ReleaseType.GA)
|
||||
return GameInstanceIconType.COMMAND.getIcon();
|
||||
else if (version instanceof GameVersionNumber.Old)
|
||||
return GameInstanceIconType.CRAFT_TABLE.getIcon();
|
||||
else
|
||||
return GameInstanceIconType.GRASS.getIcon();
|
||||
}
|
||||
|
||||
/// Clears the weak cache and notifies listeners.
|
||||
void invalidate() {
|
||||
cache = null;
|
||||
fireValueChangedEvent();
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates the marker indicating that the most recent launch ended abnormally.
|
||||
public void markLaunchedAbnormally() {
|
||||
try {
|
||||
Files.createFile(getInstanceRoot().resolve(".abnormal"));
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes the abnormal-launch marker when present.
|
||||
///
|
||||
/// @return whether a regular marker file was present
|
||||
public boolean unmarkLaunchedAbnormally() {
|
||||
Path file = getInstanceRoot().resolve(".abnormal");
|
||||
if (!Files.isRegularFile(file)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
Files.delete(file);
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to delete abnormal launch marker: " + file, e);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Loads settings once while this object is the repository's current instance.
|
||||
private void ensureGameSettingsLoaded() {
|
||||
if (!gameSettingsLoaded && getRepository().findInstance(id) == this) {
|
||||
loadGameSettings();
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether this instance still owns the settings object used by the current snapshot.
|
||||
private boolean ownsGameSettings() {
|
||||
@Nullable HMCLGameInstance current = getRepository().findInstance(id);
|
||||
boolean owns = current == this
|
||||
|| current != null && gameSettings != null && current.gameSettings == gameSettings;
|
||||
if (!owns && gameSettings != null) {
|
||||
gameSettings.setSavable(false);
|
||||
}
|
||||
return owns;
|
||||
}
|
||||
|
||||
private void loadGameSettings() {
|
||||
gameSettingsLoaded = true;
|
||||
LoadResult result = loadGameSettingsFile(getGameSettingsFile());
|
||||
if (result.setting() != null) {
|
||||
initSettings(result.setting(), result.allowSave());
|
||||
return;
|
||||
}
|
||||
if (!result.allowSave()) {
|
||||
gameSettingsReadOnly = true;
|
||||
return;
|
||||
}
|
||||
|
||||
@Nullable GameSettingsPresetID legacyParent = getRepository().getGameDirectory().getLegacyGameSettings();
|
||||
if (SettingsManager.getGameSettings(legacyParent) == null) {
|
||||
legacyParent = null;
|
||||
}
|
||||
|
||||
LegacyGameSettingsMigrator.InstanceMigrationResult migrationResult =
|
||||
LegacyGameSettingsMigrator.migrateInstanceGameSettings(
|
||||
getRepository(), id, legacyParent);
|
||||
if (migrationResult != null) {
|
||||
initSettings(migrationResult.setting(), true);
|
||||
try {
|
||||
saveSettingsSync();
|
||||
migrationResult.saveReceipt();
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to save migrated instance game settings for " + id, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Path getGameSettingsFile() {
|
||||
return getLayout().getInstanceGameSettingsFile(id);
|
||||
}
|
||||
|
||||
public LaunchOptions.Builder getLaunchOptions(JavaRuntime javaVersion, Path gameDir, List<String> javaAgents, List<String> javaArguments, boolean makeLaunchScript) {
|
||||
GameSettings.Effective vs = getEffectiveSettings();
|
||||
boolean noJVMOptions = vs.getInheritable(GameSettings::noJVMOptionsProperty);
|
||||
boolean autoMemory = vs.getInheritable(GameSettings::autoMemoryProperty);
|
||||
GameVersionNumber gameVersionNumber = getVersion();
|
||||
|
||||
@Nullable Integer maxMemory;
|
||||
if (autoMemory) {
|
||||
maxMemory = noJVMOptions
|
||||
? null
|
||||
: Math.toIntExact(HMCLGameRepository.getAutoAllocatedMemory(
|
||||
SystemInfo.getPhysicalMemoryStatus().available(),
|
||||
javaVersion.getPlatform()) / 1024L / 1024L);
|
||||
} else {
|
||||
maxMemory = vs.getMaxMemory();
|
||||
}
|
||||
|
||||
LaunchOptions.Builder builder = new LaunchOptions.Builder()
|
||||
.setInstanceId(getId())
|
||||
.setGameDir(gameDir)
|
||||
.setJava(javaVersion)
|
||||
.setVersionType(Metadata.TITLE)
|
||||
.setVersionName(getId().id())
|
||||
.setProfileName(Metadata.TITLE)
|
||||
.setGameArguments(StringUtils.tokenize(vs.getInheritable(GameSettings::gameArgumentsProperty)))
|
||||
.setOverrideJavaArguments(StringUtils.tokenize(vs.getInheritable(GameSettings::jvmOptionsProperty)))
|
||||
.setMaxMemory(maxMemory)
|
||||
.setMinMemory(vs.getInheritable(GameSettings::minMemoryProperty))
|
||||
.setMetaspace(Lang.toIntOrNull(vs.getInheritable(GameSettings::permSizeProperty)))
|
||||
.setEnvironmentVariables(
|
||||
Lang.mapOf(StringUtils.tokenize(vs.getInheritable(GameSettings::environmentVariablesProperty))
|
||||
.stream()
|
||||
.map(it -> {
|
||||
int idx = it.indexOf('=');
|
||||
return idx >= 0 ? pair(it.substring(0, idx), it.substring(idx + 1)) : pair(it, "");
|
||||
})
|
||||
.collect(Collectors.toList())
|
||||
)
|
||||
)
|
||||
.setWidth(vs.getWidth())
|
||||
.setHeight(vs.getHeight())
|
||||
.setFullscreen(vs.getInheritable(GameSettings::windowTypeProperty) == GameWindowType.FULLSCREEN)
|
||||
.setWrapper(vs.getInheritable(GameSettings::commandWrapperProperty))
|
||||
.setProxyOption(getProxyOption())
|
||||
.setPreLaunchCommand(vs.getInheritable(GameSettings::preLaunchCommandProperty))
|
||||
.setPostExitCommand(vs.getInheritable(GameSettings::postExitCommandProperty))
|
||||
.setNoGeneratedJVMArgs(noJVMOptions)
|
||||
.setNoGeneratedOptimizingJVMArgs(vs.getInheritable(GameSettings::noOptimizingJVMOptionsProperty))
|
||||
.setUseCustomNatives(vs.getInheritable(GameSettings::useCustomNativesProperty))
|
||||
.setNativesDir(vs.getInheritable(GameSettings::nativesDirectoryProperty))
|
||||
.setProcessPriority(vs.getInheritable(GameSettings::processPriorityProperty))
|
||||
.setGraphicsBackend(vs.getInheritable(GameSettings::graphicsBackendProperty))
|
||||
.setRenderer(vs.getRenderer(gameVersionNumber))
|
||||
.setEnableDebugLogOutput(vs.getInheritable(GameSettings::enableDebugLogOutputProperty))
|
||||
.setAllowAutoAgent(vs.getInheritable(GameSettings::allowAutoAgentProperty))
|
||||
.setDisableAutoGameOptions(vs.getInheritable(GameSettings::disableAutoGameOptionsProperty))
|
||||
.setUseNativeGLFWorSDL(vs.getInheritable(GameSettings::useNativeGLFWorSDLProperty))
|
||||
.setUseNativeOpenAL(vs.getInheritable(GameSettings::useNativeOpenALProperty))
|
||||
.setUseHighPerformanceGPU(vs.getInheritable(GameSettings::highPerformanceProperty))
|
||||
.setDaemon(!makeLaunchScript && vs.getInheritable(GameSettings::launcherVisibilityProperty).isDaemon())
|
||||
.setJavaAgents(javaAgents)
|
||||
.setJavaArguments(javaArguments);
|
||||
|
||||
QuickPlayOption quickPlayOption = vs.getQuickPlayOption();
|
||||
if (quickPlayOption != null) {
|
||||
builder.setQuickPlayOption(quickPlayOption);
|
||||
}
|
||||
|
||||
Path json = getModpackConfigurationFile();
|
||||
if (Files.exists(json)) {
|
||||
try {
|
||||
String jsonText = Files.readString(json);
|
||||
ModpackConfiguration<?> modpackConfiguration = JsonUtils.GSON.fromJson(jsonText, ModpackConfiguration.class);
|
||||
ModpackProvider provider = ModpackHelper.getProviderByType(modpackConfiguration.getType());
|
||||
if (provider != null) provider.injectLaunchOptions(jsonText, builder);
|
||||
} catch (IOException | JsonParseException e) {
|
||||
LOG.warning("Failed to parse modpack configuration file " + json, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (autoMemory && builder.getJavaArguments().stream().anyMatch(it -> it.startsWith("-Xmx")))
|
||||
builder.setMaxMemory(null);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static ProxyOption getProxyOption() {
|
||||
return switch (settings().proxyTypeProperty().get()) {
|
||||
case SYSTEM -> ProxyOption.Default.INSTANCE;
|
||||
case DIRECT -> ProxyOption.Direct.INSTANCE;
|
||||
case HTTP, SOCKS -> {
|
||||
String proxyHost = settings().proxyHostProperty().get();
|
||||
int proxyPort = settings().proxyPortProperty().get();
|
||||
|
||||
if (StringUtils.isBlank(proxyHost) || proxyPort < 0 || proxyPort > 0xFFFF) {
|
||||
yield ProxyOption.Default.INSTANCE;
|
||||
}
|
||||
|
||||
String proxyUser = settings().proxyUserProperty().get();
|
||||
String proxyPass = settings().proxyPasswordProperty().get();
|
||||
|
||||
if (StringUtils.isBlank(proxyUser)) {
|
||||
proxyUser = null;
|
||||
proxyPass = null;
|
||||
} else if (proxyPass == null) {
|
||||
proxyPass = "";
|
||||
}
|
||||
|
||||
if (settings().proxyTypeProperty().get() == ProxyType.HTTP) {
|
||||
yield new ProxyOption.Http(proxyHost, proxyPort, proxyUser, proxyPass);
|
||||
} else {
|
||||
yield new ProxyOption.Socks(proxyHost, proxyPort, proxyUser, proxyPass);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Loads a new-format instance game settings file.
|
||||
private static LoadResult loadGameSettingsFile(Path file) {
|
||||
if (!Files.exists(file)) {
|
||||
return new LoadResult(null, true);
|
||||
}
|
||||
|
||||
try {
|
||||
JsonObject jsonObject = JsonUtils.fromJsonFile(LauncherSettings.SETTINGS_GSON, file, JsonObject.class);
|
||||
if (jsonObject == null) {
|
||||
LOG.warning("Instance game settings are empty: " + file);
|
||||
GameSettings.Instance fallback = new GameSettings.Instance();
|
||||
return new LoadResult(fallback, true);
|
||||
}
|
||||
|
||||
JsonSchema.CompatibilityResult schemaResult =
|
||||
JsonSchema.check(jsonObject, GameSettings.Instance.CURRENT_SCHEMA);
|
||||
switch (schemaResult.status()) {
|
||||
case MISSING -> LOG.warning("Missing schema in instance game settings: " + file);
|
||||
case INVALID -> LOG.warning("Invalid schema in instance game settings: %s, Actual: %s".formatted(file, schemaResult.invalidValue()));
|
||||
case UNPARSEABLE -> LOG.warning("Unparseable schema in instance game settings: %s, Actual: %s".formatted(file, schemaResult.actual()));
|
||||
case UNEXPECTED_ID -> LOG.warning("Unexpected instance game settings schema. Expected: %s, Actual: %s".formatted(GameSettings.Instance.CURRENT_SCHEMA, schemaResult.actual()));
|
||||
case UNSUPPORTED_MAJOR, READ_ONLY_PRESERVE_SCHEMA -> LOG.warning("Unsupported instance game settings schema. Expected: %s, Actual: %s".formatted(GameSettings.Instance.CURRENT_SCHEMA, schemaResult.actual()));
|
||||
case READ_WRITE, READ_WRITE_PRESERVE_SCHEMA -> {
|
||||
}
|
||||
}
|
||||
if (!schemaResult.readable()) {
|
||||
GameSettings.Instance fallback = new GameSettings.Instance();
|
||||
fallback.setSavable(false);
|
||||
return new LoadResult(fallback, false);
|
||||
}
|
||||
|
||||
GameSettings.@Nullable Instance setting =
|
||||
LauncherSettings.SETTINGS_GSON.fromJson(jsonObject, GameSettings.Instance.class);
|
||||
if (setting == null) {
|
||||
LOG.warning("Instance game settings deserialized to null: " + file);
|
||||
GameSettings.Instance fallback = new GameSettings.Instance();
|
||||
fallback.setBackupOnNextSave(true);
|
||||
return new LoadResult(fallback, true);
|
||||
}
|
||||
if (!schemaResult.preserveSchema() && !GameSettings.Instance.CURRENT_SCHEMA.equals(setting.getSchema())) {
|
||||
setting.setSchema(GameSettings.Instance.CURRENT_SCHEMA);
|
||||
}
|
||||
return new LoadResult(setting, schemaResult.allowSave());
|
||||
} catch (JsonParseException ex) {
|
||||
LOG.warning("Failed to parse game setting " + file, ex);
|
||||
GameSettings.Instance fallback = new GameSettings.Instance();
|
||||
fallback.setBackupOnNextSave(true);
|
||||
return new LoadResult(fallback, true);
|
||||
} catch (Exception ex) {
|
||||
LOG.warning("Failed to load game setting " + file, ex);
|
||||
return new LoadResult(null, false);
|
||||
}
|
||||
}
|
||||
|
||||
/// Keeps old local custom running directories effective under the new source-selection model.
|
||||
private static void normalizeRunningDirectoryOverride(GameSettings.Instance setting) {
|
||||
if (StringUtils.isNotBlank(setting.runningDirectoryProperty().getValue())) {
|
||||
setting.getOverrideProperties().add(GameSettings.PROPERTY_RUNNING_DIRECTORY);
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of loading an instance-specific game settings file.
|
||||
///
|
||||
/// @param setting the loaded instance settings, or `null` when unavailable
|
||||
/// @param allowSave whether the file may be overwritten
|
||||
private record LoadResult(@Nullable GameSettings.Instance setting, boolean allowSave) {
|
||||
}
|
||||
|
||||
/// Optional reference to an HMCL game instance bound to a repository.
|
||||
///
|
||||
/// Replaces the former `(repository, instanceId)` pair for UI and service context that may or
|
||||
/// may not have a selected instance. When present, [#instance()] is a snapshot member and may
|
||||
/// become stale after the repository publishes a new snapshot; call [#refreshed()] to re-resolve
|
||||
/// from the current snapshot while preserving repository context.
|
||||
@NotNullByDefault
|
||||
public static final class Optional {
|
||||
private final HMCLGameRepository repository;
|
||||
private final @Nullable HMCLGameInstance instance;
|
||||
|
||||
/// Creates an empty optional bound only to a repository.
|
||||
///
|
||||
/// @param repository the repository
|
||||
public Optional(HMCLGameRepository repository) {
|
||||
this.repository = Objects.requireNonNull(repository);
|
||||
this.instance = null;
|
||||
}
|
||||
|
||||
/// Creates an optional that holds the given instance.
|
||||
///
|
||||
/// @param instance the instance
|
||||
public Optional(HMCLGameInstance instance) {
|
||||
this.repository = instance.getRepository();
|
||||
this.instance = instance;
|
||||
}
|
||||
|
||||
/// Creates an optional by resolving `instanceId` from the repository's current snapshot.
|
||||
///
|
||||
/// @param repository the repository
|
||||
/// @param instanceId the instance id, or `null` for an empty optional
|
||||
/// @return an optional that is empty when `instanceId` is null or not registered
|
||||
public static Optional of(HMCLGameRepository repository, @Nullable GameInstanceID instanceId) {
|
||||
if (instanceId == null) {
|
||||
return new Optional(repository);
|
||||
}
|
||||
HMCLGameInstance instance = repository.findInstance(instanceId);
|
||||
return instance != null ? new Optional(instance) : new Optional(repository);
|
||||
}
|
||||
|
||||
/// Creates an optional that holds the given instance.
|
||||
///
|
||||
/// @param instance the instance
|
||||
/// @return the optional
|
||||
public static Optional of(HMCLGameInstance instance) {
|
||||
return new Optional(instance);
|
||||
}
|
||||
|
||||
/// Creates an empty optional bound only to a repository.
|
||||
///
|
||||
/// @param repository the repository
|
||||
/// @return the empty optional
|
||||
public static Optional empty(HMCLGameRepository repository) {
|
||||
return new Optional(repository);
|
||||
}
|
||||
|
||||
/// Returns the repository associated with this optional.
|
||||
///
|
||||
/// @return the repository
|
||||
public HMCLGameRepository repository() {
|
||||
return repository;
|
||||
}
|
||||
|
||||
/// Returns the held instance, if any.
|
||||
///
|
||||
/// @return the instance, or `null` when empty
|
||||
@Contract(pure = true)
|
||||
public @Nullable HMCLGameInstance instance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
/// Returns the held instance id, if any.
|
||||
///
|
||||
/// @return the instance id, or `null` when empty
|
||||
@Contract(pure = true)
|
||||
public @Nullable GameInstanceID instanceId() {
|
||||
return instance != null ? instance.getId() : null;
|
||||
}
|
||||
|
||||
/// Returns whether an instance is present.
|
||||
///
|
||||
/// @return whether [#instance()] is non-null
|
||||
public boolean isPresent() {
|
||||
return instance != null;
|
||||
}
|
||||
|
||||
/// Returns whether no instance is present.
|
||||
///
|
||||
/// @return whether [#instance()] is null
|
||||
public boolean isEmpty() {
|
||||
return instance == null;
|
||||
}
|
||||
|
||||
/// Re-resolves the held instance id from the repository's current snapshot.
|
||||
///
|
||||
/// @return this optional when empty; otherwise a fresh optional for the same id
|
||||
public Optional refreshed() {
|
||||
if (instance == null) {
|
||||
return this;
|
||||
}
|
||||
return of(repository, instance.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,16 +42,27 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
*/
|
||||
public final class HMCLGameLauncher extends DefaultLauncher {
|
||||
|
||||
public HMCLGameLauncher(GameRepository repository, GameInstanceManifest manifest, AuthInfo authInfo, LaunchOptions options) {
|
||||
this(repository, manifest, authInfo, options, null);
|
||||
/// Creates a launcher with daemon process monitors.
|
||||
///
|
||||
/// @param instance the instance being launched
|
||||
/// @param manifest the effective launch-time manifest
|
||||
/// @param authInfo authentication information for the game process
|
||||
/// @param options launch options
|
||||
/// @param listener process listener, or `null` to inherit IO
|
||||
public HMCLGameLauncher(GameInstance instance, GameInstanceManifest manifest, AuthInfo authInfo, LaunchOptions options, ProcessListener listener) {
|
||||
this(instance, manifest, authInfo, options, listener, true);
|
||||
}
|
||||
|
||||
public HMCLGameLauncher(GameRepository repository, GameInstanceManifest manifest, AuthInfo authInfo, LaunchOptions options, ProcessListener listener) {
|
||||
this(repository, manifest, authInfo, options, listener, true);
|
||||
}
|
||||
|
||||
public HMCLGameLauncher(GameRepository repository, GameInstanceManifest manifest, AuthInfo authInfo, LaunchOptions options, ProcessListener listener, boolean daemon) {
|
||||
super(repository, manifest, authInfo, options, listener, daemon);
|
||||
/// Creates a launcher for the given instance and launch plan.
|
||||
///
|
||||
/// @param instance the instance being launched
|
||||
/// @param manifest the effective launch-time manifest
|
||||
/// @param authInfo authentication information for the game process
|
||||
/// @param options launch options
|
||||
/// @param listener process listener, or `null` to inherit IO
|
||||
/// @param daemon whether monitors should be daemon threads
|
||||
public HMCLGameLauncher(GameInstance instance, GameInstanceManifest manifest, AuthInfo authInfo, LaunchOptions options, ProcessListener listener, boolean daemon) {
|
||||
super(instance, manifest, authInfo, options, listener, daemon);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -66,7 +77,7 @@ public final class HMCLGameLauncher extends DefaultLauncher {
|
||||
if (options.isDisableAutoGameOptions())
|
||||
return;
|
||||
|
||||
Path runDir = repository.getRunDirectory(manifest.id());
|
||||
Path runDir = instance.getRunDirectory();
|
||||
Path optionsFile = runDir.resolve("options.txt");
|
||||
Path configFolder = runDir.resolve("config");
|
||||
|
||||
@@ -91,8 +102,8 @@ public final class HMCLGameLauncher extends DefaultLauncher {
|
||||
* 1.11 ~ 1.12 : zh_cn works fine, zh_CN will display Chinese but the language setting will incorrectly show English as selected
|
||||
* 1.13+ : zh_cn works fine, zh_CN will automatically switch to English
|
||||
*/
|
||||
GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(repository.getGameVersion(manifest));
|
||||
if (gameVersion.compareTo("1.1") < 0)
|
||||
GameVersionNumber gameVersion = instance.getVersion();
|
||||
if (gameVersion == GameVersionNumber.unknown() || gameVersion.compareTo("1.1") < 0)
|
||||
return;
|
||||
|
||||
String lang = normalizedLanguageTag(locale, gameVersion);
|
||||
@@ -180,7 +191,7 @@ public final class HMCLGameLauncher extends DefaultLauncher {
|
||||
Library library = new Library(new Artifact("org.glavo", "lwjgl-unsafe-agent", agentVersion));
|
||||
String fileName = library.artifact().getFileName();
|
||||
|
||||
Path agentPath = repository.getLibraryFile(manifest, library).toAbsolutePath().normalize();
|
||||
Path agentPath = instance.getLayout().getLibraryFile(instance.getId(), library).toAbsolutePath().normalize();
|
||||
if (agentPath.toString().contains("=")) {
|
||||
throw new IOException("Invalid library path: " + agentPath);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
@NotNullByDefault
|
||||
public final class HMCLGameRepositoryLayout extends DefaultGameRepositoryLayout {
|
||||
/// Directory under the instance root that stores HMCL-managed instance metadata.
|
||||
private static final String INSTANCE_METADATA_DIRECTORY = ".hmcl";
|
||||
|
||||
/// Directory under the instance metadata directory that stores instance configuration files.
|
||||
private static final String INSTANCE_CONFIG_DIRECTORY = "config";
|
||||
|
||||
/// Directory under the instance metadata directory that stores instance state files.
|
||||
private static final String INSTANCE_STATE_DIRECTORY = "state";
|
||||
|
||||
/// Current file name for instance-specific game settings.
|
||||
private static final String INSTANCE_GAME_SETTINGS_FILENAME = "instance-game-settings.json";
|
||||
|
||||
public HMCLGameRepositoryLayout(Path baseDirectory) {
|
||||
super(baseDirectory);
|
||||
}
|
||||
|
||||
/// Returns the HMCL-managed metadata directory under the instance root.
|
||||
///
|
||||
/// This directory stores instance-scoped files owned by HMCL.
|
||||
public Path getInstanceMetadataDirectory(GameInstanceID instanceId) {
|
||||
return getInstanceRoot(instanceId).resolve(INSTANCE_METADATA_DIRECTORY);
|
||||
}
|
||||
|
||||
/// Returns the HMCL-managed configuration directory under the instance metadata directory.
|
||||
public Path getInstanceConfigDirectory(GameInstanceID instanceId) {
|
||||
return getInstanceMetadataDirectory(instanceId).resolve(INSTANCE_CONFIG_DIRECTORY);
|
||||
}
|
||||
|
||||
/// Returns the HMCL-managed state directory under the instance metadata directory.
|
||||
public Path getInstanceStateDirectory(GameInstanceID instanceId) {
|
||||
return getInstanceMetadataDirectory(instanceId).resolve(INSTANCE_STATE_DIRECTORY);
|
||||
}
|
||||
|
||||
/// Returns the current local game settings path under the instance configuration directory.
|
||||
public Path getInstanceGameSettingsFile(GameInstanceID instanceId) {
|
||||
return getInstanceConfigDirectory(instanceId).resolve(INSTANCE_GAME_SETTINGS_FILENAME);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/// HMCL repository snapshot, parallel to [HMCLGameInstance] in the instance hierarchy.
|
||||
@NotNullByDefault
|
||||
public class HMCLGameRepositorySnapshot extends DefaultGameRepositorySnapshot {
|
||||
/// Creates an empty unsealed HMCL snapshot.
|
||||
///
|
||||
/// @param repository the owning repository
|
||||
/// @param layout the HMCL layout for this snapshot
|
||||
public HMCLGameRepositorySnapshot(HMCLGameRepository repository, HMCLGameRepositoryLayout layout) {
|
||||
super(repository, layout);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HMCLGameRepository getRepository() {
|
||||
return (HMCLGameRepository) super.getRepository();
|
||||
}
|
||||
|
||||
@Override
|
||||
public HMCLGameRepositoryLayout getLayout() {
|
||||
return (HMCLGameRepositoryLayout) super.getLayout();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HMCLGameRepositorySnapshot newEmpty() {
|
||||
return new HMCLGameRepositorySnapshot(getRepository(), getLayout());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Collection<HMCLGameInstance> getInstances() {
|
||||
return (Collection<HMCLGameInstance>) super.getInstances();
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ package org.jackhuang.hmcl.game;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.download.GameBuilder;
|
||||
import org.jackhuang.hmcl.modpack.MinecraftInstanceTask;
|
||||
import org.jackhuang.hmcl.modpack.Modpack;
|
||||
import org.jackhuang.hmcl.modpack.ModpackConfiguration;
|
||||
@@ -27,53 +27,110 @@ import org.jackhuang.hmcl.modpack.ModpackInstallTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
|
||||
/// Installs or updates an HMCL modpack by rebuilding its recognized game components.
|
||||
@NotNullByDefault
|
||||
public final class HMCLModpackInstallTask extends Task<Void> {
|
||||
private final Path zipFile;
|
||||
private final GameInstanceID instanceId;
|
||||
|
||||
/// Existing instance selecting update mode, or `null` for a new installation.
|
||||
private final @Nullable DefaultGameInstance updateTarget;
|
||||
|
||||
private final HMCLGameRepository repository;
|
||||
private final DefaultDependencyManager dependency;
|
||||
private final Modpack modpack;
|
||||
private final List<Task<?>> dependencies = new ArrayList<>(1);
|
||||
private final List<Task<?>> dependents = new ArrayList<>(4);
|
||||
|
||||
public HMCLModpackInstallTask(HMCLGameRepository repository, Path zipFile, Modpack modpack, GameInstanceID instanceId) {
|
||||
/// Creates a task that installs a new HMCL modpack instance.
|
||||
///
|
||||
/// @param repository the target HMCL game repository
|
||||
/// @param zipFile the HMCL modpack archive
|
||||
/// @param modpack the parsed modpack metadata
|
||||
/// @param instanceId the id of the new instance
|
||||
public HMCLModpackInstallTask(
|
||||
HMCLGameRepository repository,
|
||||
Path zipFile,
|
||||
Modpack modpack,
|
||||
GameInstanceID instanceId) {
|
||||
this(repository, zipFile, modpack, instanceId, null);
|
||||
}
|
||||
|
||||
/// Creates a task that updates an existing HMCL modpack instance.
|
||||
///
|
||||
/// @param repository the target HMCL game repository
|
||||
/// @param zipFile the HMCL modpack archive
|
||||
/// @param modpack the parsed modpack metadata
|
||||
/// @param instance the existing instance to update
|
||||
/// @throws IllegalArgumentException if `instance` belongs to another repository, has no
|
||||
/// modpack configuration, or records another provider type
|
||||
public HMCLModpackInstallTask(
|
||||
HMCLGameRepository repository,
|
||||
Path zipFile,
|
||||
Modpack modpack,
|
||||
DefaultGameInstance instance) {
|
||||
this(repository, zipFile, modpack, instance.getId(), instance);
|
||||
}
|
||||
|
||||
/// Creates an HMCL modpack task in the mode selected by `updateTarget`.
|
||||
///
|
||||
/// @param repository the target HMCL game repository
|
||||
/// @param zipFile the HMCL modpack archive
|
||||
/// @param modpack the parsed modpack metadata
|
||||
/// @param instanceId the target instance id
|
||||
/// @param updateTarget the existing instance selecting update mode, or `null` for install
|
||||
/// @throws IllegalArgumentException if an update target belongs to another repository or has
|
||||
/// no compatible configuration
|
||||
private HMCLModpackInstallTask(
|
||||
HMCLGameRepository repository,
|
||||
Path zipFile,
|
||||
Modpack modpack,
|
||||
GameInstanceID instanceId,
|
||||
@Nullable DefaultGameInstance updateTarget) {
|
||||
this.repository = repository;
|
||||
this.dependency = repository.getDependency();
|
||||
this.zipFile = zipFile;
|
||||
this.instanceId = instanceId;
|
||||
this.updateTarget = updateTarget;
|
||||
this.modpack = modpack;
|
||||
if (this.updateTarget != null) {
|
||||
dependency.validateGameInstance(this.updateTarget);
|
||||
}
|
||||
|
||||
Path run = repository.getRunDirectory(this.instanceId);
|
||||
Path json = repository.getModpackConfiguration(this.instanceId);
|
||||
if (repository.hasInstance(this.instanceId) && Files.notExists(json))
|
||||
throw new IllegalArgumentException("Instance " + instanceId + " already exists");
|
||||
Path run = repository.getLayout().getInstanceRoot(this.instanceId);
|
||||
Path json = repository.getLayout().getModpackConfigurationFile(this.instanceId);
|
||||
if (this.updateTarget != null && Files.notExists(json))
|
||||
throw new IllegalArgumentException("Instance " + instanceId + " is not a HMCL modpack. Cannot update this instance.");
|
||||
|
||||
dependents.add(dependency.newGameBuilder().name(this.instanceId).gameVersion(modpack.getGameVersion()).buildAsync());
|
||||
|
||||
onDone().register(event -> {
|
||||
if (event.isFailed()) repository.removeInstanceFromDisk(this.instanceId);
|
||||
});
|
||||
|
||||
ModpackConfiguration<Modpack> config = null;
|
||||
@Nullable ModpackConfiguration<Modpack> config = null;
|
||||
try {
|
||||
if (Files.exists(json)) {
|
||||
if (this.updateTarget != null && Files.exists(json)) {
|
||||
config = JsonUtils.fromJsonFile(json, ModpackConfiguration.typeOf(Modpack.class));
|
||||
|
||||
if (!HMCLModpackProvider.INSTANCE.getName().equals(config.getType()))
|
||||
if (config == null || !HMCLModpackProvider.INSTANCE.getName().equals(config.getType()))
|
||||
throw new IllegalArgumentException("Instance " + instanceId + " is not a HMCL modpack. Cannot update this instance.");
|
||||
}
|
||||
} catch (JsonParseException | IOException ignore) {
|
||||
}
|
||||
dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), Collections.singletonList("/minecraft"), it -> !"pack.json".equals(it), config));
|
||||
dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), Collections.singletonList("/minecraft"), modpack, HMCLModpackProvider.INSTANCE, modpack.getName(), modpack.getVersion(), repository.getModpackConfiguration(this.instanceId)).withStage("hmcl.modpack"));
|
||||
|
||||
onDone().register(event -> {
|
||||
if (this.updateTarget == null && event.isFailed()) {
|
||||
repository.removeInstanceFromDisk(this.instanceId);
|
||||
}
|
||||
});
|
||||
|
||||
dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), List.of("/minecraft"), it -> !"pack.json".equals(it), config));
|
||||
dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), List.of("/minecraft"), modpack, HMCLModpackProvider.INSTANCE, modpack.getName(), modpack.getVersion(), repository.getLayout().getModpackConfigurationFile(this.instanceId)).withStage("hmcl.modpack"));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -86,20 +143,43 @@ public final class HMCLModpackInstallTask extends Task<Void> {
|
||||
return dependents;
|
||||
}
|
||||
|
||||
/// Reads the legacy pack manifest and schedules one builder for all recognized components.
|
||||
///
|
||||
/// The pack manifest is used only for component detection. The builder creates a new
|
||||
/// patch-structured manifest instead of attempting to mutate the legacy flattened manifest.
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
String json = CompressingUtils.readTextZipEntry(zipFile, "minecraft/pack.json");
|
||||
GameInstanceManifest originalManifest = JsonUtils.GSON.fromJson(json, GameInstanceManifest.class).withId(instanceId).withJar(null);
|
||||
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(originalManifest, null);
|
||||
Task<GameInstanceManifest> libraryTask = Task.supplyAsync(() -> originalManifest);
|
||||
// reinstall libraries
|
||||
// libraries of Forge and OptiFine should be obtained by installation.
|
||||
for (LibraryAnalyzer.LibraryMark mark : analyzer) {
|
||||
if (LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId().equals(mark.getLibraryId()))
|
||||
continue;
|
||||
libraryTask = libraryTask.thenComposeAsync(version -> dependency.installLibraryAsync(modpack.getGameVersion(), version, mark.getLibraryId(), mark.getLibraryVersion()));
|
||||
}
|
||||
GameInstanceManifest originalManifest = JsonUtils.fromNonNullJson(json, GameInstanceManifest.class)
|
||||
.withId(instanceId)
|
||||
.withJar(null);
|
||||
GameComponentAnalyzer analyzer = GameComponentAnalyzer.analyze(originalManifest, null);
|
||||
|
||||
dependencies.add(libraryTask.thenComposeAsync(repository::saveAsync));
|
||||
try (GameBuilder builder = updateTarget == null
|
||||
? dependency.newGameBuilder(instanceId)
|
||||
: dependency.newGameBuilder(updateTarget)) {
|
||||
builder.enableIsolation();
|
||||
|
||||
String gameVersion = modpack.getGameVersion();
|
||||
builder.component(GameComponentType.GAME, gameVersion);
|
||||
for (GameComponentAnalyzer.Mark mark : analyzer) {
|
||||
if (mark.componentType() == GameComponentType.GAME) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@Nullable String componentVersion = mark.version();
|
||||
if (componentVersion != null) {
|
||||
if (mark.componentType() == GameComponentType.OPTIFINE) {
|
||||
Matcher matcher = GameComponentAnalyzer.OPTIFINE_VERSION_PATTERN.matcher(componentVersion);
|
||||
if (matcher.matches()) {
|
||||
componentVersion = matcher.group("optifine");
|
||||
}
|
||||
}
|
||||
builder.component(mark.componentType(), componentVersion);
|
||||
}
|
||||
}
|
||||
dependencies.add(builder.buildAsync());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
@@ -42,12 +43,12 @@ public final class HMCLModpackProvider implements ModpackProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task<?> createCompletionTask(DefaultDependencyManager dependencyManager, GameInstanceID instanceId) {
|
||||
public @Nullable Task<?> createCompletionTask(DefaultDependencyManager dependencyManager, DefaultGameInstance instance) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task<?> createUpdateTask(DefaultDependencyManager dependencyManager, GameInstanceID instanceId, Path zipFile, Modpack modpack) throws MismatchedModpackTypeException {
|
||||
public Task<?> createUpdateTask(DefaultDependencyManager dependencyManager, DefaultGameInstance instance, Path zipFile, Modpack modpack) throws MismatchedModpackTypeException {
|
||||
if (!(modpack.getManifest() instanceof HMCLModpackManifest))
|
||||
throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName());
|
||||
|
||||
@@ -55,7 +56,7 @@ public final class HMCLModpackProvider implements ModpackProvider {
|
||||
throw new IllegalArgumentException("HMCLModpackProvider requires HMCLGameRepository");
|
||||
}
|
||||
|
||||
return new ModpackUpdateTask(dependencyManager.getGameRepository(), instanceId, new HMCLModpackInstallTask(repository, zipFile, modpack, instanceId));
|
||||
return new ModpackUpdateTask(instance, new HMCLModpackInstallTask(repository, zipFile, modpack, instance));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -25,8 +25,6 @@ import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloadException;
|
||||
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.download.MaintainTask;
|
||||
import org.jackhuang.hmcl.download.game.*;
|
||||
import org.jackhuang.hmcl.java.JavaManager;
|
||||
import org.jackhuang.hmcl.java.JavaRuntime;
|
||||
@@ -84,9 +82,8 @@ public final class LauncherHelper {
|
||||
|
||||
private static final String LWJGL_3_4_1_TIP = "lwjgl3.4.1-ffm";
|
||||
|
||||
private final HMCLGameRepository repository;
|
||||
private final HMCLGameInstance gameInstance;
|
||||
private Account account;
|
||||
private final GameInstanceID selectedInstanceId;
|
||||
private Path scriptFile;
|
||||
private final GameSettings.Effective setting;
|
||||
private LauncherVisibility launcherVisibility;
|
||||
@@ -94,16 +91,23 @@ public final class LauncherHelper {
|
||||
private QuickPlayOption quickPlayOption;
|
||||
private boolean disableOfflineSkin = false;
|
||||
|
||||
public LauncherHelper(HMCLGameRepository repository, Account account, GameInstanceID selectedInstanceId) {
|
||||
this.repository = Objects.requireNonNull(repository);
|
||||
public LauncherHelper(HMCLGameInstance gameInstance, Account account) {
|
||||
this.gameInstance = Objects.requireNonNull(gameInstance);
|
||||
this.account = Objects.requireNonNull(account);
|
||||
this.selectedInstanceId = selectedInstanceId;
|
||||
this.setting = repository.getEffectiveGameSettings(selectedInstanceId);
|
||||
this.setting = gameInstance.getEffectiveSettings();
|
||||
this.launcherVisibility = setting.getInheritable(GameSettings::launcherVisibilityProperty);
|
||||
this.showLogs = setting.getInheritable(GameSettings::showLogsProperty);
|
||||
this.launchingStepsPane.setTitle(i18n("instance.launch"));
|
||||
}
|
||||
|
||||
public HMCLGameInstance getGameInstance() {
|
||||
return gameInstance;
|
||||
}
|
||||
|
||||
private HMCLGameRepository repository() {
|
||||
return gameInstance.getRepository();
|
||||
}
|
||||
|
||||
private final TaskExecutorDialogPane launchingStepsPane = new TaskExecutorDialogPane(TaskCancellationAction.NORMAL);
|
||||
|
||||
public Account getAccount() {
|
||||
@@ -134,7 +138,7 @@ public final class LauncherHelper {
|
||||
public void launch() {
|
||||
FXUtils.checkFxUserThread();
|
||||
|
||||
LOG.info("Launching game version: " + selectedInstanceId);
|
||||
LOG.info("Launching game instance: " + gameInstance.getId());
|
||||
|
||||
Controllers.dialog(launchingStepsPane);
|
||||
launch0();
|
||||
@@ -145,48 +149,55 @@ public final class LauncherHelper {
|
||||
launch();
|
||||
}
|
||||
|
||||
/// Builds and executes the launch pipeline for the captured game instance.
|
||||
private void launch0() {
|
||||
// https://github.com/HMCL-dev/HMCL/pull/4121
|
||||
PROCESSES.removeIf(it -> it.get() == null);
|
||||
|
||||
HMCLGameRepository repository = repository();
|
||||
DefaultDependencyManager dependencyManager = repository.getDependency();
|
||||
AtomicReference<GameInstanceManifest> version = new AtomicReference<>(MaintainTask.maintain(repository, repository.getResolvedInstanceManifest(selectedInstanceId).launchManifest()));
|
||||
Optional<String> gameVersion = repository.getGameVersion(version.get());
|
||||
boolean integrityCheck = repository.unmarkInstanceLaunchedAbnormally(selectedInstanceId);
|
||||
// Resolve already deduplicated libraries; apply loader-specific argument repairs for this launch.
|
||||
var launchManifest = new AtomicReference<>(LaunchManifestNormalizer.repairForLaunch(gameInstance.getResolvedManifest()));
|
||||
boolean integrityCheck = gameInstance.unmarkLaunchedAbnormally();
|
||||
CountDownLatch launchingLatch = new CountDownLatch(1);
|
||||
List<String> javaAgents = new ArrayList<>(0);
|
||||
List<String> javaArguments = new ArrayList<>(0);
|
||||
|
||||
AtomicReference<JavaRuntime> javaVersionRef = new AtomicReference<>();
|
||||
|
||||
TaskExecutor executor = checkGameState(repository, setting, version.get())
|
||||
TaskExecutor executor = checkGameState(gameInstance, setting, launchManifest.get())
|
||||
.thenComposeAsync(java -> {
|
||||
javaVersionRef.set(Objects.requireNonNull(java));
|
||||
version.set(NativePatcher.patchNative(repository, version.get(), gameVersion.orElse(null), java, setting, javaArguments));
|
||||
launchManifest.set(NativePatcher.patchNative(gameInstance, launchManifest.get(), java, setting, javaArguments));
|
||||
if (setting.getInheritable(GameSettings::notCheckGameProperty))
|
||||
return null;
|
||||
return Task.allOf(
|
||||
dependencyManager.checkGameCompletionAsync(version.get(), integrityCheck),
|
||||
dependencyManager.checkGameCompletionAsync(gameInstance, launchManifest.get(), integrityCheck),
|
||||
Task.composeAsync(() -> {
|
||||
try {
|
||||
ModpackConfiguration<?> configuration = ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(selectedInstanceId));
|
||||
ModpackProvider provider = ModpackHelper.getProviderByType(configuration.getType());
|
||||
@Nullable ModpackConfiguration<?> configuration =
|
||||
gameInstance.readModpackConfiguration();
|
||||
if (configuration == null) return null;
|
||||
@Nullable ModpackProvider provider =
|
||||
ModpackHelper.getProviderByType(configuration.getType());
|
||||
if (provider == null) return null;
|
||||
else return provider.createCompletionTask(dependencyManager, selectedInstanceId);
|
||||
else return provider.createCompletionTask(
|
||||
dependencyManager,
|
||||
gameInstance);
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
Task.composeAsync(() -> {
|
||||
if (OperatingSystem.CURRENT_OS != OperatingSystem.WINDOWS
|
||||
|| !(setting.getRenderer(GameVersionNumber.asGameVersion(gameVersion)) instanceof Renderer.Driver renderer)
|
||||
|| !(setting.getRenderer(gameInstance.getVersion()) instanceof Renderer.Driver renderer)
|
||||
|| renderer.mesaDriverName() == null)
|
||||
return null;
|
||||
|
||||
Library lib = NativePatcher.getWindowsMesaLoader(java, renderer, OperatingSystem.SYSTEM_VERSION);
|
||||
if (lib == null)
|
||||
return null;
|
||||
Path file = dependencyManager.getGameRepository().getLibraryFile(version.get(), lib);
|
||||
Path file = gameInstance.getLayout().getLibraryFile(gameInstance.getId(), lib);
|
||||
if (file.toAbsolutePath().toString().indexOf('=') >= 0) {
|
||||
LOG.warning("Invalid character '=' in the libraries directory path, unable to attach software renderer loader");
|
||||
return null;
|
||||
@@ -194,7 +205,7 @@ public final class LauncherHelper {
|
||||
|
||||
String agent = FileUtils.getAbsolutePath(file) + "=" + renderer.mesaDriverName();
|
||||
|
||||
if (GameLibrariesTask.shouldDownloadLibrary(repository, version.get(), lib, integrityCheck)) {
|
||||
if (GameLibrariesTask.shouldDownloadLibrary(repository, launchManifest.get(), lib, integrityCheck)) {
|
||||
return new LibraryDownloadTask(dependencyManager, file, lib)
|
||||
.thenRunAsync(() -> javaAgents.add(agent));
|
||||
} else {
|
||||
@@ -204,18 +215,13 @@ public final class LauncherHelper {
|
||||
})
|
||||
);
|
||||
}).withStage("launch.state.dependencies")
|
||||
.thenComposeAsync(() -> {
|
||||
if (gameVersion.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return new GameVerificationFixTask(dependencyManager, gameVersion.get(), version.get());
|
||||
})
|
||||
.thenComposeAsync(() -> new GameVerificationFixTask(gameInstance, gameInstance.getVersion(), launchManifest.get()))
|
||||
.thenComposeAsync(() -> {
|
||||
if (setting.getInheritable(GameSettings::allowAutoAgentProperty)
|
||||
|| setting.getInheritable(GameSettings::noJVMOptionsProperty)
|
||||
|| setting.getInheritable(GameSettings::noOptimizingJVMOptionsProperty)
|
||||
|| Boolean.TRUE.equals(state().getShownTips().get(LWJGL_3_4_1_TIP))
|
||||
|| !NativePatcher.needPatchMemoryUtil(version.get(), javaVersionRef.get().getParsedVersion())) {
|
||||
|| !NativePatcher.needPatchMemoryUtil(launchManifest.get(), javaVersionRef.get().getParsedVersion())) {
|
||||
return Task.completed(null);
|
||||
} else {
|
||||
CompletableFuture<Void> future = new CompletableFuture<>();
|
||||
@@ -234,8 +240,8 @@ public final class LauncherHelper {
|
||||
})
|
||||
.thenComposeAsync(() -> logIn(account).withStage("launch.state.logging_in"))
|
||||
.thenComposeAsync(authInfo -> Task.supplyAsync(() -> {
|
||||
LaunchOptions.Builder launchOptionsBuilder = repository.getLaunchOptions(
|
||||
selectedInstanceId, javaVersionRef.get(), repository.getBaseDirectory(), javaAgents, javaArguments, scriptFile != null);
|
||||
LaunchOptions.Builder launchOptionsBuilder = gameInstance.getLaunchOptions(
|
||||
javaVersionRef.get(), repository.getBaseDirectory(), javaAgents, javaArguments, scriptFile != null);
|
||||
if (disableOfflineSkin) {
|
||||
launchOptionsBuilder.setDaemon(false);
|
||||
}
|
||||
@@ -275,16 +281,16 @@ public final class LauncherHelper {
|
||||
|
||||
LaunchOptions launchOptions = launchOptionsBuilder.create();
|
||||
|
||||
LOG.info("Here's the structure of game mod directory:\n" + FileUtils.printFileStructure(repository.getModsDirectory(selectedInstanceId), 10));
|
||||
LOG.info("Here's the structure of game mod directory:\n" + FileUtils.printFileStructure(gameInstance.getModsDirectory(), 10));
|
||||
|
||||
return new HMCLGameLauncher(
|
||||
repository,
|
||||
version.get(),
|
||||
gameInstance,
|
||||
launchManifest.get(),
|
||||
authInfo,
|
||||
launchOptions,
|
||||
launcherVisibility == LauncherVisibility.CLOSE
|
||||
? null // Unnecessary to start listening to game process output when close launcher immediately after game launched.
|
||||
: new HMCLProcessListener(repository, version.get(), authInfo, launchOptions, launchingLatch, gameVersion.isPresent())
|
||||
: new HMCLProcessListener(authInfo, launchOptions, launchingLatch, gameInstance.getVersion().compareTo(GameVersionNumber.unknown()) != 0)
|
||||
);
|
||||
}).thenComposeAsync(launcher -> { // launcher is prev task's result
|
||||
if (scriptFile == null) {
|
||||
@@ -348,8 +354,7 @@ public final class LauncherHelper {
|
||||
message = i18n("launch.failed.decompressing_natives") + "\n" + ex.getLocalizedMessage();
|
||||
} else if (ex instanceof LibraryDownloadException) {
|
||||
message = i18n("launch.failed.download_library", ((LibraryDownloadException) ex).getLibrary().name()) + "\n";
|
||||
if (ex.getCause() instanceof ResponseCodeException) {
|
||||
ResponseCodeException rce = (ResponseCodeException) ex.getCause();
|
||||
if (ex.getCause() instanceof ResponseCodeException rce) {
|
||||
int responseCode = rce.getResponseCode();
|
||||
String uri = rce.getUri();
|
||||
if (responseCode == 404)
|
||||
@@ -363,8 +368,7 @@ public final class LauncherHelper {
|
||||
URI uri = ((DownloadException) ex).getUri();
|
||||
if (ex.getCause() instanceof SocketTimeoutException) {
|
||||
message = i18n("install.failed.downloading.timeout", uri);
|
||||
} else if (ex.getCause() instanceof ResponseCodeException) {
|
||||
ResponseCodeException responseCodeException = (ResponseCodeException) ex.getCause();
|
||||
} else if (ex.getCause() instanceof ResponseCodeException responseCodeException) {
|
||||
if (I18n.hasKey("download.code." + responseCodeException.getResponseCode())) {
|
||||
message = i18n("download.code." + responseCodeException.getResponseCode(), uri);
|
||||
} else {
|
||||
@@ -379,8 +383,7 @@ public final class LauncherHelper {
|
||||
message = i18n("account.failed.injector_download_failure");
|
||||
} else if (ex instanceof CharacterDeletedException) {
|
||||
message = i18n("account.failed.character_deleted");
|
||||
} else if (ex instanceof ResponseCodeException) {
|
||||
ResponseCodeException rce = (ResponseCodeException) ex;
|
||||
} else if (ex instanceof ResponseCodeException rce) {
|
||||
int responseCode = rce.getResponseCode();
|
||||
String uri = rce.getUri();
|
||||
if (responseCode == 404)
|
||||
@@ -423,9 +426,9 @@ public final class LauncherHelper {
|
||||
executor.start();
|
||||
}
|
||||
|
||||
private static Task<JavaRuntime> checkGameState(HMCLGameRepository repository, GameSettings.Effective setting, GameInstanceManifest manifest) {
|
||||
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(manifest, repository.getGameVersion(manifest).orElse(null));
|
||||
GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(analyzer.getVersion(LibraryAnalyzer.LibraryType.MINECRAFT));
|
||||
private static Task<JavaRuntime> checkGameState(HMCLGameInstance gameInstance, GameSettings.Effective setting, GameInstanceManifest manifest) {
|
||||
GameComponentAnalyzer analyzer = GameComponentAnalyzer.analyze(manifest, gameInstance.getVersion());
|
||||
GameVersionNumber gameVersion = gameInstance.getVersion();
|
||||
|
||||
Task<JavaRuntime> getJavaTask = Task.supplyAsync(() -> {
|
||||
try {
|
||||
@@ -456,9 +459,9 @@ public final class LauncherHelper {
|
||||
int targetJavaVersionMajor = Integer.parseInt(setting.getInheritable(GameSettings::customJavaVersionProperty));
|
||||
GameJavaVersion minimumJavaVersion = null;
|
||||
if (gameVersion.compareTo("1.12.2") == 0) {
|
||||
Optional<String> cleanroomVersion = analyzer.getVersion(LibraryAnalyzer.LibraryType.CLEANROOM);
|
||||
if (cleanroomVersion.isPresent()) {
|
||||
minimumJavaVersion = GameJavaVersion.getCleanroomJavaVersion(cleanroomVersion.get());
|
||||
@Nullable String cleanroomVersion = analyzer.getVersion(GameComponentType.CLEANROOM);
|
||||
if (cleanroomVersion != null) {
|
||||
minimumJavaVersion = GameJavaVersion.getCleanroomJavaVersion(cleanroomVersion);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -480,9 +483,9 @@ public final class LauncherHelper {
|
||||
}
|
||||
} else {
|
||||
if (gameVersion.compareTo("1.12.2") == 0) {
|
||||
Optional<String> cleanroomVersion = analyzer.getVersion(LibraryAnalyzer.LibraryType.CLEANROOM);
|
||||
if (cleanroomVersion.isPresent()) {
|
||||
targetJavaVersion = GameJavaVersion.getCleanroomJavaVersion(cleanroomVersion.get());
|
||||
@Nullable String cleanroomVersion = analyzer.getVersion(GameComponentType.CLEANROOM);
|
||||
if (cleanroomVersion != null) {
|
||||
targetJavaVersion = GameJavaVersion.getCleanroomJavaVersion(cleanroomVersion);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -491,7 +494,7 @@ public final class LauncherHelper {
|
||||
}
|
||||
|
||||
if (targetJavaVersion != null && supportedVersions.contains(targetJavaVersion)) {
|
||||
downloadJava(targetJavaVersion, repository)
|
||||
downloadJava(targetJavaVersion, gameInstance.getRepository())
|
||||
.whenCompleteAsync((downloadedJava, exception) -> {
|
||||
if (exception == null) {
|
||||
future.complete(downloadedJava);
|
||||
@@ -554,10 +557,9 @@ public final class LauncherHelper {
|
||||
} else {
|
||||
GameJavaVersion gameJavaVersion;
|
||||
if (violatedMandatoryConstraints.contains(JavaVersionConstraint.CLEANROOM)) {
|
||||
String cleanroomVersion = analyzer.getVersion(LibraryAnalyzer.LibraryType.CLEANROOM)
|
||||
.orElse("");
|
||||
@Nullable String cleanroomVersion = analyzer.getVersion(GameComponentType.CLEANROOM);
|
||||
|
||||
gameJavaVersion = !cleanroomVersion.isEmpty()
|
||||
gameJavaVersion = cleanroomVersion != null
|
||||
? GameJavaVersion.getCleanroomJavaVersion(cleanroomVersion)
|
||||
: GameJavaVersion.JAVA_21;
|
||||
} else if (violatedMandatoryConstraints.contains(JavaVersionConstraint.GAME_JSON))
|
||||
@@ -568,7 +570,7 @@ public final class LauncherHelper {
|
||||
gameJavaVersion = null;
|
||||
|
||||
if (gameJavaVersion != null) {
|
||||
FXUtils.runInFX(() -> downloadJava(gameJavaVersion, repository).whenCompleteAsync((downloadedJava, throwable) -> {
|
||||
FXUtils.runInFX(() -> downloadJava(gameJavaVersion, gameInstance.getRepository()).whenCompleteAsync((downloadedJava, throwable) -> {
|
||||
if (throwable == null) {
|
||||
setting.setJavaAutoSelected();
|
||||
future.complete(downloadedJava);
|
||||
@@ -638,7 +640,7 @@ public final class LauncherHelper {
|
||||
break;
|
||||
case MODDED_JAVA_16:
|
||||
// Minecraft<=1.17.1+Forge[37.0.0,37.0.60) not compatible with Java 17
|
||||
String forgePatchVersion = analyzer.getVersion(LibraryAnalyzer.LibraryType.FORGE).orElse(null);
|
||||
@Nullable String forgePatchVersion = analyzer.getVersion(GameComponentType.FORGE);
|
||||
if (forgePatchVersion != null && VersionNumber.compare(forgePatchVersion, "37.0.60") < 0)
|
||||
suggestions.add(i18n("launch.advice.forge37_0_60"));
|
||||
else
|
||||
@@ -651,8 +653,8 @@ public final class LauncherHelper {
|
||||
suggestions.add(i18n("launch.advice.modded_java", 21, gameVersion));
|
||||
break;
|
||||
case CLEANROOM: {
|
||||
String cleanroomVersion = analyzer.getVersion(LibraryAnalyzer.LibraryType.CLEANROOM).orElse("");
|
||||
if (!cleanroomVersion.isEmpty())
|
||||
@Nullable String cleanroomVersion = analyzer.getVersion(GameComponentType.CLEANROOM);
|
||||
if (cleanroomVersion != null)
|
||||
suggestions.add(i18n("launch.advice.cleanroom", GameJavaVersion.getCleanroomJavaVersion(cleanroomVersion).majorVersion(), cleanroomVersion));
|
||||
else
|
||||
suggestions.add(i18n("launch.advice.cleanroom", 21, ""));
|
||||
@@ -681,7 +683,7 @@ public final class LauncherHelper {
|
||||
suggestions.add(i18n("launch.advice.not_enough_space", totalMemorySizeMB));
|
||||
}
|
||||
|
||||
VersionNumber forgeVersion = analyzer.getVersion(LibraryAnalyzer.LibraryType.FORGE)
|
||||
VersionNumber forgeVersion = Optional.ofNullable(analyzer.getVersion(GameComponentType.FORGE))
|
||||
.map(VersionNumber::asVersion)
|
||||
.orElse(null);
|
||||
|
||||
@@ -829,16 +831,12 @@ public final class LauncherHelper {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The managed process listener.
|
||||
* Guarantee that one [JavaProcess], one [HMCLProcessListener].
|
||||
* Because every time we launched a game, we generates a new [HMCLProcessListener]
|
||||
*/
|
||||
/// The managed process listener.
|
||||
/// Guarantee that one Java [Process], one [HMCLProcessListener].
|
||||
/// Because every time we launched a game, we generates a new [HMCLProcessListener]
|
||||
private final class HMCLProcessListener implements ProcessListener {
|
||||
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
private final HMCLGameRepository repository;
|
||||
private final GameInstanceManifest manifest;
|
||||
private final LaunchOptions launchOptions;
|
||||
private ManagedProcess process;
|
||||
private volatile boolean lwjgl;
|
||||
@@ -850,9 +848,7 @@ public final class LauncherHelper {
|
||||
private Thread submitLogThread;
|
||||
private LinkedBlockingQueue<Log> logBuffer;
|
||||
|
||||
public HMCLProcessListener(HMCLGameRepository repository, GameInstanceManifest manifest, AuthInfo authInfo, LaunchOptions launchOptions, CountDownLatch launchingLatch, boolean detectWindow) {
|
||||
this.repository = repository;
|
||||
this.manifest = manifest;
|
||||
public HMCLProcessListener(AuthInfo authInfo, LaunchOptions launchOptions, CountDownLatch launchingLatch, boolean detectWindow) {
|
||||
this.launchOptions = launchOptions;
|
||||
this.launchingLatch = launchingLatch;
|
||||
this.detectWindow = detectWindow;
|
||||
@@ -979,7 +975,7 @@ public final class LauncherHelper {
|
||||
Log4jLevel level = isErrorStream && !log.startsWith("[authlib-injector]") ? Log4jLevel.ERROR : null;
|
||||
if (showLogs) {
|
||||
if (level == null)
|
||||
level = Lang.requireNonNullElse(Log4jLevel.guessLevel(log), Log4jLevel.INFO);
|
||||
level = Objects.requireNonNullElse(Log4jLevel.guessLevel(log), Log4jLevel.INFO);
|
||||
logBuffer.add(new Log(log, level));
|
||||
} else {
|
||||
lock.lock();
|
||||
@@ -1037,8 +1033,8 @@ public final class LauncherHelper {
|
||||
}
|
||||
|
||||
if (exitType != ExitType.NORMAL) {
|
||||
repository.markInstanceLaunchedAbnormally(manifest.id());
|
||||
runLater(() -> new GameCrashWindow(process, exitType, repository, manifest, launchOptions, logs).show());
|
||||
gameInstance.markLaunchedAbnormally();
|
||||
runLater(() -> new GameCrashWindow(process, exitType, gameInstance, launchOptions, logs).show());
|
||||
}
|
||||
|
||||
checkExit();
|
||||
|
||||
@@ -138,6 +138,11 @@ public abstract class LocalizedRemoteAddonRepository implements RemoteAddonRepos
|
||||
return getBackedRemoteModRepository().getAddonById(downloadProvider, id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RemoteAddon resolveDependency(DownloadProvider downloadProvider, String id) throws IOException {
|
||||
return getBackedRemoteModRepository().resolveDependency(downloadProvider, id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RemoteAddon.File getAddonFile(String projectId, String fileId) throws IOException {
|
||||
return getBackedRemoteModRepository().getAddonFile(projectId, fileId);
|
||||
|
||||
@@ -17,14 +17,18 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||
import org.jackhuang.hmcl.util.io.Zipper;
|
||||
import org.jackhuang.hmcl.util.logging.Logger;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@@ -41,22 +45,24 @@ public final class LogExporter {
|
||||
}
|
||||
|
||||
public static CompletableFuture<Void> exportLogs(
|
||||
Path zipFile, DefaultGameRepository repository, GameInstanceID instanceId, String logs, String launchScript,
|
||||
Path zipFile, DefaultGameInstance instance, LaunchOptions options, String logs, String launchScript,
|
||||
PathMatcher logMatcher) {
|
||||
Path runDirectory = repository.getRunDirectory(instanceId);
|
||||
Path baseDirectory = repository.getBaseDirectory();
|
||||
DefaultGameRepositorySnapshot repositorySnapshot = instance.getSnapshot();
|
||||
Path runDirectory = options.getGameDir();
|
||||
List<GameInstanceID> instances = new ArrayList<>();
|
||||
|
||||
GameInstanceID currentInstanceId = instanceId;
|
||||
GameInstanceID currentInstanceId = instance.id;
|
||||
HashSet<GameInstanceID> resolvedSoFar = new HashSet<>();
|
||||
while (true) {
|
||||
if (resolvedSoFar.contains(currentInstanceId)) break;
|
||||
resolvedSoFar.add(currentInstanceId);
|
||||
GameInstanceManifest currentVersion = repository.getInstanceManifest(currentInstanceId);
|
||||
@Nullable DefaultGameInstance currentInstance = repositorySnapshot.get(currentInstanceId);
|
||||
if (currentInstance == null)
|
||||
break;
|
||||
instances.add(currentInstanceId);
|
||||
|
||||
if (currentVersion.inheritsFrom() != null) {
|
||||
currentInstanceId = currentVersion.inheritsFrom();
|
||||
if (currentInstance.getManifest().inheritsFrom() != null) {
|
||||
currentInstanceId = currentInstance.getManifest().inheritsFrom();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
@@ -74,9 +80,11 @@ public final class LogExporter {
|
||||
zipper.putTextFile(Logger.filterForbiddenToken(launchScript), OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "launch.bat" : "launch.sh");
|
||||
|
||||
for (GameInstanceID id : instances) {
|
||||
Path instanceJson = repository.getInstanceJson(id);
|
||||
if (Files.exists(instanceJson)) {
|
||||
zipper.putFile(instanceJson, id + ".json");
|
||||
@Nullable DefaultGameInstance currentInstance = repositorySnapshot.get(id);
|
||||
if (currentInstance != null) {
|
||||
try (var writer = new OutputStreamWriter(zipper.putStream(id + ".json"), StandardCharsets.UTF_8)) {
|
||||
JsonUtils.GSON.toJson(currentInstance.getManifest(), writer);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
|
||||
@@ -18,11 +18,9 @@
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.Unzipper;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
@@ -44,17 +42,14 @@ public class ManuallyCreatedModpackInstallTask extends Task<Path> {
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
Path subdirectory;
|
||||
try (FileSystem fs = CompressingUtils.readonly(zipFile).setEncoding(charset).build()) {
|
||||
subdirectory = ModpackHelper.findMinecraftDirectoryInManuallyCreatedModpack(zipFile.toString(), fs);
|
||||
}
|
||||
String subdirectory = ModpackHelper.findMinecraftDirectoryInManuallyCreatedModpack(zipFile.toString(), zipFile);
|
||||
|
||||
Path dest = Paths.get("externalgames").resolve(name);
|
||||
|
||||
setResult(dest);
|
||||
|
||||
new Unzipper(zipFile, dest)
|
||||
.setSubDirectory(subdirectory.toString())
|
||||
.setSubDirectory(subdirectory)
|
||||
.setTerminateIfSubDirectoryNotExists()
|
||||
.setEncoding(charset)
|
||||
.unzip();
|
||||
|
||||
@@ -31,11 +31,7 @@ import org.jackhuang.hmcl.modpack.multimc.MultiMCModpackProvider;
|
||||
import org.jackhuang.hmcl.modpack.server.ServerModpackManifest;
|
||||
import org.jackhuang.hmcl.modpack.server.ServerModpackProvider;
|
||||
import org.jackhuang.hmcl.modpack.server.ServerModpackRemoteInstallTask;
|
||||
import org.jackhuang.hmcl.setting.GameSettings;
|
||||
import org.jackhuang.hmcl.setting.GameWindowType;
|
||||
import org.jackhuang.hmcl.setting.JavaVersionType;
|
||||
import org.jackhuang.hmcl.setting.GameDirectory;
|
||||
import org.jackhuang.hmcl.setting.GameDirectoryManager;
|
||||
import org.jackhuang.hmcl.setting.*;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
@@ -46,13 +42,13 @@ import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.LocalizedText;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.tree.ArchiveFileTree;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
@@ -60,10 +56,8 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Lang.toIterable;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
/// Utilities for reading, installing, and applying modpack-specific game settings.
|
||||
@@ -114,8 +108,8 @@ public final class ModpackHelper {
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(file, charset)) {
|
||||
findMinecraftDirectoryInManuallyCreatedModpack(file.toString(), fs);
|
||||
try {
|
||||
findMinecraftDirectoryInManuallyCreatedModpack(file.toString(), file);
|
||||
throw new ManuallyCreatedModpackException(file);
|
||||
} catch (IOException e) {
|
||||
// ignore it
|
||||
@@ -124,28 +118,35 @@ public final class ModpackHelper {
|
||||
throw new UnsupportedModpackException(file.toString());
|
||||
}
|
||||
|
||||
public static Path findMinecraftDirectoryInManuallyCreatedModpack(String modpackName, FileSystem fs) throws IOException, UnsupportedModpackException {
|
||||
Path root = fs.getPath("/");
|
||||
if (isMinecraftDirectory(root)) return root;
|
||||
try (Stream<Path> firstLayer = Files.list(root)) {
|
||||
for (Path dir : toIterable(firstLayer)) {
|
||||
if (isMinecraftDirectory(dir)) return dir;
|
||||
public static String findMinecraftDirectoryInManuallyCreatedModpack(String modpackName, Path zipPath)
|
||||
throws IOException, UnsupportedModpackException {
|
||||
|
||||
try (Stream<Path> secondLayer = Files.list(dir)) {
|
||||
for (Path subdir : toIterable(secondLayer)) {
|
||||
if (isMinecraftDirectory(subdir)) return subdir;
|
||||
try (ArchiveFileTree<?, ?> tree = ArchiveFileTree.open(zipPath)) {
|
||||
ArchiveFileTree.Dir<?> rootDir = tree.getRoot();
|
||||
|
||||
if (isMinecraftDirectory(rootDir)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
for (ArchiveFileTree.Dir<?> firstLayer : rootDir.getSubDirs().values()) {
|
||||
if (isMinecraftDirectory(firstLayer)) {
|
||||
return firstLayer.getName();
|
||||
}
|
||||
|
||||
for (ArchiveFileTree.Dir<?> secondLayer : firstLayer.getSubDirs().values()) {
|
||||
if (isMinecraftDirectory(secondLayer)) {
|
||||
return firstLayer.getName() + "/" + secondLayer.getName();
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
|
||||
throw new UnsupportedModpackException(modpackName);
|
||||
}
|
||||
|
||||
private static boolean isMinecraftDirectory(Path path) {
|
||||
return Files.isDirectory(path.resolve("versions")) &&
|
||||
(path.getFileName() == null || ".minecraft".equals(FileUtils.getName(path)));
|
||||
private static boolean isMinecraftDirectory(ArchiveFileTree.Dir<?> dir) {
|
||||
return dir.getSubDirs().containsKey("versions") && (dir.isRoot() || ".minecraft".equals(dir.getName()));
|
||||
}
|
||||
|
||||
public static ModpackConfiguration<?> readModpackConfiguration(Path file) throws IOException {
|
||||
@@ -157,15 +158,9 @@ public final class ModpackHelper {
|
||||
}
|
||||
|
||||
public static Task<?> getInstallTask(HMCLGameRepository repository, ServerModpackManifest manifest, GameInstanceID instanceId, Modpack modpack) {
|
||||
repository.markInstanceAsModpack(instanceId);
|
||||
|
||||
ExceptionalRunnable<?> success = () -> {
|
||||
repository.refresh();
|
||||
GameSettings.Instance setting = repository.getInstanceGameSettingsOrCreate(instanceId);
|
||||
repository.undoMark(instanceId);
|
||||
if (setting != null) {
|
||||
setting.getOverrideProperties().add(GameSettings.PROPERTY_RUNNING_DIRECTORY);
|
||||
}
|
||||
repository.getInstance(instanceId).enableIsolation();
|
||||
};
|
||||
|
||||
ExceptionalConsumer<Exception, ?> failure = ex -> {
|
||||
@@ -200,16 +195,10 @@ public final class ModpackHelper {
|
||||
});
|
||||
}
|
||||
|
||||
public static Task<?> getInstallTask(HMCLGameRepository repository, Path zipFile, GameInstanceID instanceId, Modpack modpack, String iconUrl) {
|
||||
repository.markInstanceAsModpack(instanceId);
|
||||
|
||||
public static Task<?> getInstallTask(HMCLGameRepository repository, Path zipFile, GameInstanceID instanceId, Modpack modpack, @Nullable String iconUrl) {
|
||||
ExceptionalRunnable<?> success = () -> {
|
||||
repository.refresh();
|
||||
GameSettings.Instance setting = repository.getInstanceGameSettingsOrCreate(instanceId);
|
||||
repository.undoMark(instanceId);
|
||||
if (setting != null) {
|
||||
setting.getOverrideProperties().add(GameSettings.PROPERTY_RUNNING_DIRECTORY);
|
||||
}
|
||||
repository.getInstance(instanceId).enableIsolation();
|
||||
};
|
||||
|
||||
ExceptionalConsumer<Exception, ?> failure = ex -> {
|
||||
@@ -237,10 +226,14 @@ public final class ModpackHelper {
|
||||
|
||||
public static Task<Void> getUpdateTask(HMCLGameRepository repository, ServerModpackManifest manifest, Charset charset, GameInstanceID instanceId, ModpackConfiguration<?> configuration) throws UnsupportedModpackException {
|
||||
switch (configuration.getType()) {
|
||||
case ServerModpackRemoteInstallTask.MODPACK_TYPE:
|
||||
return new ModpackUpdateTask(repository, instanceId, new ServerModpackRemoteInstallTask(repository.getDependency(), manifest, instanceId))
|
||||
case ServerModpackRemoteInstallTask.MODPACK_TYPE: {
|
||||
HMCLGameInstance instance = repository.getInstance(instanceId);
|
||||
return new ModpackUpdateTask(
|
||||
instance,
|
||||
new ServerModpackRemoteInstallTask(repository.getDependency(), manifest, instance))
|
||||
.thenComposeAsync(repository.refreshAsync())
|
||||
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
|
||||
}
|
||||
default:
|
||||
throw new UnsupportedModpackException();
|
||||
}
|
||||
@@ -253,11 +246,11 @@ public final class ModpackHelper {
|
||||
throw new UnsupportedModpackException();
|
||||
}
|
||||
if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
|
||||
return provider.createUpdateTask(repository.getDependency(), instanceId, zipFile, modpack)
|
||||
return provider.createUpdateTask(repository.getDependency(), repository.getInstance(instanceId), zipFile, modpack)
|
||||
.thenComposeAsync(() -> createMultiMCPostUpdateTask(repository, (MultiMCInstanceConfiguration) modpack.getManifest(), instanceId))
|
||||
.thenComposeAsync(repository.refreshAsync());
|
||||
else
|
||||
return provider.createUpdateTask(repository.getDependency(), instanceId, zipFile, modpack)
|
||||
return provider.createUpdateTask(repository.getDependency(), repository.getInstance(instanceId), zipFile, modpack)
|
||||
.thenComposeAsync(repository.refreshAsync());
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
||||
private String idToken;
|
||||
|
||||
private OAuthServer(int port) {
|
||||
super(port);
|
||||
super("localhost", port);
|
||||
|
||||
this.port = port;
|
||||
|
||||
|
||||
@@ -26,10 +26,10 @@ import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.game.GameComponentAnalyzer;
|
||||
import org.jackhuang.hmcl.game.GameInstanceManifest;
|
||||
import org.jackhuang.hmcl.game.GameJavaVersion;
|
||||
import org.jackhuang.hmcl.game.JavaVersionConstraint;
|
||||
import org.jackhuang.hmcl.game.GameInstanceManifest;
|
||||
import org.jackhuang.hmcl.setting.SettingsManager;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
@@ -37,7 +37,6 @@ import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.CacheRepository;
|
||||
import org.jackhuang.hmcl.util.DigestUtils;
|
||||
import org.jackhuang.hmcl.util.FXThread;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
@@ -321,7 +320,7 @@ public final class JavaManager {
|
||||
|
||||
@Nullable
|
||||
public static JavaRuntime findSuitableJava(Collection<JavaRuntime> javaRuntimes, GameVersionNumber gameVersion, GameInstanceManifest manifest) {
|
||||
LibraryAnalyzer analyzer = manifest != null ? LibraryAnalyzer.analyze(manifest, gameVersion != null ? gameVersion.toString() : null) : null;
|
||||
GameComponentAnalyzer analyzer = manifest != null ? GameComponentAnalyzer.analyze(manifest, gameVersion) : null;
|
||||
|
||||
boolean forceX86 = Architecture.SYSTEM_ARCH == Architecture.ARM64
|
||||
&& (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS || OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
|
||||
@@ -438,7 +437,7 @@ public final class JavaManager {
|
||||
FileUtils.tryGetPath(System.getenv("localappdata"), "Packages\\Microsoft.4297127D64EC6_8wekyb3d8bbwe\\LocalCache\\Local\\runtime")
|
||||
.ifPresent(it -> searcher.searchAllOfficialJava(it, false));
|
||||
|
||||
FileUtils.tryGetPath(Lang.requireNonNullElse(System.getenv("ProgramFiles(x86)"), "C:\\Program Files (x86)"), "Minecraft Launcher\\runtime")
|
||||
FileUtils.tryGetPath(Objects.requireNonNullElse(System.getenv("ProgramFiles(x86)"), "C:\\Program Files (x86)"), "Minecraft Launcher\\runtime")
|
||||
.ifPresent(it -> searcher.searchAllOfficialJava(it, false));
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX && Architecture.SYSTEM_ARCH == Architecture.X86_64) {
|
||||
searcher.searchAllOfficialJava(Path.of(System.getProperty("user.home"), ".minecraft/runtime"), false);
|
||||
@@ -499,7 +498,7 @@ public final class JavaManager {
|
||||
it.isJDK() ? "JDK" : "JRE",
|
||||
it.getVersion(),
|
||||
it.getPlatform().getArchitecture().getDisplayName(),
|
||||
Lang.requireNonNullElse(it.getVendor(), "Unknown"),
|
||||
Objects.requireNonNullElse(it.getVendor(), "Unknown"),
|
||||
it.getBinary()))
|
||||
.collect(Collectors.joining("\n", "Finished Java lookup, found " + searcher.javaRuntimes.size() + "\n", "")));
|
||||
return searcher.javaRuntimes;
|
||||
@@ -842,7 +841,7 @@ public final class JavaManager {
|
||||
}
|
||||
|
||||
void searchJavaInProgramFiles(String env, String defaultValue) {
|
||||
String programFiles = Lang.requireNonNullElse(System.getenv(env), defaultValue);
|
||||
String programFiles = Objects.requireNonNullElse(System.getenv(env), defaultValue);
|
||||
Path path;
|
||||
try {
|
||||
path = Path.of(programFiles);
|
||||
|
||||
@@ -47,13 +47,8 @@ import java.util.*;
|
||||
|
||||
import static java.util.stream.Collectors.toList;
|
||||
import static javafx.collections.FXCollections.observableArrayList;
|
||||
import static org.jackhuang.hmcl.setting.SettingsManager.settings;
|
||||
import static org.jackhuang.hmcl.setting.SettingsManager.getAccountMetadataRecords;
|
||||
import static org.jackhuang.hmcl.setting.SettingsManager.getAuthlibInjectorServers;
|
||||
import static org.jackhuang.hmcl.setting.SettingsManager.getUserAccountMetadataRecords;
|
||||
import static org.jackhuang.hmcl.setting.SettingsManager.userSettings;
|
||||
import static org.jackhuang.hmcl.setting.SettingsManager.*;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
|
||||
import static org.jackhuang.hmcl.util.Lang.immutableListOf;
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
@@ -73,7 +68,7 @@ public final class Accounts {
|
||||
public static final OfflineAccountFactory FACTORY_OFFLINE = new OfflineAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER);
|
||||
public static final AuthlibInjectorAccountFactory FACTORY_AUTHLIB_INJECTOR = new AuthlibInjectorAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER, Accounts::getOrCreateAuthlibInjectorServer);
|
||||
public static final MicrosoftAccountFactory FACTORY_MICROSOFT = new MicrosoftAccountFactory(new MicrosoftService(OAUTH_CALLBACK));
|
||||
public static final List<AccountFactory<?>> FACTORIES = immutableListOf(FACTORY_OFFLINE, FACTORY_MICROSOFT, FACTORY_AUTHLIB_INJECTOR);
|
||||
public static final List<AccountFactory<?>> FACTORIES = List.of(FACTORY_OFFLINE, FACTORY_MICROSOFT, FACTORY_AUTHLIB_INJECTOR);
|
||||
|
||||
// ==== login type / account factory mapping ====
|
||||
private static final Map<String, AccountFactory<?>> type2factory = new HashMap<>();
|
||||
|
||||
@@ -22,10 +22,9 @@ import javafx.beans.value.ChangeListener;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.event.EventBus;
|
||||
import org.jackhuang.hmcl.event.RefreshedGameInstancesEvent;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.HMCLGameInstance;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepositorySnapshot;
|
||||
import org.jackhuang.hmcl.util.PortablePath;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.i18n.LocalizedText;
|
||||
@@ -44,7 +43,6 @@ import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.SettingsManager.*;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
/// Manages the merged runtime view of local and user game directories.
|
||||
@@ -141,13 +139,18 @@ public final class GameDirectoryManager {
|
||||
/// The selected game repository, or `null` before the fallback game directory is resolved.
|
||||
private static final ObjectProperty<@UnknownNullability HMCLGameRepository> selectedRepository = new SimpleObjectProperty<>(GameDirectoryManager.class, "selectedRepository");
|
||||
|
||||
/// The selected instance ID projected from the selected repository.
|
||||
private static final ReadOnlyObjectWrapper<GameInstanceID> selectedInstance = new ReadOnlyObjectWrapper<>(GameDirectoryManager.class, "selectedInstance");
|
||||
/// The selected instance projected from the selected repository's current snapshot.
|
||||
private static final ReadOnlyObjectWrapper<@Nullable HMCLGameInstance> selectedInstance =
|
||||
new ReadOnlyObjectWrapper<>(GameDirectoryManager.class, "selectedInstance");
|
||||
|
||||
/// Updates [#selectedInstance] when the selected repository changes its selected instance.
|
||||
private static final ChangeListener<GameInstanceID> selectedRepositoryInstanceListener =
|
||||
private static final ChangeListener<@Nullable HMCLGameInstance> selectedRepositoryInstanceListener =
|
||||
(observable, oldValue, newValue) -> selectedInstance.set(newValue);
|
||||
|
||||
/// Reacts when the selected repository publishes a new snapshot.
|
||||
private static final ChangeListener<? super HMCLGameRepositorySnapshot> selectedRepositorySnapshotListener =
|
||||
(observable, oldValue, newValue) -> onSelectedRepositorySnapshotChanged();
|
||||
|
||||
/// Initializes game directory state from the stores loaded by [SettingsManager].
|
||||
///
|
||||
/// This method creates the built-in local and user-home game directories when required, rebuilds
|
||||
@@ -202,25 +205,32 @@ public final class GameDirectoryManager {
|
||||
@Nullable HMCLGameRepository oldRepository = selectedRepository.get();
|
||||
if (oldRepository != null) {
|
||||
oldRepository.selectedInstanceProperty().removeListener(selectedRepositoryInstanceListener);
|
||||
oldRepository.snapshotProperty().removeListener(selectedRepositorySnapshotListener);
|
||||
}
|
||||
HMCLGameRepository repository = getOrCreateRepository(newValue);
|
||||
selectedRepository.set(repository);
|
||||
selectedInstance.set(repository.getSelectedInstance());
|
||||
repository.selectedInstanceProperty().addListener(selectedRepositoryInstanceListener);
|
||||
repository.snapshotProperty().addListener(selectedRepositorySnapshotListener);
|
||||
if (repository.isLoaded()) {
|
||||
onSelectedRepositorySnapshotChanged();
|
||||
}
|
||||
repository.refreshAsync().start();
|
||||
});
|
||||
selectedGameDirectory.set(currentGameDirectory != null ? currentGameDirectory : mergedGameDirectories.get(0));
|
||||
}
|
||||
|
||||
EventBus.EVENT_BUS.channel(RefreshedGameInstancesEvent.class).registerWeak(event -> {
|
||||
runInFX(() -> {
|
||||
@Nullable HMCLGameRepository repository = selectedRepository.get();
|
||||
if (repository != null && repository == event.getSource()) {
|
||||
repository.refreshSelectedInstance();
|
||||
for (Consumer<HMCLGameRepository> listener : versionsListeners)
|
||||
listener.accept(repository);
|
||||
}
|
||||
});
|
||||
});
|
||||
/// Restores selection and notifies consumers after the selected repository publishes a loaded snapshot.
|
||||
private static void onSelectedRepositorySnapshotChanged() {
|
||||
@Nullable HMCLGameRepository repository = selectedRepository.get();
|
||||
if (repository == null || !repository.isLoaded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
repository.refreshSelectedInstance();
|
||||
for (Consumer<HMCLGameRepository> listener : versionsListeners) {
|
||||
listener.accept(repository);
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates the built-in game directories only when no game directory exists.
|
||||
@@ -480,17 +490,26 @@ public final class GameDirectoryManager {
|
||||
}
|
||||
|
||||
/// Returns the selected instance property projected from the selected repository.
|
||||
public static ReadOnlyObjectProperty<@Nullable GameInstanceID> selectedInstanceProperty() {
|
||||
///
|
||||
/// The value is `null` when the selected repository has no registered selected instance.
|
||||
///
|
||||
/// @return the read-only selected-instance property
|
||||
public static ReadOnlyObjectProperty<@Nullable HMCLGameInstance> selectedInstanceProperty() {
|
||||
return selectedInstance.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
/// Returns the selected instance ID for the selected repository.
|
||||
public static @Nullable GameInstanceID getSelectedInstance() {
|
||||
/// Returns the selected instance from the selected repository's current snapshot.
|
||||
///
|
||||
/// @return the selected instance, or `null` when none is registered
|
||||
public static @Nullable HMCLGameInstance getSelectedInstance() {
|
||||
return getSelectedRepository().getSelectedInstance();
|
||||
}
|
||||
|
||||
/// Sets the selected instance ID for the selected repository.
|
||||
public static void setSelectedInstance(@Nullable GameInstanceID instance) {
|
||||
/// Sets the selected instance for the selected repository.
|
||||
///
|
||||
/// @param instance the instance to select, or `null` to clear the selection
|
||||
/// @throws IllegalArgumentException if `instance` belongs to another repository
|
||||
public static void setSelectedInstance(@Nullable HMCLGameInstance instance) {
|
||||
getSelectedRepository().setSelectedInstance(instance);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,9 @@ package org.jackhuang.hmcl.setting;
|
||||
|
||||
import javafx.scene.image.Image;
|
||||
import org.jackhuang.hmcl.addon.mod.ModLoaderType;
|
||||
import org.jackhuang.hmcl.game.GameComponentType;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public enum GameInstanceIconType {
|
||||
DEFAULT("/assets/img/grass.png"),
|
||||
@@ -47,6 +49,7 @@ public enum GameInstanceIconType {
|
||||
case FORGE -> GameInstanceIconType.FORGE;
|
||||
case NEO_FORGE -> GameInstanceIconType.NEO_FORGE;
|
||||
case FABRIC -> GameInstanceIconType.FABRIC;
|
||||
case LEGACY_FABRIC -> GameInstanceIconType.LEGACY_FABRIC;
|
||||
case QUILT -> GameInstanceIconType.QUILT;
|
||||
case LITE_LOADER -> GameInstanceIconType.CHICKEN;
|
||||
case CLEANROOM -> GameInstanceIconType.CLEANROOM;
|
||||
@@ -54,6 +57,22 @@ public enum GameInstanceIconType {
|
||||
};
|
||||
}
|
||||
|
||||
public static @Nullable GameInstanceIconType getIconType(GameComponentType componentType) {
|
||||
return switch (componentType) {
|
||||
case GAME -> GameInstanceIconType.GRASS;
|
||||
case FABRIC, FABRIC_API -> GameInstanceIconType.FABRIC;
|
||||
case LEGACY_FABRIC, LEGACY_FABRIC_API -> GameInstanceIconType.LEGACY_FABRIC;
|
||||
case FORGE -> GameInstanceIconType.FORGE;
|
||||
case CLEANROOM -> GameInstanceIconType.CLEANROOM;
|
||||
case LITELOADER -> GameInstanceIconType.CHICKEN;
|
||||
case OPTIFINE -> GameInstanceIconType.OPTIFINE;
|
||||
case QUILT, QUILT_API -> GameInstanceIconType.QUILT;
|
||||
case NEO_FORGE -> GameInstanceIconType.NEO_FORGE;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private final String resourceUrl;
|
||||
|
||||
GameInstanceIconType(String resourceUrl) {
|
||||
|
||||
@@ -813,15 +813,15 @@ public sealed abstract class GameSettings extends ObservableSetting {
|
||||
}
|
||||
|
||||
/// Property name for using native GLFW.
|
||||
public static final String PROPERTY_USE_NATIVE_GLFW = "useNativeGLFW";
|
||||
public static final String PROPERTY_USE_NATIVE_GLFW_OR_SDL = "useNativeGLFW";
|
||||
|
||||
/// If `true`, HMCL will use native GLFW.
|
||||
@SerializedName(PROPERTY_USE_NATIVE_GLFW)
|
||||
private final InheritableProperty<Boolean> useNativeGLFW = newInheritableProperty(PROPERTY_USE_NATIVE_GLFW, false);
|
||||
@SerializedName(PROPERTY_USE_NATIVE_GLFW_OR_SDL)
|
||||
private final InheritableProperty<Boolean> useNativeGLFWorSDL = newInheritableProperty(PROPERTY_USE_NATIVE_GLFW_OR_SDL, false);
|
||||
|
||||
/// Returns the native GLFW property.
|
||||
public InheritableProperty<Boolean> useNativeGLFWProperty() {
|
||||
return useNativeGLFW;
|
||||
public InheritableProperty<Boolean> useNativeGLFWorSDLProperty() {
|
||||
return useNativeGLFWorSDL;
|
||||
}
|
||||
|
||||
/// Property name for using native OpenAL.
|
||||
|
||||
@@ -132,12 +132,12 @@ public final class LegacyGameSettingsMigrator {
|
||||
HMCLGameRepository repository,
|
||||
GameInstanceID instanceId,
|
||||
@Nullable GameSettingsPresetID parent) {
|
||||
Path instanceRoot = repository.getInstanceRoot(instanceId);
|
||||
Path instanceRoot = repository.getLayout().getInstanceRoot(instanceId);
|
||||
Path file = instanceRoot.resolve(LEGACY_INSTANCE_SETTINGS_FILENAME);
|
||||
if (!Files.exists(file)) {
|
||||
return null;
|
||||
}
|
||||
Path receiptLocation = repository.getInstanceStateDirectory(instanceId)
|
||||
Path receiptLocation = repository.getLayout().getInstanceStateDirectory(instanceId)
|
||||
.resolve(LEGACY_INSTANCE_SETTINGS_MIGRATION_RECEIPT_FILENAME);
|
||||
if (MigrationReceipt.matches(receiptLocation, file)) {
|
||||
LOG.info("Skipping already migrated legacy version setting " + file);
|
||||
@@ -271,7 +271,7 @@ public final class LegacyGameSettingsMigrator {
|
||||
GameSettings.PROPERTY_NOT_PATCH_NATIVES,
|
||||
GameSettings.PROPERTY_USE_CUSTOM_NATIVES,
|
||||
GameSettings.PROPERTY_NATIVES_DIRECTORY,
|
||||
GameSettings.PROPERTY_USE_NATIVE_GLFW,
|
||||
GameSettings.PROPERTY_USE_NATIVE_GLFW_OR_SDL,
|
||||
GameSettings.PROPERTY_USE_NATIVE_OPENAL
|
||||
));
|
||||
}
|
||||
@@ -342,7 +342,7 @@ public final class LegacyGameSettingsMigrator {
|
||||
boolean useCustomNatives = isLegacyCustomNativesDirectory(source);
|
||||
target.useCustomNativesProperty().setValue(useCustomNatives);
|
||||
target.nativesDirectoryProperty().setValue(useCustomNatives ? JsonUtils.getString(source, "nativesDir", "") : "");
|
||||
target.useNativeGLFWProperty().setValue(JsonUtils.getBoolean(source, "useNativeGLFW", false));
|
||||
target.useNativeGLFWorSDLProperty().setValue(JsonUtils.getBoolean(source, "useNativeGLFW", false));
|
||||
target.useNativeOpenALProperty().setValue(JsonUtils.getBoolean(source, "useNativeOpenAL", false));
|
||||
}
|
||||
|
||||
|
||||
@@ -153,6 +153,7 @@ public final class StyleSheets {
|
||||
addColor(builder, "-monet-primary-seed", scheme.getPrimaryColorSeed());
|
||||
|
||||
addColor(builder, scheme, ColorRole.PRIMARY, 0.5);
|
||||
addColor(builder, scheme, ColorRole.PRIMARY, 0.8);
|
||||
addColor(builder, scheme, ColorRole.SECONDARY_CONTAINER, 0.5);
|
||||
addColor(builder, scheme, ColorRole.SURFACE, 0.5);
|
||||
addColor(builder, scheme, ColorRole.SURFACE, 0.8);
|
||||
|
||||
@@ -33,10 +33,15 @@ import javafx.stage.*;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.LauncherHelper;
|
||||
import org.jackhuang.hmcl.game.ModpackHelper;
|
||||
import org.jackhuang.hmcl.java.JavaManager;
|
||||
import org.jackhuang.hmcl.java.JavaRuntime;
|
||||
import org.jackhuang.hmcl.modpack.Modpack;
|
||||
import org.jackhuang.hmcl.setting.*;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.ui.account.AccountListPage;
|
||||
@@ -56,6 +61,7 @@ import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.i18n.SupportedLocale;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
@@ -64,6 +70,8 @@ import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
@@ -371,6 +379,42 @@ public final class Controllers {
|
||||
}, updateShowTips);
|
||||
}, updateShowTips);
|
||||
}
|
||||
|
||||
tryInstallBundledModpack(GameDirectoryManager.getSelectedRepository());
|
||||
}
|
||||
|
||||
/// Offers automatic install when a package exists under `.hmcl/modpack/`.
|
||||
///
|
||||
/// Called from [Controllers#initialize] after the UI is ready. Install does not wait for repository
|
||||
/// refresh: instance paths come from the selected repository layout. The package file itself is the
|
||||
/// install signal; it is deleted after a successful install so later startups do not re-prompt.
|
||||
private static void tryInstallBundledModpack(HMCLGameRepository repository) {
|
||||
@Nullable Path modpackFile = Metadata.findBundledModpackFile();
|
||||
if (modpackFile == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.info("Found bundled modpack at " + modpackFile + "; starting automatic install");
|
||||
|
||||
Controllers.taskDialog(
|
||||
Task.composeAsync(Schedulers.io(), () -> {
|
||||
Charset encoding = CompressingUtils.findSuitableEncoding(modpackFile);
|
||||
Modpack modpack = ModpackHelper.readModpackManifest(modpackFile, encoding);
|
||||
return ModpackHelper.getInstallTask(
|
||||
repository, modpackFile, new GameInstanceID(modpack.getName()), modpack, null);
|
||||
})
|
||||
.whenComplete(Schedulers.javafx(), (ignored, exception) -> {
|
||||
if (exception != null) {
|
||||
LOG.warning("Failed to install bundled modpack", exception);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Files.deleteIfExists(modpackFile);
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to delete bundled modpack: " + modpackFile, e);
|
||||
}
|
||||
}), i18n("modpack.installing"), TaskCancellationAction.NO_CANCEL
|
||||
);
|
||||
}
|
||||
|
||||
public static void dialog(Region content) {
|
||||
@@ -554,7 +598,7 @@ public final class Controllers {
|
||||
break;
|
||||
case "hmcl://game/launch":
|
||||
var repository = GameDirectoryManager.getSelectedRepository();
|
||||
Instances.launch(repository, repository.getSelectedInstance(), LauncherHelper::setKeep);
|
||||
Instances.launch(repository.getSelectedInstance(), LauncherHelper::setKeep);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
@@ -598,4 +642,12 @@ public final class Controllers {
|
||||
|
||||
FXUtils.shutdown();
|
||||
}
|
||||
|
||||
public static final boolean AUTO_TRIM_HEAP = !"false".equalsIgnoreCase(System.getenv("HMCL_AUTO_TRIM_HEAP"));
|
||||
|
||||
public static void trimHeap() {
|
||||
if (AUTO_TRIM_HEAP) {
|
||||
System.gc();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,6 @@ import javafx.scene.text.TextFlow;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Screen;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.Callback;
|
||||
import javafx.util.Duration;
|
||||
import javafx.util.StringConverter;
|
||||
import org.glavo.url.WebURL;
|
||||
@@ -76,11 +75,11 @@ import org.jackhuang.hmcl.ui.construct.MenuSeparator;
|
||||
import org.jackhuang.hmcl.ui.construct.PopupMenu;
|
||||
import org.jackhuang.hmcl.ui.image.ImageLoader;
|
||||
import org.jackhuang.hmcl.ui.image.ImageUtils;
|
||||
import org.jackhuang.hmcl.util.FXThread;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.ResourceNotFoundError;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.ExtendedProperties;
|
||||
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.SystemUtils;
|
||||
@@ -470,25 +469,31 @@ public final class FXUtils {
|
||||
private static final Duration TOOLTIP_SLOW_SHOW_DELAY = Duration.millis(500);
|
||||
private static final Duration TOOLTIP_SHOW_DURATION = Duration.millis(5000);
|
||||
|
||||
@FXThread
|
||||
public static void installTooltip(Node node, Duration showDelay, Duration showDuration, Duration hideDelay, Tooltip tooltip) {
|
||||
checkFxUserThread();
|
||||
tooltip.setShowDelay(showDelay);
|
||||
tooltip.setShowDuration(showDuration);
|
||||
tooltip.setHideDelay(hideDelay);
|
||||
Tooltip.install(node, tooltip);
|
||||
}
|
||||
|
||||
@FXThread
|
||||
public static void installFastTooltip(Node node, Tooltip tooltip) {
|
||||
runInFX(() -> installTooltip(node, TOOLTIP_FAST_SHOW_DELAY, TOOLTIP_SHOW_DURATION, Duration.ZERO, tooltip));
|
||||
installTooltip(node, TOOLTIP_FAST_SHOW_DELAY, TOOLTIP_SHOW_DURATION, Duration.ZERO, tooltip);
|
||||
}
|
||||
|
||||
@FXThread
|
||||
public static void installFastTooltip(Node node, String tooltip) {
|
||||
installFastTooltip(node, new Tooltip(tooltip));
|
||||
}
|
||||
|
||||
@FXThread
|
||||
public static void installSlowTooltip(Node node, Tooltip tooltip) {
|
||||
runInFX(() -> installTooltip(node, TOOLTIP_SLOW_SHOW_DELAY, TOOLTIP_SHOW_DURATION, Duration.ZERO, tooltip));
|
||||
installTooltip(node, TOOLTIP_SLOW_SHOW_DELAY, TOOLTIP_SHOW_DURATION, Duration.ZERO, tooltip);
|
||||
}
|
||||
|
||||
@FXThread
|
||||
public static void installSlowTooltip(Node node, String tooltip) {
|
||||
installSlowTooltip(node, new Tooltip(tooltip));
|
||||
}
|
||||
@@ -728,116 +733,6 @@ public final class FXUtils {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class EnumBidirectionalBinding<E extends Enum<E>> implements InvalidationListener, WeakListener {
|
||||
private final WeakReference<JFXComboBox<E>> comboBoxRef;
|
||||
private final WeakReference<Property<E>> propertyRef;
|
||||
private final int hashCode;
|
||||
|
||||
private boolean updating = false;
|
||||
|
||||
private EnumBidirectionalBinding(JFXComboBox<E> comboBox, Property<E> property) {
|
||||
this.comboBoxRef = new WeakReference<>(comboBox);
|
||||
this.propertyRef = new WeakReference<>(property);
|
||||
this.hashCode = System.identityHashCode(comboBox) ^ System.identityHashCode(property);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidated(Observable sourceProperty) {
|
||||
if (!updating) {
|
||||
final JFXComboBox<E> comboBox = comboBoxRef.get();
|
||||
final Property<E> property = propertyRef.get();
|
||||
|
||||
if (comboBox == null || property == null) {
|
||||
if (comboBox != null) {
|
||||
comboBox.getSelectionModel().selectedItemProperty().removeListener(this);
|
||||
}
|
||||
|
||||
if (property != null) {
|
||||
property.removeListener(this);
|
||||
}
|
||||
} else {
|
||||
updating = true;
|
||||
try {
|
||||
if (property == sourceProperty) {
|
||||
E newValue = property.getValue();
|
||||
comboBox.getSelectionModel().select(newValue);
|
||||
} else {
|
||||
E newValue = comboBox.getSelectionModel().getSelectedItem();
|
||||
property.setValue(newValue);
|
||||
}
|
||||
} finally {
|
||||
updating = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean wasGarbageCollected() {
|
||||
return comboBoxRef.get() == null || propertyRef.get() == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return hashCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o)
|
||||
return true;
|
||||
if (!(o instanceof EnumBidirectionalBinding))
|
||||
return false;
|
||||
|
||||
EnumBidirectionalBinding<?> that = (EnumBidirectionalBinding<?>) o;
|
||||
|
||||
final JFXComboBox<E> comboBox = this.comboBoxRef.get();
|
||||
final Property<E> property = this.propertyRef.get();
|
||||
|
||||
final JFXComboBox<?> thatComboBox = that.comboBoxRef.get();
|
||||
final Property<?> thatProperty = that.propertyRef.get();
|
||||
|
||||
if (comboBox == null || property == null || thatComboBox == null || thatProperty == null)
|
||||
return false;
|
||||
|
||||
return comboBox == thatComboBox && property == thatProperty;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind combo box selection with given enum property bidirectionally.
|
||||
* You should <b>only and always</b> use {@code bindEnum} as well as {@code unbindEnum} at the same time.
|
||||
*
|
||||
* @param comboBox the combo box being bound with {@code property}.
|
||||
* @param property the property being bound with {@code combo box}.
|
||||
* @see #unbindEnum(JFXComboBox, Property)
|
||||
* @see ExtendedProperties#selectedItemPropertyFor(ComboBox)
|
||||
*/
|
||||
public static <T extends Enum<T>> void bindEnum(JFXComboBox<T> comboBox, Property<T> property) {
|
||||
EnumBidirectionalBinding<T> binding = new EnumBidirectionalBinding<>(comboBox, property);
|
||||
|
||||
comboBox.getSelectionModel().selectedItemProperty().removeListener(binding);
|
||||
property.removeListener(binding);
|
||||
|
||||
comboBox.getSelectionModel().select(property.getValue());
|
||||
comboBox.getSelectionModel().selectedItemProperty().addListener(binding);
|
||||
property.addListener(binding);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unbind combo box selection with given enum property bidirectionally.
|
||||
* You should <b>only and always</b> use {@code bindEnum} as well as {@code unbindEnum} at the same time.
|
||||
*
|
||||
* @param comboBox the combo box being bound with the property which can be inferred by {@code bindEnum}.
|
||||
* @see #bindEnum(JFXComboBox, Property)
|
||||
* @see ExtendedProperties#selectedItemPropertyFor(ComboBox)
|
||||
*/
|
||||
public static <T extends Enum<T>> void unbindEnum(JFXComboBox<T> comboBox, Property<T> property) {
|
||||
EnumBidirectionalBinding<T> binding = new EnumBidirectionalBinding<>(comboBox, property);
|
||||
comboBox.getSelectionModel().selectedItemProperty().removeListener(binding);
|
||||
property.removeListener(binding);
|
||||
}
|
||||
|
||||
private static final class PaintBidirectionalBinding implements InvalidationListener, WeakListener {
|
||||
private final WeakReference<ColorPicker> colorPickerRef;
|
||||
private final WeakReference<Property<Paint>> propertyRef;
|
||||
@@ -932,146 +827,6 @@ public final class FXUtils {
|
||||
property.addListener(binding);
|
||||
}
|
||||
|
||||
private static final class WindowsSizeBidirectionalBinding implements InvalidationListener, WeakListener {
|
||||
private final WeakReference<JFXComboBox<String>> comboBoxRef;
|
||||
private final WeakReference<IntegerProperty> widthPropertyRef;
|
||||
private final WeakReference<IntegerProperty> heightPropertyRef;
|
||||
|
||||
private final int hashCode;
|
||||
|
||||
private boolean updating = false;
|
||||
|
||||
private WindowsSizeBidirectionalBinding(JFXComboBox<String> comboBox,
|
||||
IntegerProperty widthProperty,
|
||||
IntegerProperty heightProperty) {
|
||||
this.comboBoxRef = new WeakReference<>(comboBox);
|
||||
this.widthPropertyRef = new WeakReference<>(widthProperty);
|
||||
this.heightPropertyRef = new WeakReference<>(heightProperty);
|
||||
this.hashCode = System.identityHashCode(comboBox)
|
||||
^ System.identityHashCode(widthProperty)
|
||||
^ System.identityHashCode(heightProperty);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidated(Observable observable) {
|
||||
if (!updating) {
|
||||
var comboBox = this.comboBoxRef.get();
|
||||
var widthProperty = this.widthPropertyRef.get();
|
||||
var heightProperty = this.heightPropertyRef.get();
|
||||
|
||||
if (comboBox == null || widthProperty == null || heightProperty == null) {
|
||||
if (comboBox != null) {
|
||||
comboBox.focusedProperty().removeListener(this);
|
||||
comboBox.sceneProperty().removeListener(this);
|
||||
}
|
||||
if (widthProperty != null)
|
||||
widthProperty.removeListener(this);
|
||||
if (heightProperty != null)
|
||||
heightProperty.removeListener(this);
|
||||
} else {
|
||||
updating = true;
|
||||
try {
|
||||
int width = widthProperty.get();
|
||||
int height = heightProperty.get();
|
||||
|
||||
if (observable instanceof ReadOnlyProperty<?>
|
||||
&& ((ReadOnlyProperty<?>) observable).getBean() == comboBox) {
|
||||
String value = comboBox.valueProperty().get();
|
||||
if (value == null)
|
||||
value = "";
|
||||
int idx = value.indexOf('x');
|
||||
if (idx < 0)
|
||||
idx = value.indexOf('*');
|
||||
|
||||
if (idx < 0) {
|
||||
LOG.warning("Bad window size: " + value);
|
||||
comboBox.setValue(width + "x" + height);
|
||||
return;
|
||||
}
|
||||
|
||||
String widthStr = value.substring(0, idx).trim();
|
||||
String heightStr = value.substring(idx + 1).trim();
|
||||
|
||||
int newWidth;
|
||||
int newHeight;
|
||||
try {
|
||||
newWidth = Integer.parseInt(widthStr);
|
||||
newHeight = Integer.parseInt(heightStr);
|
||||
} catch (NumberFormatException e) {
|
||||
LOG.warning("Bad window size: " + value);
|
||||
comboBox.setValue(width + "x" + height);
|
||||
return;
|
||||
}
|
||||
|
||||
widthProperty.set(newWidth);
|
||||
heightProperty.set(newHeight);
|
||||
} else {
|
||||
comboBox.setValue(width + "x" + height);
|
||||
}
|
||||
} finally {
|
||||
updating = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean wasGarbageCollected() {
|
||||
return this.comboBoxRef.get() == null
|
||||
|| this.widthPropertyRef.get() == null
|
||||
|| this.heightPropertyRef.get() == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return hashCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (!(obj instanceof WindowsSizeBidirectionalBinding))
|
||||
return false;
|
||||
|
||||
var that = (WindowsSizeBidirectionalBinding) obj;
|
||||
|
||||
var comboBox = this.comboBoxRef.get();
|
||||
var widthProperty = this.widthPropertyRef.get();
|
||||
var heightProperty = this.heightPropertyRef.get();
|
||||
|
||||
var thatComboBox = that.comboBoxRef.get();
|
||||
var thatWidthProperty = that.widthPropertyRef.get();
|
||||
var thatHeightProperty = that.heightPropertyRef.get();
|
||||
|
||||
if (comboBox == null || widthProperty == null || heightProperty == null
|
||||
|| thatComboBox == null || thatWidthProperty == null || thatHeightProperty == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return comboBox == thatComboBox
|
||||
&& widthProperty == thatWidthProperty
|
||||
&& heightProperty == thatHeightProperty;
|
||||
}
|
||||
}
|
||||
|
||||
public static void bindWindowsSize(JFXComboBox<String> comboBox, IntegerProperty widthProperty, IntegerProperty heightProperty) {
|
||||
comboBox.setValue(widthProperty.get() + "x" + heightProperty.get());
|
||||
var binding = new WindowsSizeBidirectionalBinding(comboBox, widthProperty, heightProperty);
|
||||
comboBox.focusedProperty().addListener(binding);
|
||||
comboBox.sceneProperty().addListener(binding);
|
||||
widthProperty.addListener(binding);
|
||||
heightProperty.addListener(binding);
|
||||
}
|
||||
|
||||
public static void unbindWindowsSize(JFXComboBox<String> comboBox, IntegerProperty widthProperty, IntegerProperty heightProperty) {
|
||||
var binding = new WindowsSizeBidirectionalBinding(comboBox, widthProperty, heightProperty);
|
||||
comboBox.focusedProperty().removeListener(binding);
|
||||
comboBox.sceneProperty().removeListener(binding);
|
||||
widthProperty.removeListener(binding);
|
||||
heightProperty.removeListener(binding);
|
||||
}
|
||||
|
||||
public static void bindAllEnabled(BooleanProperty allEnabled, BooleanProperty... children) {
|
||||
int itemCount = children.length;
|
||||
int childSelectedCount = 0;
|
||||
@@ -1393,20 +1148,6 @@ public final class FXUtils {
|
||||
};
|
||||
}
|
||||
|
||||
public static <T> Callback<ListView<T>, ListCell<T>> jfxListCellFactory(Function<T, Node> graphicBuilder) {
|
||||
return view -> new JFXListCell<T>() {
|
||||
@Override
|
||||
public void updateItem(T item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
if (!empty) {
|
||||
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
|
||||
setGraphic(graphicBuilder.apply(item));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static ColumnConstraints getColumnFillingWidth() {
|
||||
ColumnConstraints constraint = new ColumnConstraints();
|
||||
constraint.setFillWidth(true);
|
||||
|
||||
@@ -34,7 +34,6 @@ import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.launch.ProcessListener;
|
||||
import org.jackhuang.hmcl.setting.StyleSheets;
|
||||
@@ -44,7 +43,6 @@ import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Log4jLevel;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
@@ -73,17 +71,15 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public class GameCrashWindow extends Stage {
|
||||
private final GameInstanceManifest manifest;
|
||||
private final HMCLGameInstance gameInstance;
|
||||
private final String memory;
|
||||
private final String total_memory;
|
||||
private final String java;
|
||||
private final LibraryAnalyzer analyzer;
|
||||
private final TextFlow reasonTextFlow = new TextFlow(new Text(i18n("game.crash.reason.unknown")));
|
||||
private final BooleanProperty loading = new SimpleBooleanProperty();
|
||||
private final TextFlow feedbackTextFlow = new TextFlow();
|
||||
|
||||
private final ManagedProcess managedProcess;
|
||||
private final DefaultGameRepository repository;
|
||||
private final ProcessListener.ExitType exitType;
|
||||
private final LaunchOptions launchOptions;
|
||||
private final View view;
|
||||
@@ -91,16 +87,14 @@ public class GameCrashWindow extends Stage {
|
||||
|
||||
private final List<Log> logs;
|
||||
|
||||
public GameCrashWindow(ManagedProcess managedProcess, ProcessListener.ExitType exitType, DefaultGameRepository repository, GameInstanceManifest manifest, LaunchOptions launchOptions, List<Log> logs) {
|
||||
public GameCrashWindow(ManagedProcess managedProcess, ProcessListener.ExitType exitType, HMCLGameInstance gameInstance, LaunchOptions launchOptions, List<Log> logs) {
|
||||
Themes.applyNativeDarkMode(this);
|
||||
|
||||
this.managedProcess = managedProcess;
|
||||
this.exitType = exitType;
|
||||
this.repository = repository;
|
||||
this.manifest = manifest;
|
||||
this.gameInstance = gameInstance;
|
||||
this.launchOptions = launchOptions;
|
||||
this.logs = logs;
|
||||
this.analyzer = LibraryAnalyzer.analyze(manifest, repository.getGameVersion(manifest).orElse(null));
|
||||
|
||||
memory = Optional.ofNullable(launchOptions.getMaxMemory()).map(i -> i + " " + i18n("settings.memory.unit.mib")).orElse("-");
|
||||
|
||||
@@ -142,7 +136,8 @@ public class GameCrashWindow extends Stage {
|
||||
|
||||
return pair(CrashReportAnalyzer.analyze(rawLog), crashReport != null ? CrashReportAnalyzer.findKeywordsFromCrashReport(crashReport) : new HashSet<>());
|
||||
}), Task.supplyAsync(() -> {
|
||||
Path latestLog = repository.getRunDirectory(manifest.id()).resolve("logs/latest.log");
|
||||
Path runDirectory = gameInstance.getRunDirectory();
|
||||
Path latestLog = runDirectory.resolve("logs/latest.log");
|
||||
if (!Files.isReadable(latestLog)) {
|
||||
return pair(new HashSet<CrashReportAnalyzer.Result>(), new HashSet<String>());
|
||||
}
|
||||
@@ -291,7 +286,7 @@ public class GameCrashWindow extends Stage {
|
||||
}
|
||||
});
|
||||
|
||||
return LogExporter.exportLogs(logFile, repository, launchOptions.getInstanceId(), logs,
|
||||
return LogExporter.exportLogs(logFile, gameInstance, launchOptions, logs,
|
||||
new CommandBuilder().addAll(managedProcess.getCommands()).toString(),
|
||||
path -> {
|
||||
try {
|
||||
@@ -342,10 +337,10 @@ public class GameCrashWindow extends Stage {
|
||||
launcher.setTitle(i18n("launcher"));
|
||||
launcher.setSubtitle(Metadata.VERSION);
|
||||
|
||||
TwoLineListItem version = new TwoLineListItem();
|
||||
version.getStyleClass().setAll("two-line-item-second-large");
|
||||
version.setTitle(i18n("game.version"));
|
||||
version.setSubtitle(GameCrashWindow.this.manifest.id().toString());
|
||||
TwoLineListItem instance = new TwoLineListItem();
|
||||
instance.getStyleClass().setAll("two-line-item-second-large");
|
||||
instance.setTitle(i18n("game.version"));
|
||||
instance.setSubtitle(GameCrashWindow.this.gameInstance.getId().toString());
|
||||
|
||||
TwoLineListItem total_memory = new TwoLineListItem();
|
||||
total_memory.getStyleClass().setAll("two-line-item-second-large");
|
||||
@@ -365,14 +360,14 @@ public class GameCrashWindow extends Stage {
|
||||
TwoLineListItem os = new TwoLineListItem();
|
||||
os.getStyleClass().setAll("two-line-item-second-large");
|
||||
os.setTitle(i18n("system.operating_system"));
|
||||
os.setSubtitle(Lang.requireNonNullElse(OperatingSystem.OS_RELEASE_NAME, OperatingSystem.SYSTEM_NAME));
|
||||
os.setSubtitle(Objects.requireNonNullElse(OperatingSystem.OS_RELEASE_NAME, OperatingSystem.SYSTEM_NAME));
|
||||
|
||||
TwoLineListItem arch = new TwoLineListItem();
|
||||
arch.getStyleClass().setAll("two-line-item-second-large");
|
||||
arch.setTitle(i18n("system.architecture"));
|
||||
arch.setSubtitle(Architecture.SYSTEM_ARCH.getDisplayName());
|
||||
|
||||
infoPane.getChildren().setAll(launcher, version, total_memory, memory, java, os, arch);
|
||||
infoPane.getChildren().setAll(launcher, instance, total_memory, memory, java, os, arch);
|
||||
}
|
||||
|
||||
HBox moddedPane = new HBox(8);
|
||||
@@ -380,15 +375,13 @@ public class GameCrashWindow extends Stage {
|
||||
moddedPane.setPadding(new Insets(8));
|
||||
moddedPane.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
for (LibraryAnalyzer.LibraryType type : LibraryAnalyzer.LibraryType.values()) {
|
||||
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);
|
||||
});
|
||||
for (GameComponentAnalyzer.Mark mark : gameInstance.getAnalyzer()) {
|
||||
if (mark.version() != null) {
|
||||
TwoLineListItem item = new TwoLineListItem();
|
||||
item.getStyleClass().setAll("two-line-item-second-large");
|
||||
item.setTitle(i18n("install.installer." + mark.componentType().getPatchId()));
|
||||
item.setSubtitle(mark.version());
|
||||
moddedPane.getChildren().add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@ public final class HTMLRenderer {
|
||||
private void applyStyle(Text text) {
|
||||
if (code) {
|
||||
text.getStyleClass().add("html-code");
|
||||
text.setStyle("-fx-font-family: \"%s\";".formatted(Lang.requireNonNullElse(settings().logFontFamilyProperty().get(), FXUtils.DEFAULT_MONOSPACE_FONT)));
|
||||
text.setStyle("-fx-font-family: \"%s\";".formatted(Objects.requireNonNullElse(settings().logFontFamilyProperty().get(), FXUtils.DEFAULT_MONOSPACE_FONT)));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -34,26 +34,22 @@ import javafx.scene.control.Skin;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.layout.*;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.game.GameComponentType;
|
||||
import org.jackhuang.hmcl.setting.GameInstanceIconType;
|
||||
import org.jackhuang.hmcl.ui.construct.ImageContainer;
|
||||
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.*;
|
||||
|
||||
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.*;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
/**
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public class InstallerItem extends Control {
|
||||
private final String id;
|
||||
private final GameComponentType type;
|
||||
private final GameInstanceIconType iconType;
|
||||
private final Style style;
|
||||
private final ObjectProperty<InstalledState> versionProperty = new SimpleObjectProperty<>(this, "version", null);
|
||||
@@ -83,30 +79,14 @@ public class InstallerItem extends Control {
|
||||
CARD,
|
||||
}
|
||||
|
||||
public InstallerItem(LibraryAnalyzer.LibraryType id, Style style) {
|
||||
this(id.getPatchId(), style);
|
||||
}
|
||||
|
||||
public InstallerItem(String id, Style style) {
|
||||
this.id = id;
|
||||
public InstallerItem(GameComponentType type, Style style) {
|
||||
this.type = type;
|
||||
this.style = style;
|
||||
|
||||
iconType = switch (id) {
|
||||
case "game" -> GameInstanceIconType.GRASS;
|
||||
case "fabric", "fabric-api" -> GameInstanceIconType.FABRIC;
|
||||
case "legacyfabric", "legacyfabric-api" -> GameInstanceIconType.LEGACY_FABRIC;
|
||||
case "forge" -> GameInstanceIconType.FORGE;
|
||||
case "cleanroom" -> GameInstanceIconType.CLEANROOM;
|
||||
case "liteloader" -> GameInstanceIconType.CHICKEN;
|
||||
case "optifine" -> GameInstanceIconType.OPTIFINE;
|
||||
case "quilt", "quilt-api" -> GameInstanceIconType.QUILT;
|
||||
case "neoforge" -> GameInstanceIconType.NEO_FORGE;
|
||||
default -> null;
|
||||
};
|
||||
this.iconType = GameInstanceIconType.getIconType(type);
|
||||
}
|
||||
|
||||
public String getLibraryId() {
|
||||
return id;
|
||||
public GameComponentType getComponentType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public ObjectProperty<InstalledState> versionProperty() {
|
||||
@@ -149,7 +129,7 @@ public class InstallerItem extends Control {
|
||||
public final static class InstallerItemGroup {
|
||||
private final InstallerItem game;
|
||||
|
||||
private final InstallerItem[] libraries;
|
||||
private final InstallerItem[] components;
|
||||
|
||||
private Set<InstallerItem> getIncompatibles(Map<InstallerItem, Set<InstallerItem>> incompatibleMap, InstallerItem item) {
|
||||
return incompatibleMap.computeIfAbsent(item, it -> new HashSet<>());
|
||||
@@ -175,19 +155,19 @@ public class InstallerItem extends Control {
|
||||
}
|
||||
}
|
||||
|
||||
public InstallerItemGroup(String gameVersion, Style style) {
|
||||
game = new InstallerItem(MINECRAFT, style);
|
||||
InstallerItem fabric = new InstallerItem(FABRIC, style);
|
||||
InstallerItem fabricApi = new InstallerItem(FABRIC_API, style);
|
||||
InstallerItem forge = new InstallerItem(FORGE, style);
|
||||
InstallerItem cleanroom = new InstallerItem(CLEANROOM, style);
|
||||
InstallerItem legacyfabric = new InstallerItem(LEGACY_FABRIC, style);
|
||||
InstallerItem legacyfabricApi = new InstallerItem(LEGACY_FABRIC_API, style);
|
||||
InstallerItem neoForge = new InstallerItem(NEO_FORGE, style);
|
||||
InstallerItem liteLoader = new InstallerItem(LITELOADER, style);
|
||||
InstallerItem optiFine = new InstallerItem(OPTIFINE, style);
|
||||
InstallerItem quilt = new InstallerItem(QUILT, style);
|
||||
InstallerItem quiltApi = new InstallerItem(QUILT_API, style);
|
||||
public InstallerItemGroup(GameVersionNumber gameVersion, Style style) {
|
||||
game = new InstallerItem(GameComponentType.GAME, style);
|
||||
InstallerItem fabric = new InstallerItem(GameComponentType.FABRIC, style);
|
||||
InstallerItem fabricApi = new InstallerItem(GameComponentType.FABRIC_API, style);
|
||||
InstallerItem forge = new InstallerItem(GameComponentType.FORGE, style);
|
||||
InstallerItem cleanroom = new InstallerItem(GameComponentType.CLEANROOM, style);
|
||||
InstallerItem legacyfabric = new InstallerItem(GameComponentType.LEGACY_FABRIC, style);
|
||||
InstallerItem legacyfabricApi = new InstallerItem(GameComponentType.LEGACY_FABRIC_API, style);
|
||||
InstallerItem neoForge = new InstallerItem(GameComponentType.NEO_FORGE, style);
|
||||
InstallerItem liteLoader = new InstallerItem(GameComponentType.LITELOADER, style);
|
||||
InstallerItem optiFine = new InstallerItem(GameComponentType.OPTIFINE, style);
|
||||
InstallerItem quilt = new InstallerItem(GameComponentType.QUILT, style);
|
||||
InstallerItem quiltApi = new InstallerItem(GameComponentType.QUILT_API, style);
|
||||
|
||||
Map<InstallerItem, Set<InstallerItem>> incompatibleMap = new HashMap<>();
|
||||
mutualIncompatible(incompatibleMap, forge, fabric, quilt, neoForge, cleanroom, legacyfabric);
|
||||
@@ -217,7 +197,7 @@ public class InstallerItem extends Control {
|
||||
for (InstallerItem other : incompatibleItems) {
|
||||
InstalledState otherVersion = other.versionProperty.get();
|
||||
if (otherVersion != null) {
|
||||
return new IncompatibleState(other.id, otherVersion.version);
|
||||
return new IncompatibleState(other.type.getPatchId(), otherVersion.version);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,7 +206,7 @@ public class InstallerItem extends Control {
|
||||
}
|
||||
|
||||
if (gameVersion != null) {
|
||||
game.versionProperty.set(new InstalledState(gameVersion, false, false));
|
||||
game.versionProperty.set(new InstalledState(gameVersion.toString(), false, false));
|
||||
}
|
||||
|
||||
InstallerItem[] all = {game, forge, neoForge, liteLoader, optiFine, fabric, fabricApi, quilt, quiltApi, legacyfabric, legacyfabricApi, cleanroom};
|
||||
@@ -235,22 +215,19 @@ public class InstallerItem extends Control {
|
||||
if (!item.resolvedStateProperty.isBound()) {
|
||||
item.resolvedStateProperty.bind(Bindings.createObjectBinding(() -> {
|
||||
InstalledState itemVersion = item.versionProperty.get();
|
||||
if (itemVersion != null) {
|
||||
return itemVersion;
|
||||
}
|
||||
return InstallableState.INSTANCE;
|
||||
return Objects.requireNonNullElse(itemVersion, InstallableState.INSTANCE);
|
||||
}, item.versionProperty));
|
||||
}
|
||||
}
|
||||
|
||||
if (gameVersion == null) {
|
||||
this.libraries = all;
|
||||
} else if (gameVersion.equals("1.12.2")) {
|
||||
this.libraries = new InstallerItem[]{game, forge, cleanroom, liteLoader, legacyfabric, legacyfabricApi, optiFine};
|
||||
} else if (GameVersionNumber.compare(gameVersion, "1.13.2") <= 0) {
|
||||
this.libraries = new InstallerItem[]{game, forge, liteLoader, optiFine, legacyfabric, legacyfabricApi};
|
||||
this.components = all;
|
||||
} else if (gameVersion.compareTo("1.12.2") == 0) {
|
||||
this.components = new InstallerItem[]{game, forge, cleanroom, liteLoader, legacyfabric, legacyfabricApi, optiFine};
|
||||
} else if (gameVersion.compareTo("1.13.2") <= 0) {
|
||||
this.components = new InstallerItem[]{game, forge, liteLoader, optiFine, legacyfabric, legacyfabricApi};
|
||||
} else {
|
||||
this.libraries = new InstallerItem[]{game, forge, neoForge, optiFine, fabric, fabricApi, quilt, quiltApi};
|
||||
this.components = new InstallerItem[]{game, forge, neoForge, optiFine, fabric, fabricApi, quilt, quiltApi};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,8 +235,8 @@ public class InstallerItem extends Control {
|
||||
return game;
|
||||
}
|
||||
|
||||
public InstallerItem[] getLibraries() {
|
||||
return libraries;
|
||||
public InstallerItem[] getComponents() {
|
||||
return components;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,7 +286,7 @@ public class InstallerItem extends Control {
|
||||
nameLabel.getStyleClass().add("installer-item-name");
|
||||
nameLabel.setMouseTransparent(true);
|
||||
pane.getChildren().add(nameLabel);
|
||||
nameLabel.textProperty().set(I18n.hasKey("install.installer." + control.id) ? i18n("install.installer." + control.id) : control.id);
|
||||
nameLabel.textProperty().set(I18n.hasKey("install.installer." + control.type.getPatchId()) ? i18n("install.installer." + control.type.getPatchId()) : control.type.getPatchId());
|
||||
HBox.setMargin(nameLabel, new Insets(0, 4, 0, 4));
|
||||
|
||||
Label statusLabel = new Label();
|
||||
@@ -355,7 +332,7 @@ public class InstallerItem extends Control {
|
||||
pane.getChildren().add(buttonsContainer);
|
||||
|
||||
JFXButton removeButton = FXUtils.newToggleButton4(SVG.CLOSE);
|
||||
if (control.id.equals(MINECRAFT.getPatchId())) {
|
||||
if (control.type == GameComponentType.GAME) {
|
||||
removeButton.setVisible(false);
|
||||
} else {
|
||||
removeButton.visibleProperty().bind(Bindings.createBooleanBinding(() -> {
|
||||
|
||||
@@ -40,7 +40,6 @@ import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.util.CircularArrayList;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Log4jLevel;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.platform.ManagedProcess;
|
||||
@@ -52,10 +51,7 @@ import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.SettingsManager.settings;
|
||||
@@ -324,7 +320,7 @@ public final class LogWindow extends Stage {
|
||||
listView.scrollTo(listView.getItems().size() - 1);
|
||||
});
|
||||
|
||||
listView.setStyle("-fx-font-family: \"" + Lang.requireNonNullElse(settings().logFontFamilyProperty().get(), FXUtils.DEFAULT_MONOSPACE_FONT)
|
||||
listView.setStyle("-fx-font-family: \"" + Objects.requireNonNullElse(settings().logFontFamilyProperty().get(), FXUtils.DEFAULT_MONOSPACE_FONT)
|
||||
+ "\"; -fx-font-size: " + settings().logFontSizeProperty().get() + "px;");
|
||||
listView.setCellFactory(x -> new ListCell<>() {
|
||||
{
|
||||
|
||||
@@ -18,13 +18,7 @@
|
||||
package org.jackhuang.hmcl.ui.account;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ListProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleListProperty;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.FXCollections;
|
||||
@@ -47,7 +41,6 @@ import org.jackhuang.hmcl.ui.construct.ClassTitle;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
|
||||
|
||||
@@ -56,7 +49,6 @@ import java.util.Locale;
|
||||
import static org.jackhuang.hmcl.setting.SettingsManager.userSettings;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.createSelectedItemPropertyFor;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class AccountListPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
static final BooleanProperty RESTRICTED = new SimpleBooleanProperty(true);
|
||||
@@ -173,13 +165,7 @@ public final class AccountListPage extends DecoratorAnimatedPage implements Deco
|
||||
|
||||
ObservableValue<String> title = BindingMapping.of(server, AuthlibInjectorServer::getName);
|
||||
item.titleProperty().bind(title);
|
||||
String host = "";
|
||||
try {
|
||||
host = NetworkUtils.toURI(server.getUrl()).getHost();
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOG.warning("Unparsable authlib-injector server url " + server.getUrl(), e);
|
||||
}
|
||||
item.subtitleProperty().set(host);
|
||||
item.setSubtitle(server.getDisplayHostUrl());
|
||||
Tooltip tooltip = new Tooltip();
|
||||
tooltip.textProperty().bind(Bindings.format("%s (%s)", title, server.getUrl()));
|
||||
FXUtils.installFastTooltip(item, tooltip);
|
||||
|
||||
@@ -67,11 +67,10 @@ import static java.util.Collections.emptyList;
|
||||
import static java.util.Collections.unmodifiableList;
|
||||
import static javafx.beans.binding.Bindings.bindContent;
|
||||
import static javafx.beans.binding.Bindings.createBooleanBinding;
|
||||
import static org.jackhuang.hmcl.setting.SettingsManager.settings;
|
||||
import static org.jackhuang.hmcl.setting.SettingsManager.getAuthlibInjectorServers;
|
||||
import static org.jackhuang.hmcl.setting.SettingsManager.settings;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.*;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.classPropertyFor;
|
||||
|
||||
public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
private static final Pattern USERNAME_CHECKER_PATTERN = Pattern.compile("^[A-Za-z0-9_]+$");
|
||||
@@ -388,20 +387,22 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
add(lblServers, 0, rowIndex);
|
||||
|
||||
cboServers = new JFXComboBox<>();
|
||||
cboServers.setCellFactory(jfxListCellFactory(server -> new TwoLineListItem(server.getName(), server.getUrl())));
|
||||
cboServers.setConverter(stringConverter(AuthlibInjectorServer::getName));
|
||||
cboServers.setConverter(stringConverter(it -> String.format("%s (%s)", it.getName(), it.getDisplayHostUrl())));
|
||||
bindContent(cboServers.getItems(), getAuthlibInjectorServers());
|
||||
cboServers.getItems().addListener(onInvalidating(
|
||||
() -> Platform.runLater( // the selection will not be updated as expected if we call it immediately
|
||||
cboServers.getSelectionModel()::selectFirst)));
|
||||
cboServers.getSelectionModel().selectFirst();
|
||||
cboServers.setPromptText(i18n("account.injector.empty"));
|
||||
|
||||
Label noServersLabel = new Label(i18n("account.injector.empty"));
|
||||
BooleanBinding noServers = createBooleanBinding(cboServers.getItems()::isEmpty, cboServers.getItems());
|
||||
classPropertyFor(cboServers, "jfx-combo-box-warning").bind(noServers);
|
||||
classPropertyFor(cboServers, "jfx-combo-box").bind(noServers.not());
|
||||
|
||||
HBox.setHgrow(cboServers, Priority.ALWAYS);
|
||||
HBox.setMargin(cboServers, new Insets(0, 10, 0, 0));
|
||||
cboServers.setMaxWidth(Double.MAX_VALUE);
|
||||
HBox.setHgrow(noServersLabel, Priority.ALWAYS);
|
||||
HBox.setMargin(noServersLabel, new Insets(0, 10, 0, 0));
|
||||
noServersLabel.setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
HBox linksContainer = new HBox();
|
||||
linksContainer.setAlignment(Pos.CENTER);
|
||||
@@ -419,7 +420,13 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
Controllers.dialog(new AddAuthlibInjectorServerPane());
|
||||
});
|
||||
|
||||
HBox boxServers = new HBox(cboServers, linksContainer, btnAddServer);
|
||||
noServersLabel.visibleProperty().bind(noServers);
|
||||
noServersLabel.managedProperty().bind(noServers);
|
||||
cboServers.visibleProperty().bind(noServers.not());
|
||||
cboServers.managedProperty().bind(noServers.not());
|
||||
|
||||
HBox boxServers = new HBox(cboServers, noServersLabel, linksContainer, btnAddServer);
|
||||
boxServers.setAlignment(Pos.CENTER_LEFT);
|
||||
add(boxServers, 1, rowIndex);
|
||||
|
||||
rowIndex++;
|
||||
|
||||
@@ -29,9 +29,7 @@ import javafx.geometry.Pos;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.FlowPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.shape.SVGPath;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.AuthInfo;
|
||||
@@ -49,10 +47,10 @@ import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.upgrade.IntegrityChecker;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.QrCodeUtils;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@@ -141,20 +139,22 @@ public class MicrosoftAccountLoginPane extends JFXDialogLayout implements Dialog
|
||||
setBody(rootContainer);
|
||||
rootContainer.setAlignment(Pos.TOP_CENTER);
|
||||
|
||||
if (Accounts.OAUTH_CALLBACK.getClientId().isEmpty()) {
|
||||
var snapshotHint = new HintPane(MessageDialogPane.MessageType.WARNING);
|
||||
StackPane warningPane = new StackPane();
|
||||
|
||||
if (!IntegrityChecker.isOfficial()) {
|
||||
var unofficialHintPane = new HintPane(MessageDialogPane.MessageType.WARNING);
|
||||
unofficialHintPane.setSegment(i18n("unofficial.hint"));
|
||||
warningPane.getChildren().add(unofficialHintPane);
|
||||
} else if (Accounts.OAUTH_CALLBACK.getClientId().isEmpty()) {
|
||||
HintPane snapshotHint = new HintPane(MessageDialogPane.MessageType.WARNING);
|
||||
snapshotHint.setSegment(i18n("account.methods.microsoft.snapshot"));
|
||||
rootContainer.getChildren().add(snapshotHint);
|
||||
warningPane.getChildren().add(snapshotHint);
|
||||
btnLogin.setDisable(true);
|
||||
loginButtonSpinner.setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IntegrityChecker.isOfficial()) {
|
||||
var unofficialHintPane = new HintPane(MessageDialogPane.MessageType.WARNING);
|
||||
unofficialHintPane.setSegment(i18n("unofficial.hint"));
|
||||
rootContainer.getChildren().add(unofficialHintPane);
|
||||
}
|
||||
rootContainer.getChildren().add(warningPane);
|
||||
|
||||
if (currentStep instanceof Step.Init) {
|
||||
btnLogin.setOnAction(e -> this.step.set(new Step.StartAuthorizationCodeLogin()));
|
||||
@@ -198,6 +198,20 @@ public class MicrosoftAccountLoginPane extends JFXDialogLayout implements Dialog
|
||||
} else if (currentStep instanceof Step.WaitForScanQrCode wait) {
|
||||
loginButtonSpinner.setLoading(true);
|
||||
|
||||
VBox leftVBox = new VBox(6);
|
||||
leftVBox.setAlignment(Pos.CENTER);
|
||||
VBox rightVBox = new VBox(6);
|
||||
|
||||
HBox hbox = new HBox(leftVBox, rightVBox);
|
||||
|
||||
leftVBox.prefWidthProperty().bind(hbox.widthProperty().divide(2));
|
||||
rightVBox.prefWidthProperty().bind(hbox.widthProperty().divide(2));
|
||||
leftVBox.setMaxWidth(Double.MAX_VALUE);
|
||||
rightVBox.setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
HBox.setHgrow(leftVBox, Priority.ALWAYS);
|
||||
HBox.setHgrow(rightVBox, Priority.ALWAYS);
|
||||
|
||||
String scanUri = "https://www.microsoft.com/link".equals(wait.verificationUri())
|
||||
? "https://www.microsoft.com/link?otc=" + wait.userCode()
|
||||
: wait.verificationUri();
|
||||
@@ -217,7 +231,7 @@ public class MicrosoftAccountLoginPane extends JFXDialogLayout implements Dialog
|
||||
|
||||
var lblCode = new Label(wait.userCode());
|
||||
lblCode.getStyleClass().add("code-label");
|
||||
lblCode.setStyle("-fx-font-family: \"" + Lang.requireNonNullElse(settings().logFontFamilyProperty().get(), FXUtils.DEFAULT_MONOSPACE_FONT) + "\";");
|
||||
lblCode.setStyle("-fx-font-family: \"" + Objects.requireNonNullElse(settings().logFontFamilyProperty().get(), FXUtils.DEFAULT_MONOSPACE_FONT) + "\";");
|
||||
|
||||
var codeBox = new StackPane(lblCode);
|
||||
codeBox.getStyleClass().add("code-box");
|
||||
@@ -225,7 +239,10 @@ public class MicrosoftAccountLoginPane extends JFXDialogLayout implements Dialog
|
||||
FXUtils.onClicked(codeBox, () -> FXUtils.copyText(wait.userCode()));
|
||||
codeBox.setMaxWidth(USE_PREF_SIZE);
|
||||
|
||||
rootContainer.getChildren().addAll(deviceHint, new Group(qrCode), codeBox);
|
||||
leftVBox.getChildren().setAll(new Group(qrCode), codeBox);
|
||||
rightVBox.getChildren().setAll(warningPane, deviceHint);
|
||||
|
||||
rootContainer.getChildren().setAll(hbox);
|
||||
} else if (currentStep instanceof Step.DeviceLoginCompleted) {
|
||||
loginButtonSpinner.setLoading(true);
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ public final class FontComboBox extends JFXComboBox<String> {
|
||||
|
||||
styleProperty().bind(Bindings.concat("-fx-font-family: \"", valueProperty(), "\""));
|
||||
|
||||
setCellFactory(listView -> new JFXListCell<String>() {
|
||||
setCellFactory(listView -> new JFXListCell<>() {
|
||||
@Override
|
||||
public void updateItem(String item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
@@ -30,6 +30,7 @@ public abstract class MDListCell<T> extends ListCell<T> {
|
||||
|
||||
private final StackPane container = new StackPane();
|
||||
private final StackPane root = new StackPane();
|
||||
private final RipplerContainer ripplerContainer = new RipplerContainer(container);
|
||||
|
||||
public MDListCell(JFXListView<T> listView) {
|
||||
|
||||
@@ -37,7 +38,6 @@ public abstract class MDListCell<T> extends ListCell<T> {
|
||||
setGraphic(null);
|
||||
|
||||
root.getStyleClass().add("md-list-cell");
|
||||
RipplerContainer ripplerContainer = new RipplerContainer(container);
|
||||
root.getChildren().setAll(ripplerContainer);
|
||||
|
||||
Region clippedContainer = (Region) listView.lookup(".clipped-container");
|
||||
@@ -60,8 +60,10 @@ public abstract class MDListCell<T> extends ListCell<T> {
|
||||
|
||||
if (oldItem == item && oldEmpty == empty) return;
|
||||
|
||||
ripplerContainer.releaseRippleImmediately();
|
||||
|
||||
updateControl(item, empty);
|
||||
if (empty) {
|
||||
if (empty || item == null) {
|
||||
setGraphic(null);
|
||||
} else {
|
||||
setGraphic(root);
|
||||
|
||||
@@ -205,6 +205,10 @@ public class RipplerContainer extends StackPane {
|
||||
ripplerFillProperty().set(ripplerFill);
|
||||
}
|
||||
|
||||
public void releaseRippleImmediately() {
|
||||
buttonRippler.releaseRippleImmediately();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
|
||||
return getClassCssMetaData();
|
||||
|
||||
@@ -27,6 +27,7 @@ import javafx.scene.layout.*;
|
||||
import org.jackhuang.hmcl.task.FetchTask;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.task.TaskListener;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
@@ -113,7 +114,15 @@ public class TaskExecutorDialogPane extends BorderPane {
|
||||
executor.addTaskListener(new TaskListener() {
|
||||
@Override
|
||||
public void onStop(boolean success, TaskExecutor executor) {
|
||||
Platform.runLater(() -> fireEvent(new DialogCloseEvent()));
|
||||
Platform.runLater(() -> {
|
||||
fireEvent(new DialogCloseEvent());
|
||||
|
||||
if (Controllers.AUTO_TRIM_HEAP) {
|
||||
// Trim heap memory when tasks finish, allowing GC to return memory to the OS faster,
|
||||
// prevent excessive peak memory from consuming system resources.
|
||||
Platform.runLater(Controllers::trimHeap);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -882,6 +882,7 @@ public final class Decorator {
|
||||
private void playOpenAnimation() {
|
||||
if (!AnimationUtils.playWindowAnimation()) {
|
||||
stopWindowAnimation();
|
||||
Controllers.trimHeap();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -896,7 +897,13 @@ public final class Decorator {
|
||||
new KeyValue(root.scaleXProperty(), 1, Motion.EASE),
|
||||
new KeyValue(root.scaleYProperty(), 1, Motion.EASE),
|
||||
new KeyValue(root.scaleZProperty(), 1, Motion.EASE)));
|
||||
playWindowAnimation(timeline, this::resetRootTransform);
|
||||
playWindowAnimation(timeline, () -> {
|
||||
resetRootTransform();
|
||||
|
||||
// Proactively triggering GC after the window open animation typically
|
||||
// reduces physical memory usage after HMCL startup.
|
||||
Controllers.trimHeap();
|
||||
});
|
||||
// Apply the initial frame before WINDOW_SHOWING returns so the window cannot flash at full opacity.
|
||||
timeline.jumpTo(Duration.ZERO);
|
||||
}
|
||||
@@ -1018,6 +1025,13 @@ public final class Decorator {
|
||||
return;
|
||||
}
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS
|
||||
&& event.getEventType() == KeyEvent.KEY_PRESSED
|
||||
&& event.isMetaDown()
|
||||
&& event.getCode() == KeyCode.Q) {
|
||||
return;
|
||||
}
|
||||
|
||||
Node newTarget;
|
||||
@Nullable JFXDialogPane currentDialogPane =
|
||||
(JFXDialogPane) getDialogContainer().getProperties()
|
||||
|
||||
@@ -22,7 +22,10 @@ import com.jfoenix.controls.JFXTextField;
|
||||
import com.jfoenix.validation.RequiredFieldValidator;
|
||||
import com.jfoenix.validation.base.ValidatorBase;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.geometry.Insets;
|
||||
@@ -38,8 +41,13 @@ import org.jackhuang.hmcl.setting.GameDirectoryManager;
|
||||
import org.jackhuang.hmcl.setting.SettingsManager;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.LineFileChooserButton;
|
||||
import org.jackhuang.hmcl.ui.construct.LineToggleButton;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.PageCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.FXThread;
|
||||
import org.jackhuang.hmcl.util.PortablePath;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.LocalizedText;
|
||||
@@ -187,6 +195,7 @@ public final class GameDirectoryPage extends BorderPane implements DecoratorPage
|
||||
txtGameDirectoryName.setText(suggestedName);
|
||||
}
|
||||
};
|
||||
locationChangeListener.changed(null, this.location.get(), this.location.get());
|
||||
locationProperty().addListener(locationChangeListener);
|
||||
|
||||
txtGameDirectoryName.textProperty().addListener(new ChangeListener<>() {
|
||||
@@ -200,8 +209,8 @@ public final class GameDirectoryPage extends BorderPane implements DecoratorPage
|
||||
});
|
||||
}
|
||||
|
||||
/// Saves the edited game directory or adds a new entry to the appropriate game directory store.
|
||||
private void onSave() {
|
||||
@FXThread
|
||||
private void doSave() {
|
||||
if (gameDirectory != null) {
|
||||
LocalizedText name = LocalizedText.plain(txtGameDirectoryName.getText());
|
||||
PortablePath path = StringUtils.isNotBlank(getLocation()) ? createPortableLocation() : gameDirectory.getPath();
|
||||
@@ -249,6 +258,14 @@ public final class GameDirectoryPage extends BorderPane implements DecoratorPage
|
||||
fireEvent(new PageCloseEvent());
|
||||
}
|
||||
|
||||
/// Saves the edited game directory or adds a new entry to the appropriate game directory store.
|
||||
@FXThread
|
||||
private void onSave() {
|
||||
if (Objects.equals(Path.of(getLocation()).getRoot(), Path.of(getLocation()))) {
|
||||
Controllers.confirm(i18n("game_directory.root"), i18n("message.warning"), MessageDialogPane.MessageType.WARNING, this::doSave, null);
|
||||
} else doSave();
|
||||
}
|
||||
|
||||
/// Creates the portable path for the current location according to the relative-path toggle.
|
||||
private PortablePath createPortableLocation() {
|
||||
if (toggleUseRelativePath.isSelected()) {
|
||||
|
||||
@@ -28,9 +28,8 @@ import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.FlowPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.game.GameComponentType;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
@@ -41,6 +40,7 @@ import org.jackhuang.hmcl.ui.wizard.Navigation;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardPage;
|
||||
import org.jackhuang.hmcl.util.SettingsMap;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.SettingsManager.state;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
@@ -58,37 +58,37 @@ public abstract class AbstractInstallersPage extends Control implements WizardPa
|
||||
|
||||
public AbstractInstallersPage(WizardController controller, String gameVersion, DownloadProvider downloadProvider) {
|
||||
this.controller = controller;
|
||||
this.group = new InstallerItem.InstallerItemGroup(gameVersion, getInstallerItemStyle());
|
||||
this.group = new InstallerItem.InstallerItemGroup(GameVersionNumber.asGameVersion(gameVersion), getInstallerItemStyle());
|
||||
|
||||
for (InstallerItem library : group.getLibraries()) {
|
||||
String libraryId = library.getLibraryId();
|
||||
if (libraryId.equals(LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId())) continue;
|
||||
library.setOnInstall(() -> {
|
||||
for (InstallerItem component : group.getComponents()) {
|
||||
GameComponentType type = component.getComponentType();
|
||||
if (type == GameComponentType.GAME) continue;
|
||||
component.setOnInstall(() -> {
|
||||
if (!Boolean.TRUE.equals(state().getShownTips().get(FABRIC_QUILT_API_TIP))
|
||||
&& (LibraryAnalyzer.LibraryType.FABRIC_API.getPatchId().equals(libraryId)
|
||||
|| LibraryAnalyzer.LibraryType.QUILT_API.getPatchId().equals(libraryId)
|
||||
|| LibraryAnalyzer.LibraryType.LEGACY_FABRIC_API.getPatchId().equals(libraryId))) {
|
||||
&& (type == GameComponentType.FABRIC_API
|
||||
|| type == GameComponentType.QUILT_API
|
||||
|| type == GameComponentType.LEGACY_FABRIC_API)) {
|
||||
Controllers.dialog(new MessageDialogPane.Builder(
|
||||
i18n("install.installer.fabric-quilt-api.warning", i18n("install.installer." + libraryId)),
|
||||
i18n("install.installer.fabric-quilt-api.warning", i18n("install.installer." + type.getPatchId())),
|
||||
i18n("message.warning"),
|
||||
MessageDialogPane.MessageType.WARNING
|
||||
).ok(null).addCancel(i18n("button.do_not_show_again"), () -> state().getShownTips().put(FABRIC_QUILT_API_TIP, true)).build());
|
||||
}
|
||||
|
||||
if (!(library.resolvedStateProperty().get() instanceof InstallerItem.IncompatibleState))
|
||||
if (!(component.resolvedStateProperty().get() instanceof InstallerItem.IncompatibleState))
|
||||
controller.onNext(
|
||||
new VersionsPage(
|
||||
controller,
|
||||
i18n("install.installer.choose", i18n("install.installer." + libraryId)),
|
||||
i18n("install.installer.choose", i18n("install.installer." + type.getPatchId())),
|
||||
gameVersion,
|
||||
downloadProvider,
|
||||
libraryId,
|
||||
type,
|
||||
() -> controller.onPrev(false, Navigation.NavigationDirection.PREVIOUS)
|
||||
), Navigation.NavigationDirection.NEXT
|
||||
);
|
||||
});
|
||||
library.setOnRemove(() -> {
|
||||
controller.getSettings().remove(libraryId);
|
||||
component.setOnRemove(() -> {
|
||||
controller.getSettings().remove(type.getPatchId());
|
||||
reload();
|
||||
});
|
||||
}
|
||||
@@ -166,16 +166,16 @@ public abstract class AbstractInstallersPage extends Control implements WizardPa
|
||||
}
|
||||
|
||||
{
|
||||
InstallerItem[] libraries = control.group.getLibraries();
|
||||
InstallerItem[] components = control.group.getComponents();
|
||||
|
||||
FlowPane libraryPane = new FlowPane(16, 16, libraries);
|
||||
FlowPane libraryPane = new FlowPane(16, 16, components);
|
||||
ScrollPane scrollPane = new ScrollPane(libraryPane);
|
||||
scrollPane.setFitToWidth(true);
|
||||
scrollPane.setFitToHeight(true);
|
||||
BorderPane.setMargin(scrollPane, new Insets(16, 0, 16, 0));
|
||||
root.setCenter(scrollPane);
|
||||
|
||||
if (libraries.length <= 8)
|
||||
if (components.length <= 8)
|
||||
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,11 +20,11 @@ package org.jackhuang.hmcl.ui.download;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import org.jackhuang.hmcl.download.ComponentRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||
import org.jackhuang.hmcl.game.GameComponentType;
|
||||
import org.jackhuang.hmcl.game.GameInstanceManifest;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.HMCLGameInstance;
|
||||
import org.jackhuang.hmcl.ui.InstallerItem;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
@@ -32,29 +32,29 @@ import org.jackhuang.hmcl.util.SettingsMap;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.MINECRAFT;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
class AdditionalInstallersPage extends AbstractInstallersPage {
|
||||
protected final BooleanProperty compatible = new SimpleBooleanProperty();
|
||||
protected final HMCLGameRepository repository;
|
||||
protected final String gameVersion;
|
||||
protected final GameInstanceManifest manifest;
|
||||
protected final HMCLGameInstance instance;
|
||||
|
||||
public AdditionalInstallersPage(String gameVersion, GameInstanceManifest manifest, WizardController controller, HMCLGameRepository repository, DownloadProvider downloadProvider) {
|
||||
public AdditionalInstallersPage(HMCLGameInstance instance, String gameVersion, WizardController controller, DownloadProvider downloadProvider) {
|
||||
super(controller, gameVersion, downloadProvider);
|
||||
this.instance = instance;
|
||||
this.gameVersion = gameVersion;
|
||||
this.manifest = manifest;
|
||||
this.repository = repository;
|
||||
this.manifest = instance.getManifest();
|
||||
|
||||
txtName.setText(manifest.id().toString());
|
||||
txtName.setText(instance.getId().id());
|
||||
txtName.setEditable(false);
|
||||
|
||||
for (InstallerItem library : group.getLibraries()) {
|
||||
String libraryId = library.getLibraryId();
|
||||
if (libraryId.equals("game")) continue;
|
||||
library.setOnRemove(() -> {
|
||||
controller.getSettings().put(libraryId, new UpdateInstallerWizardProvider.RemoveVersionAction(libraryId));
|
||||
for (InstallerItem component : group.getComponents()) {
|
||||
if (component.getComponentType() == GameComponentType.GAME) continue;
|
||||
component.setOnRemove(() -> {
|
||||
controller.getSettings().put(
|
||||
component.getComponentType().getPatchId(),
|
||||
new UpdateInstallerWizardProvider.RemoveComponentAction(component.getComponentType()));
|
||||
reload();
|
||||
});
|
||||
}
|
||||
@@ -69,38 +69,34 @@ class AdditionalInstallersPage extends AbstractInstallersPage {
|
||||
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return i18n("settings.tabs.installers");
|
||||
return i18n("install.change_version.title", instance.getId().id());
|
||||
}
|
||||
|
||||
private String getVersion(String id) {
|
||||
return Optional.ofNullable(controller.getSettings().get(id))
|
||||
.flatMap(it -> Lang.tryCast(it, RemoteVersion.class))
|
||||
.map(RemoteVersion::getSelfVersion).orElse(null);
|
||||
private String getVersion(GameComponentType type) {
|
||||
return Optional.ofNullable(controller.getSettings().get(type.getPatchId()))
|
||||
.flatMap(it -> Lang.tryCast(it, ComponentRemoteVersion.class))
|
||||
.map(ComponentRemoteVersion::getSelfVersion).orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void reload() {
|
||||
GameInstanceManifest.Resolved resolvedManifest = repository.resolve(manifest);
|
||||
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(resolvedManifest, repository.getGameVersion(manifest).orElse(null));
|
||||
String game = analyzer.getVersion(MINECRAFT).orElse(null);
|
||||
String currentGameVersion = Lang.nonNull(getVersion("game"), game);
|
||||
|
||||
boolean gameVersionChanged = !instance.getVersion().toString().equals(getVersion(GameComponentType.GAME));
|
||||
boolean compatible = true;
|
||||
|
||||
for (InstallerItem library : group.getLibraries()) {
|
||||
String libraryId = library.getLibraryId();
|
||||
String version = analyzer.getVersion(libraryId).orElse(null);
|
||||
String libraryVersion = Lang.requireNonNullElse(getVersion(libraryId), version);
|
||||
boolean alreadyInstalled = version != null && !(controller.getSettings().get(libraryId) instanceof UpdateInstallerWizardProvider.RemoveVersionAction);
|
||||
if (!"game".equals(libraryId) && currentGameVersion != null && !currentGameVersion.equals(game) && getVersion(libraryId) == null && alreadyInstalled) {
|
||||
for (InstallerItem component : group.getComponents()) {
|
||||
GameComponentType componentType = component.getComponentType();
|
||||
String version = instance.getComponentVersion(component.getComponentType());
|
||||
String libraryVersion = Lang.requireNonNullElse(getVersion(componentType), version);
|
||||
boolean alreadyInstalled = version != null && !(controller.getSettings().get(componentType.getPatchId()) instanceof UpdateInstallerWizardProvider.RemoveComponentAction);
|
||||
if (component.getComponentType() != GameComponentType.GAME && gameVersionChanged && getVersion(componentType) == null && alreadyInstalled) {
|
||||
// For third-party libraries, if game version is being changed, and the library is not being reinstalled,
|
||||
// warns the user that we should update the library.
|
||||
library.versionProperty().set(new InstallerItem.InstalledState(libraryVersion, false, true));
|
||||
component.versionProperty().set(new InstallerItem.InstalledState(libraryVersion, false, true));
|
||||
compatible = false;
|
||||
} else if (alreadyInstalled || getVersion(libraryId) != null) {
|
||||
library.versionProperty().set(new InstallerItem.InstalledState(libraryVersion, false, false));
|
||||
} else if (alreadyInstalled || getVersion(componentType) != null) {
|
||||
component.versionProperty().set(new InstallerItem.InstalledState(libraryVersion, false, false));
|
||||
} else {
|
||||
library.versionProperty().set(null);
|
||||
component.versionProperty().set(null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,11 @@ import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.addon.repository.CurseForgeRemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.download.*;
|
||||
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
|
||||
import org.jackhuang.hmcl.game.GameComponentType;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.HMCLDependencyManager;
|
||||
import org.jackhuang.hmcl.game.HMCLGameBuilder;
|
||||
import org.jackhuang.hmcl.game.HMCLGameInstance;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.setting.GameDirectoryManager;
|
||||
@@ -45,24 +49,20 @@ import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.ui.instances.DownloadListPage;
|
||||
import org.jackhuang.hmcl.ui.instances.HMCLLocalizedDownloadListPage;
|
||||
import org.jackhuang.hmcl.ui.instances.GameInstancePage;
|
||||
import org.jackhuang.hmcl.ui.instances.Instances;
|
||||
import org.jackhuang.hmcl.ui.wizard.Navigation;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
|
||||
import org.jackhuang.hmcl.util.FileNameSet;
|
||||
import org.jackhuang.hmcl.util.SettingsMap;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
@@ -95,7 +95,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
|
||||
public DownloadPage(GameInstanceID uploadInstance) {
|
||||
newGameTab.setNodeSupplier(loadVersionFor(() -> new VersionsPage(versionPageNavigator, i18n("install.installer.choose", i18n("install.installer.game")), "", DownloadProviders.getDownloadProvider(),
|
||||
"game", versionPageNavigator::onGameSelected)));
|
||||
GameComponentType.GAME, versionPageNavigator::onGameSelected)));
|
||||
modpackTab.setNodeSupplier(loadVersionFor(() -> {
|
||||
DownloadListPage page = HMCLLocalizedDownloadListPage.ofModPack((downloadProvider, repository, __, modpack, file) -> {
|
||||
Instances.downloadModpackImpl(downloadProvider, repository, uploadInstance, modpack, file);
|
||||
@@ -135,35 +135,31 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
private static <T extends Node> Supplier<T> loadVersionFor(Supplier<T> nodeSupplier) {
|
||||
return () -> {
|
||||
T node = nodeSupplier.get();
|
||||
if (node instanceof GameInstancePage.GameInstanceLoadable loadable) {
|
||||
loadable.loadInstance(GameDirectoryManager.getSelectedRepository(), null);
|
||||
if (node instanceof DownloadListPage page) {
|
||||
page.loadInstance(HMCLGameInstance.Optional.empty(GameDirectoryManager.getSelectedRepository()));
|
||||
}
|
||||
return node;
|
||||
};
|
||||
}
|
||||
|
||||
public static void download(DownloadProvider downloadProvider, HMCLGameRepository repository, @Nullable GameInstanceID instanceId, RemoteAddon.Version file, String subdirectoryName) {
|
||||
if (instanceId == null) {
|
||||
instanceId = repository.getSelectedInstance();
|
||||
@Nullable HMCLGameInstance instance = instanceId != null
|
||||
? repository.findInstance(instanceId)
|
||||
: repository.getSelectedInstance();
|
||||
Path runDirectory = instance != null ? instance.getRunDirectory() : repository.getBaseDirectory();
|
||||
|
||||
var targetPath = runDirectory.resolve(subdirectoryName);
|
||||
|
||||
FileNameSet existingPaths;
|
||||
try {
|
||||
existingPaths = FileNameSet.list(targetPath, null);
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to list folders in " + targetPath, e);
|
||||
existingPaths = new FileNameSet(false);
|
||||
}
|
||||
|
||||
Path runDirectory = instanceId != null && repository.hasInstance(instanceId) ? repository.getRunDirectory(instanceId) : repository.getBaseDirectory();
|
||||
|
||||
Set<String> existingFiles;
|
||||
|
||||
try (var list = Files.list(runDirectory.resolve(subdirectoryName))) {
|
||||
existingFiles = list.map(Path::getFileName)
|
||||
.map(Path::toString)
|
||||
.collect(Collectors.toSet());
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to list files in " + runDirectory.resolve(subdirectoryName), e);
|
||||
existingFiles = Set.of();
|
||||
}
|
||||
|
||||
Set<String> finalExistingFiles = existingFiles;
|
||||
|
||||
Controllers.prompt(i18n("archive.file.name"), (result, handler) -> {
|
||||
Path dest = runDirectory.resolve(subdirectoryName).resolve(result);
|
||||
Path dest = targetPath.resolve(result);
|
||||
|
||||
Controllers.taskDialog(Task.composeAsync(() -> {
|
||||
var task = new FileDownloadTask(downloadProvider.injectURLWithCandidates(file.file().url()), dest);
|
||||
@@ -171,9 +167,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
return task;
|
||||
}).whenComplete(Schedulers.javafx(), exception -> {
|
||||
if (exception != null) {
|
||||
if (exception instanceof CancellationException) {
|
||||
Controllers.showToast(i18n("message.cancelled"));
|
||||
} else {
|
||||
if (!(exception instanceof CancellationException)) {
|
||||
Controllers.dialog(DownloadProviders.localizeErrorMessage(exception), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR);
|
||||
}
|
||||
} else {
|
||||
@@ -181,7 +175,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
}
|
||||
}), i18n("message.downloading"), TaskCancellationAction.NORMAL);
|
||||
handler.resolve();
|
||||
}, file.file().filename(), new Validator(i18n("install.new_game.malformed"), FileUtils::isNameValid), new Validator(i18n("game_directory.already_exists"), (it) -> !finalExistingFiles.contains(it)));
|
||||
}, file.file().filename(), new Validator(i18n("install.new_game.malformed"), FileUtils::isNameValid), new Validator(i18n("game_directory.already_exists"), existingPaths::notContains));
|
||||
|
||||
}
|
||||
|
||||
@@ -191,19 +185,19 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
if (repository.getGameDirectory() == GameDirectoryManager.getSelectedGameDirectory()) {
|
||||
listenerHolder.add(FXUtils.onWeakChangeAndOperate(GameDirectoryManager.selectedInstanceProperty(), version -> {
|
||||
if (modTab.isInitialized()) {
|
||||
modTab.getNode().loadInstance(repository, null);
|
||||
modTab.getNode().loadInstance(HMCLGameInstance.Optional.empty(repository));
|
||||
}
|
||||
if (modpackTab.isInitialized()) {
|
||||
modpackTab.getNode().loadInstance(repository, null);
|
||||
modpackTab.getNode().loadInstance(HMCLGameInstance.Optional.empty(repository));
|
||||
}
|
||||
if (resourcePackTab.isInitialized()) {
|
||||
resourcePackTab.getNode().loadInstance(repository, null);
|
||||
resourcePackTab.getNode().loadInstance(HMCLGameInstance.Optional.empty(repository));
|
||||
}
|
||||
if (shaderTab.isInitialized()) {
|
||||
shaderTab.getNode().loadInstance(repository, null);
|
||||
shaderTab.getNode().loadInstance(HMCLGameInstance.Optional.empty(repository));
|
||||
}
|
||||
if (worldTab.isInitialized()) {
|
||||
worldTab.getNode().loadInstance(repository, null);
|
||||
worldTab.getNode().loadInstance(HMCLGameInstance.Optional.empty(repository));
|
||||
}
|
||||
}));
|
||||
}
|
||||
@@ -290,7 +284,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
|
||||
private static class VanillaInstallWizardProvider implements WizardProvider {
|
||||
private final HMCLGameRepository repository;
|
||||
private final DefaultDependencyManager dependencyManager;
|
||||
private final HMCLDependencyManager dependencyManager;
|
||||
private final DownloadProvider downloadProvider;
|
||||
private final GameRemoteVersion gameVersion;
|
||||
|
||||
@@ -305,27 +299,43 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
public void start(SettingsMap settings) {
|
||||
settings.put(ModpackPage.GAME_DIRECTORY, repository.getGameDirectory());
|
||||
settings.put(ModpackPage.REPOSITORY, repository);
|
||||
settings.put(LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId(), gameVersion);
|
||||
settings.put(GameComponentType.GAME.getPatchId(), gameVersion);
|
||||
}
|
||||
|
||||
private Task<Void> finishVersionDownloadingAsync(SettingsMap settings) {
|
||||
GameBuilder builder = dependencyManager.newGameBuilder();
|
||||
|
||||
/// Builds the selected instance and selects it after successful completion.
|
||||
///
|
||||
/// @param settings the installer selections and target instance id
|
||||
/// @return the builder task with its stage hints preserved as the outermost task wrapper
|
||||
private Task<?> finishVersionDownloadingAsync(SettingsMap settings) {
|
||||
GameInstanceID instanceId = settings.get(AbstractInstallersPage.INSTANCE_ID);
|
||||
builder.name(instanceId);
|
||||
builder.gameVersion(((RemoteVersion) settings.get(LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId())).getGameVersion());
|
||||
if (instanceId == null) {
|
||||
throw new IllegalStateException("Instance ID is not set");
|
||||
}
|
||||
|
||||
settings.asStringMap().forEach((key, value) -> {
|
||||
if (!LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId().equals(key)
|
||||
&& value instanceof RemoteVersion remoteVersion)
|
||||
builder.version(remoteVersion);
|
||||
});
|
||||
try (HMCLGameBuilder builder = dependencyManager.newGameBuilder(instanceId)) {
|
||||
builder.component(GameComponentType.GAME, ((ComponentRemoteVersion) settings.get(GameComponentType.GAME.getPatchId())).getGameVersion());
|
||||
|
||||
repository.applyDefaultIsolationSettingForNewInstance(instanceId, settings.isInstallingModdedVersion());
|
||||
return builder.buildAsync().whenComplete(any -> {
|
||||
repository.refresh();
|
||||
repository.applyDefaultIsolationSetting(instanceId);
|
||||
}).thenRunAsync(Schedulers.javafx(), () -> repository.setSelectedInstance(instanceId));
|
||||
settings.asStringMap().forEach((key, value) -> {
|
||||
if (!GameComponentType.GAME.getPatchId().equals(key)
|
||||
&& value instanceof ComponentRemoteVersion remoteVersion)
|
||||
builder.component(remoteVersion);
|
||||
});
|
||||
|
||||
boolean modded = GameComponentType.MOD_LOADERS.stream()
|
||||
.anyMatch(componentType ->
|
||||
settings.get(componentType.getPatchId()) instanceof ComponentRemoteVersion);
|
||||
if (repository.shouldIsolateNewInstance(modded)) {
|
||||
builder.enableIsolation();
|
||||
}
|
||||
|
||||
Task<?> buildTask = builder.buildAsync();
|
||||
buildTask.onDone().register(event -> {
|
||||
if (!event.isFailed()) {
|
||||
runInFX(() -> repository.setSelectedInstance(repository.getInstance(instanceId)));
|
||||
}
|
||||
});
|
||||
return buildTask;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -341,7 +351,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
public Node createPage(WizardController controller, int step, SettingsMap settings) {
|
||||
switch (step) {
|
||||
case 0:
|
||||
return new InstallersPage(controller, repository, ((RemoteVersion) controller.getSettings().get("game")).getGameVersion(), downloadProvider);
|
||||
return new InstallersPage(controller, repository, ((ComponentRemoteVersion) controller.getSettings().get("game")).getGameVersion(), downloadProvider);
|
||||
default:
|
||||
throw new IllegalStateException("error step " + step + ", settings: " + settings + ", pages: " + controller.getPages());
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
package org.jackhuang.hmcl.ui.download;
|
||||
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||
import org.jackhuang.hmcl.download.ComponentRemoteVersion;
|
||||
import org.jackhuang.hmcl.game.GameComponentType;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
@@ -52,20 +52,20 @@ public class InstallersPage extends AbstractInstallersPage {
|
||||
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return ((RemoteVersion) controller.getSettings().get("game")).getGameVersion();
|
||||
return ((ComponentRemoteVersion) controller.getSettings().get("game")).getGameVersion();
|
||||
}
|
||||
|
||||
private String getVersion(String id) {
|
||||
return I18n.getDisplayVersion((RemoteVersion) controller.getSettings().get(id));
|
||||
return I18n.getDisplayVersion((ComponentRemoteVersion) controller.getSettings().get(id));
|
||||
}
|
||||
|
||||
protected void reload() {
|
||||
for (InstallerItem library : group.getLibraries()) {
|
||||
String libraryId = library.getLibraryId();
|
||||
for (InstallerItem component : group.getComponents()) {
|
||||
String libraryId = component.getComponentType().getPatchId();
|
||||
if (controller.getSettings().containsKey(libraryId)) {
|
||||
library.versionProperty().set(new InstallerItem.InstalledState(getVersion(libraryId), false, false));
|
||||
component.versionProperty().set(new InstallerItem.InstalledState(getVersion(libraryId), false, false));
|
||||
} else {
|
||||
library.versionProperty().set(null);
|
||||
component.versionProperty().set(null);
|
||||
}
|
||||
}
|
||||
if (!isNameModifiedByUser) {
|
||||
@@ -115,30 +115,25 @@ public class InstallersPage extends AbstractInstallersPage {
|
||||
private void setTxtNameWithLoaders() {
|
||||
StringBuilder nameBuilder = new StringBuilder(getTitle());
|
||||
|
||||
for (InstallerItem library : group.getLibraries()) {
|
||||
String libraryId = library.getLibraryId().replace(LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId(), "");
|
||||
if (!controller.getSettings().containsKey(libraryId)) {
|
||||
for (InstallerItem component : group.getComponents()) {
|
||||
if (component.getComponentType() == GameComponentType.GAME
|
||||
|| !controller.getSettings().containsKey(component.getComponentType().getPatchId()))
|
||||
continue;
|
||||
}
|
||||
|
||||
LibraryAnalyzer.LibraryType libraryType = LibraryAnalyzer.LibraryType.fromPatchId(libraryId);
|
||||
String loaderName = switch (component.getComponentType()) {
|
||||
case FORGE -> "Forge";
|
||||
case NEO_FORGE -> "NeoForge";
|
||||
case CLEANROOM -> "Cleanroom";
|
||||
case LEGACY_FABRIC -> "LegacyFabric";
|
||||
case FABRIC -> "Fabric";
|
||||
case LITELOADER -> "LiteLoader";
|
||||
case QUILT -> "Quilt";
|
||||
case OPTIFINE -> "OptiFine";
|
||||
default -> null;
|
||||
};
|
||||
|
||||
if (libraryType != null) {
|
||||
String loaderName = switch (libraryType) {
|
||||
case FORGE -> "Forge";
|
||||
case NEO_FORGE -> "NeoForge";
|
||||
case CLEANROOM -> "Cleanroom";
|
||||
case LEGACY_FABRIC -> "LegacyFabric";
|
||||
case FABRIC -> "Fabric";
|
||||
case LITELOADER -> "LiteLoader";
|
||||
case QUILT -> "Quilt";
|
||||
case OPTIFINE -> "OptiFine";
|
||||
default -> null;
|
||||
};
|
||||
|
||||
if (loaderName != null)
|
||||
nameBuilder.append('-').append(loaderName);
|
||||
}
|
||||
if (loaderName != null)
|
||||
nameBuilder.append('-').append(loaderName);
|
||||
}
|
||||
|
||||
txtName.setText(nameBuilder.toString());
|
||||
|
||||
@@ -18,10 +18,7 @@
|
||||
package org.jackhuang.hmcl.ui.download;
|
||||
|
||||
import javafx.scene.Node;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.ManuallyCreatedModpackException;
|
||||
import org.jackhuang.hmcl.game.ModpackHelper;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.modpack.MismatchedModpackTypeException;
|
||||
import org.jackhuang.hmcl.modpack.Modpack;
|
||||
import org.jackhuang.hmcl.modpack.ModpackCompletionException;
|
||||
@@ -109,9 +106,9 @@ public final class ModpackInstallWizardProvider implements WizardProvider {
|
||||
}
|
||||
try {
|
||||
if (serverModpackManifest != null) {
|
||||
return ModpackHelper.getUpdateTask(repository, serverModpackManifest, modpack.getEncoding(), instanceId, ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(instanceId)));
|
||||
return ModpackHelper.getUpdateTask(repository, serverModpackManifest, modpack.getEncoding(), instanceId, ModpackHelper.readModpackConfiguration(repository.getLayout().getModpackConfigurationFile(instanceId)));
|
||||
} else {
|
||||
return ModpackHelper.getUpdateTask(repository, selected, modpack.getEncoding(), instanceId, ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(instanceId)));
|
||||
return ModpackHelper.getUpdateTask(repository, selected, modpack.getEncoding(), instanceId, ModpackHelper.readModpackConfiguration(repository.getLayout().getModpackConfigurationFile(instanceId)));
|
||||
}
|
||||
} catch (UnsupportedModpackException | ManuallyCreatedModpackException e) {
|
||||
Controllers.dialog(i18n("modpack.unsupported"), i18n("message.error"), MessageType.ERROR);
|
||||
@@ -124,10 +121,10 @@ public final class ModpackInstallWizardProvider implements WizardProvider {
|
||||
} else {
|
||||
if (serverModpackManifest != null) {
|
||||
return ModpackHelper.getInstallTask(repository, serverModpackManifest, instanceId, modpack)
|
||||
.thenRunAsync(Schedulers.javafx(), () -> repository.setSelectedInstance(instanceId));
|
||||
.thenRunAsync(Schedulers.javafx(), () -> repository.setSelectedInstance(repository.getInstance(instanceId)));
|
||||
} else {
|
||||
return ModpackHelper.getInstallTask(repository, selected, instanceId, modpack, iconUrl)
|
||||
.thenRunAsync(Schedulers.javafx(), () -> repository.setSelectedInstance(instanceId));
|
||||
.thenRunAsync(Schedulers.javafx(), () -> repository.setSelectedInstance(repository.getInstance(instanceId)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+42
-45
@@ -21,8 +21,9 @@ import javafx.scene.Node;
|
||||
import org.jackhuang.hmcl.download.*;
|
||||
import org.jackhuang.hmcl.download.game.GameAssetIndexDownloadTask;
|
||||
import org.jackhuang.hmcl.download.game.LibraryDownloadException;
|
||||
import org.jackhuang.hmcl.game.GameComponentType;
|
||||
import org.jackhuang.hmcl.game.GameInstanceManifest;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.HMCLGameInstance;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.task.DownloadException;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
@@ -46,22 +47,18 @@ import java.util.zip.ZipException;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class UpdateInstallerWizardProvider implements WizardProvider {
|
||||
private final HMCLGameRepository repository;
|
||||
private final HMCLGameInstance gameInstance;
|
||||
private final DefaultDependencyManager dependencyManager;
|
||||
private final String gameVersion;
|
||||
private final GameInstanceManifest manifest;
|
||||
private final String libraryId;
|
||||
private final GameComponentType componentType;
|
||||
private final String oldLibraryVersion;
|
||||
private final DownloadProvider downloadProvider;
|
||||
|
||||
public UpdateInstallerWizardProvider(@NotNull HMCLGameRepository repository, @NotNull String gameVersion, @NotNull GameInstanceManifest manifest, @NotNull String libraryId, @Nullable String oldLibraryVersion) {
|
||||
this.repository = repository;
|
||||
this.gameVersion = gameVersion;
|
||||
this.manifest = manifest;
|
||||
this.libraryId = libraryId;
|
||||
public UpdateInstallerWizardProvider(@NotNull HMCLGameInstance gameInstance, @NotNull GameComponentType componentType, @Nullable String oldLibraryVersion) {
|
||||
this.gameInstance = gameInstance;
|
||||
this.componentType = componentType;
|
||||
this.oldLibraryVersion = oldLibraryVersion;
|
||||
this.downloadProvider = DownloadProviders.getDownloadProvider();
|
||||
this.dependencyManager = repository.getDependency(downloadProvider);
|
||||
this.dependencyManager = gameInstance.getRepository().getDependency(downloadProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -74,38 +71,46 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
|
||||
settings.put("success_message", i18n("install.success"));
|
||||
settings.put(FailureCallback.KEY, (settings1, exception, next) -> alertFailureMessage(exception, next));
|
||||
|
||||
// We remove library but not save it,
|
||||
// so if installation failed will not break down current version.
|
||||
Task<GameInstanceManifest> ret = Task.supplyAsync(() -> manifest);
|
||||
var hints = new ArrayList<Task.StagesHint>();
|
||||
for (Object value : settings.asStringMap().values()) {
|
||||
if (value instanceof RemoteVersion remoteVersion) {
|
||||
ret = ret.thenComposeAsync(version -> dependencyManager.installLibraryAsync(version, remoteVersion));
|
||||
hints.add(new Task.StagesHint(String.format("hmcl.install.%s:%s", remoteVersion.getLibraryId(), remoteVersion.getSelfVersion())));
|
||||
if ("game".equals(remoteVersion.getLibraryId())) {
|
||||
hints.add(new Task.StagesHint("hmcl.install.libraries"));
|
||||
if (value instanceof ComponentRemoteVersion remoteVersion) {
|
||||
hints.add(new Task.StagesHint("hmcl.install.libraries"));
|
||||
|
||||
hints.add(new Task.StagesHint(String.format("hmcl.install.%s:%s", remoteVersion.getComponentType().getPatchId(), remoteVersion.getSelfVersion())));
|
||||
if (remoteVersion.getComponentType() == GameComponentType.GAME) {
|
||||
hints.add(new Task.StagesHint("hmcl.install.assets"));
|
||||
}
|
||||
} else if (value instanceof RemoveVersionAction removeVersionAction) {
|
||||
ret = ret.thenComposeAsync(version -> dependencyManager.removeLibraryAsync(version, removeVersionAction.libraryId));
|
||||
}
|
||||
}
|
||||
|
||||
return ret.thenComposeAsync(repository::saveAsync).thenComposeAsync(repository.refreshAsync()).withStagesHints(hints);
|
||||
return gameInstance.getRepository().updateInstanceAsync(gameInstance.getId(), publishedInstance -> {
|
||||
Task<GameInstanceManifest> update = Task.supplyAsync(publishedInstance::getManifest);
|
||||
for (Object value : settings.asStringMap().values()) {
|
||||
if (value instanceof ComponentRemoteVersion remoteVersion) {
|
||||
update = update.thenComposeAsync(manifest ->
|
||||
dependencyManager.installComponentRemoteAsync(publishedInstance, manifest, remoteVersion));
|
||||
} else if (value instanceof RemoveComponentAction removeComponentAction) {
|
||||
update = update.thenApplyAsync(manifest ->
|
||||
manifest.removeComponent(removeComponentAction.componentType));
|
||||
|
||||
}
|
||||
}
|
||||
return update;
|
||||
}).withStagesHints(hints);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Node createPage(WizardController controller, int step, SettingsMap settings) {
|
||||
switch (step) {
|
||||
case 0:
|
||||
return new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer." + libraryId)), gameVersion, downloadProvider, libraryId, () -> {
|
||||
return new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer." + componentType.getPatchId())), gameInstance.getVersion().toString(), downloadProvider, componentType, () -> {
|
||||
if (oldLibraryVersion == null) {
|
||||
controller.onFinish();
|
||||
} else if ("game".equals(libraryId)) {
|
||||
String newGameVersion = ((RemoteVersion) settings.get(libraryId)).getSelfVersion();
|
||||
controller.onNext(new AdditionalInstallersPage(newGameVersion, manifest, controller, repository, downloadProvider));
|
||||
} else if (componentType == GameComponentType.GAME) {
|
||||
String newGameVersion = ((ComponentRemoteVersion) settings.get(componentType.getPatchId())).getSelfVersion();
|
||||
controller.onNext(new AdditionalInstallersPage(gameInstance, newGameVersion, controller, downloadProvider));
|
||||
} else {
|
||||
Controllers.confirm(i18n("install.change_version.confirm", i18n("install.installer." + libraryId), oldLibraryVersion, ((RemoteVersion) settings.get(libraryId)).getSelfVersion()),
|
||||
Controllers.confirm(i18n("install.change_version.confirm", i18n("install.installer." + componentType.getPatchId()), oldLibraryVersion, ((ComponentRemoteVersion) settings.get(componentType.getPatchId())).getSelfVersion()),
|
||||
i18n("install.change_version"), controller::onFinish, controller::onCancel);
|
||||
}
|
||||
});
|
||||
@@ -129,8 +134,7 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
|
||||
public static void alertFailureMessage(Exception exception, Runnable next) {
|
||||
if (exception instanceof LibraryDownloadException) {
|
||||
String message = i18n("launch.failed.download_library", ((LibraryDownloadException) exception).getLibrary().name()) + "\n";
|
||||
if (exception.getCause() instanceof ResponseCodeException) {
|
||||
ResponseCodeException rce = (ResponseCodeException) exception.getCause();
|
||||
if (exception.getCause() instanceof ResponseCodeException rce) {
|
||||
int responseCode = rce.getResponseCode();
|
||||
String uri = rce.getUri();
|
||||
if (responseCode == 404)
|
||||
@@ -145,8 +149,7 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
|
||||
URI uri = ((DownloadException) exception).getUri();
|
||||
if (exception.getCause() instanceof SocketTimeoutException) {
|
||||
Controllers.dialog(i18n("install.failed.downloading.timeout", uri), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
|
||||
} else if (exception.getCause() instanceof ResponseCodeException) {
|
||||
ResponseCodeException responseCodeException = (ResponseCodeException) exception.getCause();
|
||||
} else if (exception.getCause() instanceof ResponseCodeException responseCodeException) {
|
||||
if (I18n.hasKey("download.code." + responseCodeException.getResponseCode())) {
|
||||
Controllers.dialog(i18n("download.code." + responseCodeException.getResponseCode(), uri), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
|
||||
} else {
|
||||
@@ -155,14 +158,14 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
|
||||
} else {
|
||||
Controllers.dialog(i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
|
||||
}
|
||||
} else if (exception instanceof UnsupportedInstallationException) {
|
||||
switch (((UnsupportedInstallationException) exception).getReason()) {
|
||||
case UnsupportedInstallationException.FORGE_1_17_OPTIFINE_H1_PRE2:
|
||||
} else if (exception instanceof UnsupportedInstallationException unsupportedInstallationException) {
|
||||
switch (unsupportedInstallationException.getReason()) {
|
||||
case UnsupportedInstallationException.CLEANROOM_NOT_COMPATIBLE_WITH_FORGE ->
|
||||
Controllers.dialog(i18n("install.failed.cleanroom_not_compatible_with_forge"), i18n("install.failed"), MessageDialogPane.MessageType.ERROR, next);
|
||||
case UnsupportedInstallationException.FORGE_1_17_OPTIFINE_H1_PRE2 ->
|
||||
Controllers.dialog(i18n("install.failed.optifine_forge_1.17"), i18n("install.failed"), MessageDialogPane.MessageType.ERROR, next);
|
||||
break;
|
||||
default:
|
||||
default ->
|
||||
Controllers.dialog(i18n("install.failed.optifine_conflict"), i18n("install.failed"), MessageDialogPane.MessageType.ERROR, next);
|
||||
break;
|
||||
}
|
||||
} else if (exception instanceof DefaultDependencyManager.UnsupportedLibraryInstallerException) {
|
||||
Controllers.dialog(i18n("install.failed.install_online"), i18n("install.failed"), MessageDialogPane.MessageType.ERROR, next);
|
||||
@@ -170,8 +173,7 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
|
||||
Controllers.dialog(i18n("install.failed.malformed"), i18n("install.failed"), MessageDialogPane.MessageType.ERROR, next);
|
||||
} else if (exception instanceof GameAssetIndexDownloadTask.GameAssetIndexMalformedException) {
|
||||
Controllers.dialog(i18n("assets.index.malformed"), i18n("install.failed"), MessageDialogPane.MessageType.ERROR, next);
|
||||
} else if (exception instanceof VersionMismatchException) {
|
||||
VersionMismatchException e = ((VersionMismatchException) exception);
|
||||
} else if (exception instanceof VersionMismatchException e) {
|
||||
Controllers.dialog(i18n("install.failed.version_mismatch", e.getExpect(), e.getActual()), i18n("install.failed"), MessageDialogPane.MessageType.ERROR, next);
|
||||
} else if (exception instanceof CancellationException) {
|
||||
// Ignore cancel
|
||||
@@ -180,11 +182,6 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
|
||||
}
|
||||
}
|
||||
|
||||
public static class RemoveVersionAction {
|
||||
private final String libraryId;
|
||||
|
||||
public RemoveVersionAction(String libraryId) {
|
||||
this.libraryId = libraryId;
|
||||
}
|
||||
public record RemoveComponentAction(GameComponentType componentType) {
|
||||
}
|
||||
}
|
||||
@@ -31,8 +31,8 @@ import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.*;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||
import org.jackhuang.hmcl.download.VersionList;
|
||||
import org.jackhuang.hmcl.download.ComponentRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.ComponentVersionList;
|
||||
import org.jackhuang.hmcl.download.cleanroom.CleanroomRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.fabric.FabricAPIRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.fabric.FabricRemoteVersion;
|
||||
@@ -45,6 +45,7 @@ import org.jackhuang.hmcl.download.neoforge.NeoForgeRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.optifine.OptiFineRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.quilt.QuiltAPIRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.quilt.QuiltRemoteVersion;
|
||||
import org.jackhuang.hmcl.game.GameComponentType;
|
||||
import org.jackhuang.hmcl.setting.GameInstanceIconType;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
@@ -77,27 +78,27 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class VersionsPage extends Control implements WizardPage, Refreshable {
|
||||
private final String gameVersion;
|
||||
private final String libraryId;
|
||||
private final GameComponentType componentType;
|
||||
private final String title;
|
||||
private final Navigation navigation;
|
||||
private final DownloadProvider downloadProvider;
|
||||
private final VersionList<?> versionList;
|
||||
private final ComponentVersionList<?> versionList;
|
||||
private final Runnable callback;
|
||||
|
||||
private final ObservableList<RemoteVersion> versions = FXCollections.observableArrayList();
|
||||
private final ObservableList<ComponentRemoteVersion> versions = FXCollections.observableArrayList();
|
||||
private final ObjectProperty<Status> status = new SimpleObjectProperty<>(Status.LOADING);
|
||||
|
||||
public VersionsPage(Navigation navigation,
|
||||
String title, String gameVersion,
|
||||
DownloadProvider downloadProvider,
|
||||
String libraryId,
|
||||
GameComponentType componentType,
|
||||
Runnable callback) {
|
||||
this.title = title;
|
||||
this.gameVersion = gameVersion;
|
||||
this.libraryId = libraryId;
|
||||
this.componentType = componentType;
|
||||
this.navigation = navigation;
|
||||
this.downloadProvider = downloadProvider;
|
||||
this.versionList = downloadProvider.getVersionListById(libraryId);
|
||||
this.versionList = downloadProvider.getVersionList(componentType);
|
||||
this.callback = callback;
|
||||
|
||||
refresh();
|
||||
@@ -152,12 +153,13 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
OLD
|
||||
}
|
||||
|
||||
private static class RemoteVersionListCell extends ListCell<RemoteVersion> {
|
||||
private static class RemoteVersionListCell extends ListCell<ComponentRemoteVersion> {
|
||||
private final VersionsPage control;
|
||||
|
||||
private final TwoLineListItem twoLineListItem = new TwoLineListItem();
|
||||
private final ImageView imageView = new ImageView();
|
||||
private final StackPane pane = new StackPane();
|
||||
private final RipplerContainer ripplerContainer;
|
||||
|
||||
RemoteVersionListCell(VersionsPage control) {
|
||||
this.control = control;
|
||||
@@ -172,7 +174,7 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
HBox actions = new HBox(8);
|
||||
actions.setAlignment(Pos.CENTER);
|
||||
{
|
||||
if ("game".equals(control.libraryId)) {
|
||||
if (control.componentType == GameComponentType.GAME) {
|
||||
JFXButton wikiButton = newToggleButton4(SVG.GLOBE_BOOK);
|
||||
wikiButton.setOnAction(event -> onOpenWiki());
|
||||
FXUtils.installFastTooltip(wikiButton, i18n("wiki.tooltip"));
|
||||
@@ -188,22 +190,22 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
|
||||
pane.getStyleClass().add("md-list-cell");
|
||||
StackPane.setMargin(hbox, new Insets(10, 16, 10, 16));
|
||||
pane.getChildren().setAll(new RipplerContainer(hbox));
|
||||
pane.getChildren().setAll(ripplerContainer = new RipplerContainer(hbox));
|
||||
|
||||
FXUtils.onClicked(this, this::onAction);
|
||||
}
|
||||
|
||||
private void onAction() {
|
||||
RemoteVersion item = getItem();
|
||||
ComponentRemoteVersion item = getItem();
|
||||
if (item == null)
|
||||
return;
|
||||
|
||||
control.navigation.getSettings().put(control.libraryId, item);
|
||||
control.navigation.getSettings().put(control.componentType.getPatchId(), item);
|
||||
control.callback.run();
|
||||
}
|
||||
|
||||
private void onOpenWiki() {
|
||||
RemoteVersion item = getItem();
|
||||
ComponentRemoteVersion item = getItem();
|
||||
if (!(item instanceof GameRemoteVersion))
|
||||
return;
|
||||
|
||||
@@ -211,19 +213,22 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateItem(RemoteVersion remoteVersion, boolean empty) {
|
||||
RemoteVersion oldRemoteVersion = getItem();
|
||||
public void updateItem(ComponentRemoteVersion remoteVersion, boolean empty) {
|
||||
ComponentRemoteVersion oldRemoteVersion = getItem();
|
||||
boolean oldEmpty = isEmpty();
|
||||
|
||||
super.updateItem(remoteVersion, empty);
|
||||
|
||||
if (oldRemoteVersion == remoteVersion && oldEmpty == empty) return;
|
||||
|
||||
ripplerContainer.releaseRippleImmediately();
|
||||
|
||||
if (empty) {
|
||||
setGraphic(null);
|
||||
return;
|
||||
}
|
||||
setGraphic(pane);
|
||||
|
||||
if (oldRemoteVersion == remoteVersion) return;
|
||||
|
||||
twoLineListItem.setTitle(I18n.getDisplayVersion(remoteVersion));
|
||||
if (remoteVersion.getReleaseDate() != null) {
|
||||
twoLineListItem.setSubtitle(I18n.formatDateTime(remoteVersion.getReleaseDate()));
|
||||
@@ -234,7 +239,7 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
twoLineListItem.getTags().clear();
|
||||
|
||||
if (remoteVersion instanceof GameRemoteVersion) {
|
||||
RemoteVersion.Type versionType = remoteVersion.getVersionType();
|
||||
ComponentRemoteVersion.Type versionType = remoteVersion.getVersionType();
|
||||
GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(remoteVersion.getGameVersion());
|
||||
|
||||
switch (versionType) {
|
||||
@@ -243,7 +248,7 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
imageView.setImage(GameInstanceIconType.GRASS.getIcon());
|
||||
}
|
||||
case SNAPSHOT, PENDING, UNOBFUSCATED -> {
|
||||
if (versionType == RemoteVersion.Type.SNAPSHOT
|
||||
if (versionType == ComponentRemoteVersion.Type.SNAPSHOT
|
||||
&& GameVersionNumber.asGameVersion(remoteVersion.getGameVersion()).isAprilFools()) {
|
||||
twoLineListItem.addTag(i18n("instance.game.april_fools"));
|
||||
imageView.setImage(GameInstanceIconType.APRIL_FOOLS.getIcon());
|
||||
@@ -295,7 +300,7 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
}
|
||||
|
||||
private static final class VersionsPageSkin extends SkinBase<VersionsPage> {
|
||||
private final JFXListView<RemoteVersion> list;
|
||||
private final JFXListView<ComponentRemoteVersion> list;
|
||||
|
||||
private final TransitionPane transitionPane;
|
||||
private final JFXSpinner spinner;
|
||||
@@ -337,7 +342,7 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
nameField.setPromptText(i18n("instance.search.prompt"));
|
||||
nameField.textProperty().addListener(o -> updateList());
|
||||
|
||||
if ("game".equals(control.libraryId)) {
|
||||
if (control.componentType == GameComponentType.GAME) {
|
||||
categoryField.getItems().setAll(
|
||||
VersionTypeFilter.ALL,
|
||||
VersionTypeFilter.RELEASE,
|
||||
@@ -453,20 +458,20 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
}
|
||||
|
||||
private void updateList() {
|
||||
Stream<RemoteVersion> versions = getSkinnable().versions.stream();
|
||||
Stream<ComponentRemoteVersion> versions = getSkinnable().versions.stream();
|
||||
|
||||
VersionTypeFilter filter = categoryField.getSelectionModel().getSelectedItem();
|
||||
if (filter != null)
|
||||
versions = versions.filter(it -> {
|
||||
RemoteVersion.Type versionType = it.getVersionType();
|
||||
ComponentRemoteVersion.Type versionType = it.getVersionType();
|
||||
return switch (filter) {
|
||||
case RELEASE -> versionType == RemoteVersion.Type.RELEASE;
|
||||
case SNAPSHOTS -> versionType == RemoteVersion.Type.SNAPSHOT
|
||||
|| versionType == RemoteVersion.Type.PENDING
|
||||
|| versionType == RemoteVersion.Type.UNOBFUSCATED;
|
||||
case APRIL_FOOLS -> versionType == RemoteVersion.Type.SNAPSHOT
|
||||
case RELEASE -> versionType == ComponentRemoteVersion.Type.RELEASE;
|
||||
case SNAPSHOTS -> versionType == ComponentRemoteVersion.Type.SNAPSHOT
|
||||
|| versionType == ComponentRemoteVersion.Type.PENDING
|
||||
|| versionType == ComponentRemoteVersion.Type.UNOBFUSCATED;
|
||||
case APRIL_FOOLS -> versionType == ComponentRemoteVersion.Type.SNAPSHOT
|
||||
&& GameVersionNumber.asGameVersion(it.getGameVersion()).isAprilFools();
|
||||
case OLD -> versionType == RemoteVersion.Type.OLD;
|
||||
case OLD -> versionType == ComponentRemoteVersion.Type.OLD;
|
||||
// case ALL,
|
||||
default -> true;
|
||||
};
|
||||
|
||||
@@ -19,8 +19,7 @@ package org.jackhuang.hmcl.ui.export;
|
||||
|
||||
import javafx.scene.Node;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.HMCLGameInstance;
|
||||
import org.jackhuang.hmcl.modpack.ModAdviser;
|
||||
import org.jackhuang.hmcl.modpack.ModpackExportInfo;
|
||||
import org.jackhuang.hmcl.modpack.mcbbs.McbbsModpackExportTask;
|
||||
@@ -37,6 +36,7 @@ import org.jackhuang.hmcl.util.SettingsMap;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.io.Zipper;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@@ -47,12 +47,10 @@ import java.util.List;
|
||||
import static org.jackhuang.hmcl.setting.SettingsManager.settings;
|
||||
|
||||
public final class ExportWizardProvider implements WizardProvider {
|
||||
private final HMCLGameRepository repository;
|
||||
private final GameInstanceID instanceId;
|
||||
private final HMCLGameInstance gameInstance;
|
||||
|
||||
public ExportWizardProvider(HMCLGameRepository repository, GameInstanceID instanceId) {
|
||||
this.repository = repository;
|
||||
this.instanceId = instanceId;
|
||||
public ExportWizardProvider(HMCLGameInstance gameInstance) {
|
||||
this.gameInstance = gameInstance;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -71,11 +69,11 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
}
|
||||
|
||||
private Task<?> exportWithLauncher(String modpackType, ModpackExportInfo exportInfo, Path modpackFile) {
|
||||
Path launcherJar = JarUtils.thisJarPath();
|
||||
@Nullable Path launcherJar = JarUtils.thisJarPath();
|
||||
boolean packWithLauncher = exportInfo.isPackWithLauncher() && launcherJar != null;
|
||||
return new Task<>() {
|
||||
Path tempModpack;
|
||||
Task<?> exportTask;
|
||||
@Nullable Path tempModpack;
|
||||
@Nullable Task<?> exportTask;
|
||||
|
||||
{
|
||||
setSignificance(TaskSignificance.MODERATE);
|
||||
@@ -136,9 +134,12 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
zip.putTextFile(
|
||||
JsonUtils.GSON.toJson(exportedServers, AuthlibInjectorServerList.class),
|
||||
".hmcl/config/authlib-injector-servers.json");
|
||||
zip.putFile(tempModpack, ModpackTypeSelectionPage.MODPACK_TYPE_MODRINTH.equals(modpackType)
|
||||
|
||||
// Bundled package under .hmcl/modpack/ (not the process workdir).
|
||||
String packageName = ModpackTypeSelectionPage.MODPACK_TYPE_MODRINTH.equals(modpackType)
|
||||
? "modpack.mrpack"
|
||||
: "modpack.zip");
|
||||
: "modpack.zip";
|
||||
zip.putFile(tempModpack, ".hmcl/" + Metadata.BUNDLED_MODPACK_DIRECTORY_NAME + "/" + packageName);
|
||||
|
||||
for (String extension : FontManager.FONT_EXTENSIONS) {
|
||||
String fileName = "font." + extension;
|
||||
@@ -150,6 +151,13 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
}
|
||||
|
||||
zip.putFile(launcherJar, launcherJar.getFileName().toString());
|
||||
} finally {
|
||||
if (tempModpack != null) {
|
||||
try {
|
||||
Files.deleteIfExists(tempModpack);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -157,7 +165,7 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
|
||||
private Task<?> exportAsMcbbs(ModpackExportInfo exportInfo, Path modpackFile) {
|
||||
return new Task<Void>() {
|
||||
Task<?> dependency = null;
|
||||
@Nullable Task<?> dependency;
|
||||
|
||||
{
|
||||
setSignificance(TaskSignificance.MODERATE);
|
||||
@@ -165,7 +173,7 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
dependency = new McbbsModpackExportTask(repository, instanceId, exportInfo, modpackFile);
|
||||
dependency = new McbbsModpackExportTask(resolveCurrentGameInstance(), exportInfo, modpackFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -177,7 +185,7 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
|
||||
private Task<?> exportAsMultiMC(ModpackExportInfo exportInfo, Path modpackFile) {
|
||||
return new Task<Void>() {
|
||||
Task<?> dependency;
|
||||
@Nullable Task<?> dependency;
|
||||
|
||||
{
|
||||
setSignificance(TaskSignificance.MODERATE);
|
||||
@@ -185,8 +193,9 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
GameSettings.Effective setting = repository.getEffectiveGameSettings(instanceId);
|
||||
dependency = new MultiMCModpackExportTask(repository, instanceId, exportInfo.getWhitelist(),
|
||||
HMCLGameInstance instance = resolveCurrentGameInstance();
|
||||
GameSettings.Effective setting = instance.getEffectiveSettings();
|
||||
dependency = new MultiMCModpackExportTask(instance, exportInfo.getWhitelist(),
|
||||
new MultiMCInstanceConfiguration(
|
||||
"OneSix",
|
||||
exportInfo.getName() + "-" + exportInfo.getVersion(),
|
||||
@@ -225,7 +234,7 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
|
||||
private Task<?> exportAsServer(ModpackExportInfo exportInfo, Path modpackFile) {
|
||||
return new Task<Void>() {
|
||||
Task<?> dependency;
|
||||
@Nullable Task<?> dependency;
|
||||
|
||||
{
|
||||
setSignificance(TaskSignificance.MODERATE);
|
||||
@@ -233,7 +242,7 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
dependency = new ServerModpackExportTask(repository, instanceId, exportInfo, modpackFile);
|
||||
dependency = new ServerModpackExportTask(resolveCurrentGameInstance(), exportInfo, modpackFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -245,7 +254,7 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
|
||||
private Task<?> exportAsModrinth(ModpackExportInfo exportInfo, Path modpackFile) {
|
||||
return new Task<Void>() {
|
||||
Task<?> dependency;
|
||||
@Nullable Task<?> dependency;
|
||||
|
||||
{
|
||||
setSignificance(TaskSignificance.MODERATE);
|
||||
@@ -254,8 +263,7 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
@Override
|
||||
public void execute() {
|
||||
dependency = new ModrinthModpackExportTask(
|
||||
repository,
|
||||
instanceId,
|
||||
resolveCurrentGameInstance(),
|
||||
exportInfo,
|
||||
modpackFile
|
||||
);
|
||||
@@ -268,12 +276,19 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the current registered snapshot for the instance selected by this wizard.
|
||||
///
|
||||
/// @return the current registered instance
|
||||
private HMCLGameInstance resolveCurrentGameInstance() {
|
||||
return gameInstance.getRepository().getInstance(gameInstance.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Node createPage(WizardController controller, int step, SettingsMap settings) {
|
||||
return switch (step) {
|
||||
case 0 -> new ModpackTypeSelectionPage(controller);
|
||||
case 1 -> new ModpackInfoPage(controller, repository, instanceId);
|
||||
case 2 -> new ModpackFileSelectionPage(controller, repository, instanceId, ModAdviser::suggestMod);
|
||||
case 1 -> new ModpackInfoPage(controller, gameInstance);
|
||||
case 2 -> new ModpackFileSelectionPage(controller, gameInstance, ModAdviser::suggestMod);
|
||||
default -> throw new IllegalArgumentException("step");
|
||||
};
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.HMCLGameInstance;
|
||||
import org.jackhuang.hmcl.modpack.ModAdviser;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
@@ -62,14 +62,15 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
*/
|
||||
public final class ModpackFileSelectionPage extends BorderPane implements WizardPage {
|
||||
private final WizardController controller;
|
||||
private final GameInstanceID instanceId;
|
||||
private final HMCLGameInstance gameInstance;
|
||||
private final ModAdviser adviser;
|
||||
private @Nullable ModpackFileTreeItem rootNode;
|
||||
|
||||
public ModpackFileSelectionPage(WizardController controller, HMCLGameRepository repository, GameInstanceID instanceId, ModAdviser adviser) {
|
||||
public ModpackFileSelectionPage(WizardController controller, HMCLGameInstance gameInstance, ModAdviser adviser) {
|
||||
this.controller = controller;
|
||||
this.instanceId = instanceId;
|
||||
this.gameInstance = gameInstance;
|
||||
this.adviser = adviser;
|
||||
GameInstanceID instanceId = gameInstance.getId();
|
||||
|
||||
JFXTreeView<String> treeView = new JFXTreeView<>();
|
||||
treeView.setSelectionModel(new NoneMultipleSelectionModel<>());
|
||||
@@ -97,17 +98,17 @@ public final class ModpackFileSelectionPage extends BorderPane implements Wizard
|
||||
btnNext.setOnAction(e -> onNext());
|
||||
nextPane.getChildren().setAll(btnNext);
|
||||
|
||||
loadRoot(repository, treeView, placeholderPane, spinnerPane, btnNext);
|
||||
spinnerPane.setOnFailedAction((__) -> loadRoot(repository, treeView, placeholderPane, spinnerPane, btnNext));
|
||||
loadRoot(treeView, placeholderPane, spinnerPane, btnNext);
|
||||
spinnerPane.setOnFailedAction((__) -> loadRoot(treeView, placeholderPane, spinnerPane, btnNext));
|
||||
|
||||
this.setBottom(nextPane);
|
||||
}
|
||||
|
||||
private void loadRoot(HMCLGameRepository repository, JFXTreeView<String> treeView, StackPane placeholderPane, SpinnerPane spinnerPane, JFXButton btnNext) {
|
||||
private void loadRoot(JFXTreeView<String> treeView, StackPane placeholderPane, SpinnerPane spinnerPane, JFXButton btnNext) {
|
||||
spinnerPane.setLoading(true);
|
||||
btnNext.setDisable(true);
|
||||
CompletableFuture
|
||||
.supplyAsync(() -> getTreeItem(repository.getRunDirectory(instanceId), "minecraft", 0), Schedulers.io())
|
||||
.supplyAsync(() -> getTreeItem(gameInstance.getRunDirectory(), "minecraft", 0), Schedulers.io())
|
||||
.whenCompleteAsync((root, throwable) -> {
|
||||
if (throwable == null) {
|
||||
if (root != null) {
|
||||
@@ -145,12 +146,12 @@ public final class ModpackFileSelectionPage extends BorderPane implements Wizard
|
||||
}
|
||||
if (fileName.startsWith("._")) // macOS system file
|
||||
state = ModAdviser.ModSuggestion.HIDDEN;
|
||||
if (FileUtils.getNameWithoutExtension(file).equals(instanceId.toString()))
|
||||
if (FileUtils.getNameWithoutExtension(file).equals(gameInstance.getId().toString()))
|
||||
state = ModAdviser.ModSuggestion.HIDDEN;
|
||||
}
|
||||
|
||||
if (isDirectory) {
|
||||
if (fileName.equals(instanceId + "-natives")) { // Ignore <version>-natives
|
||||
if (fileName.equals(gameInstance.getId() + "-natives")) { // Ignore <version>-natives
|
||||
state = ModAdviser.ModSuggestion.HIDDEN;
|
||||
}
|
||||
if (level == 1 && fileName.startsWith("natives-")) { // Ignore natives-os-arch
|
||||
@@ -161,7 +162,7 @@ public final class ModpackFileSelectionPage extends BorderPane implements Wizard
|
||||
return null;
|
||||
}
|
||||
|
||||
ModpackFileTreeItem node = new ModpackFileTreeItem(level == 0 ? instanceId.toString() : StringUtils.substringAfterLast(basePath, '/'), basePath);
|
||||
ModpackFileTreeItem node = new ModpackFileTreeItem(level == 0 ? gameInstance.getId().toString() : StringUtils.substringAfterLast(basePath, '/'), basePath);
|
||||
if (state == ModAdviser.ModSuggestion.SUGGESTED)
|
||||
node.setSelected(true);
|
||||
|
||||
|
||||
@@ -35,8 +35,7 @@ import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.HMCLGameInstance;
|
||||
import org.jackhuang.hmcl.modpack.ModpackExportInfo;
|
||||
import org.jackhuang.hmcl.modpack.mcbbs.McbbsModpackManifest;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
@@ -65,9 +64,8 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
private final WizardController controller;
|
||||
private final HMCLGameRepository repository;
|
||||
private final HMCLGameInstance gameInstance;
|
||||
private final ModpackExportInfo.Options options;
|
||||
private final GameInstanceID instanceId;
|
||||
private final boolean canIncludeLauncher;
|
||||
|
||||
private final ModpackExportInfo exportInfo = new ModpackExportInfo();
|
||||
@@ -88,19 +86,18 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
private final SimpleBooleanProperty noCreateRemoteFiles = new SimpleBooleanProperty();
|
||||
private final SimpleBooleanProperty skipCurseForgeRemoteFiles = new SimpleBooleanProperty();
|
||||
|
||||
public ModpackInfoPage(WizardController controller, HMCLGameRepository repository, GameInstanceID instanceId) {
|
||||
public ModpackInfoPage(WizardController controller, HMCLGameInstance gameInstance) {
|
||||
this.controller = controller;
|
||||
this.repository = repository;
|
||||
this.gameInstance = gameInstance;
|
||||
this.options = controller.getSettings().get(MODPACK_INFO_OPTION);
|
||||
this.instanceId = instanceId;
|
||||
|
||||
if (this.options == null)
|
||||
throw new IllegalArgumentException("Settings.MODPACK_INFO_OPTION is required");
|
||||
|
||||
name.set(instanceId.toString());
|
||||
name.set(gameInstance.getId().toString());
|
||||
author.set(Optional.ofNullable(Accounts.getSelectedAccount()).map(Account::getProfileName).orElse(""));
|
||||
|
||||
GameSettings.Effective versionSetting = repository.getEffectiveGameSettings(this.instanceId);
|
||||
GameSettings.Effective versionSetting = gameInstance.getEffectiveSettings();
|
||||
minMemory.set(Optional.ofNullable(versionSetting.getInheritable(GameSettings::minMemoryProperty)).orElse(0));
|
||||
launchArguments.set(versionSetting.getInheritable(GameSettings::gameArgumentsProperty));
|
||||
javaArguments.set(versionSetting.getInheritable(GameSettings::jvmOptionsProperty));
|
||||
@@ -213,7 +210,7 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
var instanceNamePane = new LineTextPane();
|
||||
{
|
||||
instanceNamePane.setTitle(i18n("modpack.wizard.step.initialization.exported_version"));
|
||||
instanceNamePane.setText(skinnable.instanceId.toString());
|
||||
instanceNamePane.setText(skinnable.gameInstance.getId().toString());
|
||||
|
||||
list.getContent().add(instanceNamePane);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import javafx.beans.property.Property;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.css.PseudoClass;
|
||||
@@ -76,7 +77,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
/// @author Glavo
|
||||
@NotNullByDefault
|
||||
public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
implements DecoratorPage, GameInstancePage.GameInstanceLoadable, PageAware {
|
||||
implements DecoratorPage, PageAware {
|
||||
|
||||
private static final Object INHERIT_BUTTON_TOOLTIP_KEY = new Object();
|
||||
private static final PseudoClass PSEUDO_OVERRIDDEN = PseudoClass.getPseudoClass("overridden");
|
||||
@@ -88,20 +89,13 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
private final ObjectProperty<State> state = new SimpleObjectProperty<>(this, "state", new State("", null, false, false, false));
|
||||
private final WeakListenerHolder holder = new WeakListenerHolder();
|
||||
|
||||
/// The selected game directory.
|
||||
private @Nullable GameDirectory gameDirectory;
|
||||
|
||||
/// The selected repository.
|
||||
private @Nullable HMCLGameRepository repository;
|
||||
|
||||
/// The current instance ID.
|
||||
private @Nullable GameInstanceID instanceId;
|
||||
/// The current game instance when editing instance settings, or `null` for preset settings.
|
||||
private final ObjectProperty<@Nullable HMCLGameInstance> gameInstance =
|
||||
new SimpleObjectProperty<>(this, "gameInstance");
|
||||
|
||||
/// The current setting.
|
||||
private final ObjectProperty<@Nullable S> currentSetting = new SimpleObjectProperty<>(this, "setting");
|
||||
|
||||
private final ObjectProperty<GameVersionNumber> currentGameVersionNumber = new SimpleObjectProperty<>(this, "currentGameVersionNumber", GameVersionNumber.unknown());
|
||||
|
||||
private boolean updatingJavaSetting = false;
|
||||
private boolean updatingSelectedJava = false;
|
||||
private boolean updatingParentSetting = false;
|
||||
@@ -129,12 +123,24 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
private final InvalidationListener javaListener = o -> refreshJavaSettings();
|
||||
private final InvalidationListener weakJavaListener = holder.weak(javaListener);
|
||||
|
||||
public GameSettingsPage(Class<S> settingType) {
|
||||
/// Creates a settings page.
|
||||
///
|
||||
/// @param settingType [GameSettings.Instance] or [GameSettings.Preset]
|
||||
/// @param instanceContext parent instance property for instance settings; ignored for presets and may be `null`
|
||||
public GameSettingsPage(
|
||||
Class<S> settingType,
|
||||
@Nullable ObservableValue<? extends HMCLGameInstance.Optional> instanceContext) {
|
||||
assert settingType == GameSettings.Preset.class || settingType == GameSettings.Instance.class;
|
||||
|
||||
this.isPresetSetting = settingType == GameSettings.Preset.class;
|
||||
if (!isPresetSetting) {
|
||||
bindActiveParentSetting();
|
||||
Objects.requireNonNull(instanceContext, "instanceContext");
|
||||
holder.add(FXUtils.onWeakChangeAndOperate(instanceContext, current -> {
|
||||
if (current != null) {
|
||||
loadInstance(current);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
this.scrollPane = new ScrollPane();
|
||||
@@ -790,21 +796,23 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
var highPerformancePane = createInheritableBooleanButton(GameSettings::highPerformanceProperty);
|
||||
graphicsSettings.getContent().add(highPerformancePane);
|
||||
highPerformancePane.setTitle(i18n("settings.advanced.renderer.gpu_preferences"));
|
||||
highPerformancePane.setSubtitle(i18n("settings.advanced.windows_only"));
|
||||
|
||||
this.currentGameVersionNumber.addListener((o, oldValue, newValue) -> {
|
||||
boolean showBackendChoose = isPresetSetting || newValue.compareTo("26.2-snapshot-2") >= 0;
|
||||
InvalidationListener updateGraphicsVisibility = o -> {
|
||||
GameVersionNumber version = currentGameVersion();
|
||||
boolean showBackendChoose = isPresetSetting || version.compareTo("26.2-snapshot-2") >= 0;
|
||||
graphicsBackendPane.setVisible(showBackendChoose);
|
||||
graphicsBackendPane.setManaged(showBackendChoose);
|
||||
|
||||
boolean showOpenGL = GraphicsAPI.OPENGL.isSupported(newValue);
|
||||
boolean showOpenGL = GraphicsAPI.OPENGL.isSupported(version);
|
||||
openGLRendererPane.setVisible(showOpenGL);
|
||||
openGLRendererPane.setManaged(showOpenGL);
|
||||
|
||||
boolean showVulkan = GraphicsAPI.VULKAN.isSupported(newValue);
|
||||
boolean showVulkan = isPresetSetting || GraphicsAPI.VULKAN.isSupported(version);
|
||||
vulkanRendererPane.setVisible(showVulkan);
|
||||
vulkanRendererPane.setManaged(showVulkan);
|
||||
});
|
||||
};
|
||||
this.gameInstance.addListener(updateGraphicsVisibility);
|
||||
updateGraphicsVisibility.invalidated(this.gameInstance);
|
||||
}
|
||||
|
||||
var nativeLibrarySettings = new ComponentList();
|
||||
@@ -831,10 +839,10 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
nativeLibrarySettings.getContent().add(noNativesPatchPane);
|
||||
noNativesPatchPane.setTitle(i18n("settings.advanced.dont_patch_natives"));
|
||||
|
||||
var useNativeGLFWPane = createIndependentBooleanButton(GameSettings::useNativeGLFWProperty);
|
||||
nativeLibrarySettings.getContent().add(useNativeGLFWPane);
|
||||
useNativeGLFWPane.setTitle(i18n("settings.advanced.use_native_glfw"));
|
||||
useNativeGLFWPane.setSubtitle(i18n("settings.advanced.linux_freebsd_only"));
|
||||
var useNativeGLFWORsdlPane = createIndependentBooleanButton(GameSettings::useNativeGLFWorSDLProperty);
|
||||
nativeLibrarySettings.getContent().add(useNativeGLFWORsdlPane);
|
||||
useNativeGLFWORsdlPane.setTitle(i18n("settings.advanced.use_native_glfw_or_sdl"));
|
||||
useNativeGLFWORsdlPane.setSubtitle(i18n("settings.advanced.linux_freebsd_only"));
|
||||
|
||||
var useNativeOpenALPane = createIndependentBooleanButton(GameSettings::useNativeOpenALProperty);
|
||||
nativeLibrarySettings.getContent().add(useNativeOpenALPane);
|
||||
@@ -1860,16 +1868,20 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
}
|
||||
|
||||
private boolean isCurrentInstanceModpack() {
|
||||
return repository != null && instanceId != null && repository.isModpack(instanceId);
|
||||
HMCLGameInstance gameInstance = this.gameInstance.get();
|
||||
return gameInstance != null && gameInstance.isModpack();
|
||||
}
|
||||
|
||||
/// Returns the current instance version root displayed for modpack running directories.
|
||||
private String getCurrentInstanceVersionRoot() {
|
||||
if (repository == null || instanceId == null) {
|
||||
return "";
|
||||
}
|
||||
HMCLGameInstance gameInstance = this.gameInstance.get();
|
||||
return gameInstance != null ? gameInstance.getInstanceRoot().toString() : "";
|
||||
}
|
||||
|
||||
return repository.getInstanceRoot(instanceId).toString();
|
||||
/// Returns the Minecraft version of the loaded instance, or [GameVersionNumber#unknown()] for presets.
|
||||
private GameVersionNumber currentGameVersion() {
|
||||
HMCLGameInstance gameInstance = this.gameInstance.get();
|
||||
return gameInstance != null ? gameInstance.getVersion() : GameVersionNumber.unknown();
|
||||
}
|
||||
|
||||
/// Keeps a listener attached to the current instance's parent preset property.
|
||||
@@ -2589,8 +2601,9 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
|
||||
/// Returns the runtime parent preset for an instance, including the game directory's migrated preset fallback.
|
||||
private GameSettings.Preset getEffectiveParentGameSettings(GameSettings.Instance instance) {
|
||||
if (repository != null) {
|
||||
return repository.getParentGameSettings(instance);
|
||||
HMCLGameInstance gameInstance = this.gameInstance.get();
|
||||
if (gameInstance != null) {
|
||||
return gameInstance.getRepository().getParentGameSettings(instance);
|
||||
}
|
||||
|
||||
return getExplicitParentGameSettings(instance);
|
||||
@@ -2629,26 +2642,21 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public void loadInstance(HMCLGameRepository repository, @Nullable GameInstanceID instanceId) {
|
||||
this.gameDirectory = repository.getGameDirectory();
|
||||
this.repository = repository;
|
||||
this.instanceId = instanceId;
|
||||
public void loadInstance(HMCLGameInstance.Optional instance) {
|
||||
HMCLGameInstance gameInstance = instance.instance();
|
||||
this.gameInstance.set(gameInstance);
|
||||
|
||||
assert isPresetSetting == (instanceId == null);
|
||||
assert isPresetSetting == (gameInstance == null);
|
||||
|
||||
if (instanceId != null) {
|
||||
this.currentGameVersionNumber.set(GameVersionNumber.asGameVersion(repository.getGameVersion(instanceId)));
|
||||
|
||||
@Nullable GameSettings.Instance setting = repository.getInstanceGameSettingsOrCreate(instanceId);
|
||||
if (gameInstance != null) {
|
||||
@Nullable GameSettings.Instance setting = gameInstance.getSettingsOrCreate();
|
||||
this.currentSetting.set((S) setting);
|
||||
setSettingsReadOnly(
|
||||
setting == null || repository.isInstanceGameSettingsReadOnly(instanceId),
|
||||
setting == null || gameInstance.isSettingsReadOnly(),
|
||||
i18n("settings.game.instance_settings.unsupported"),
|
||||
setting != null ? this::forceOverwriteInstanceGameSettings : null);
|
||||
loadIcon();
|
||||
} else {
|
||||
this.currentGameVersionNumber.set(GameVersionNumber.unknown());
|
||||
this.currentSetting.set((S) SettingsManager.getDefaultGameSettingsPresetOrCreate());
|
||||
setSettingsReadOnly(
|
||||
SettingsManager.isGameSettingsReadOnly(),
|
||||
@@ -2657,11 +2665,6 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the loaded instance ID, or `null` when this page edits preset settings.
|
||||
private @Nullable GameInstanceID getLoadedInstanceId() {
|
||||
return instanceId == null ? null : instanceId;
|
||||
}
|
||||
|
||||
/// Updates the page read-only state used when settings cannot be saved safely.
|
||||
///
|
||||
/// @param readOnly whether the current settings should be displayed read-only
|
||||
@@ -2701,13 +2704,13 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
|
||||
/// Backs up and overwrites the current instance's `instance-game-settings.json`.
|
||||
private void forceOverwriteInstanceGameSettings() {
|
||||
@Nullable GameInstanceID loadedInstanceId = getLoadedInstanceId();
|
||||
if (repository == null || loadedInstanceId == null) {
|
||||
HMCLGameInstance gameInstance = this.gameInstance.get();
|
||||
if (gameInstance == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Controllers.confirmBackupAndOverwrite(i18n("settings.game.instance_settings.unsupported"), () -> {
|
||||
repository.forceOverwriteInstanceGameSettings(loadedInstanceId);
|
||||
gameInstance.forceOverwriteSettings();
|
||||
setSettingsReadOnly(false, "");
|
||||
});
|
||||
}
|
||||
@@ -2721,11 +2724,12 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
}
|
||||
|
||||
private void loadIcon() {
|
||||
@Nullable GameInstanceID loadedInstanceId = getLoadedInstanceId();
|
||||
if (repository == null || loadedInstanceId == null)
|
||||
HMCLGameInstance gameInstance = this.gameInstance.get();
|
||||
if (gameInstance == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
iconPickerItem.setImage(repository.getInstanceIconImage(loadedInstanceId));
|
||||
iconPickerItem.setImage(gameInstance.getIconImage());
|
||||
}
|
||||
|
||||
/// Refreshes Java selection controls and keeps inherited parent Java properties observed.
|
||||
@@ -2789,17 +2793,19 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
private void initJavaSubtitle() {
|
||||
S setting = currentSetting.get();
|
||||
|
||||
if (setting == null || gameDirectory == null)
|
||||
if (setting == null) {
|
||||
return;
|
||||
}
|
||||
initializeSelectedJava();
|
||||
|
||||
JavaVersionType javaVersionType = setting.javaTypeProperty().getValue();
|
||||
@Nullable GameInstanceID loadedInstanceId = getLoadedInstanceId();
|
||||
GameSettings.Effective effectiveSetting = loadedInstanceId != null && repository != null ? repository.getEffectiveGameSettings(loadedInstanceId) : null;
|
||||
HMCLGameInstance gameInstance = this.gameInstance.get();
|
||||
@Nullable GameSettings.Effective effectiveSetting =
|
||||
gameInstance != null ? gameInstance.getEffectiveSettings() : null;
|
||||
JavaVersionType effectiveJavaVersionType = effectiveSetting != null ? effectiveSetting.getInheritable(GameSettings::javaTypeProperty) : javaVersionType;
|
||||
boolean autoSelected = effectiveJavaVersionType == JavaVersionType.AUTO || effectiveJavaVersionType == JavaVersionType.VERSION;
|
||||
|
||||
if (instanceId == null && autoSelected) {
|
||||
if (gameInstance == null && autoSelected) {
|
||||
javaSublist.setDescription(i18n("settings.game.java_directory.auto"));
|
||||
return;
|
||||
}
|
||||
@@ -2811,13 +2817,8 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
}
|
||||
|
||||
if (JavaManager.isInitialized()) {
|
||||
GameVersionNumber gameVersionNumber = this.currentGameVersionNumber.get();
|
||||
GameInstanceManifest manifest;
|
||||
if (this.instanceId == null) {
|
||||
manifest = null;
|
||||
} else {
|
||||
manifest = repository != null && loadedInstanceId != null ? repository.getResolvedInstanceManifest(loadedInstanceId).launchManifest() : null;
|
||||
}
|
||||
GameVersionNumber gameVersionNumber = currentGameVersion();
|
||||
GameInstanceManifest manifest = gameInstance != null ? gameInstance.getResolvedManifest() : null;
|
||||
|
||||
try {
|
||||
JavaRuntime java = effectiveSetting != null
|
||||
@@ -2837,19 +2838,21 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
}
|
||||
|
||||
private void onExploreIcon() {
|
||||
if (repository == null || instanceId == null)
|
||||
HMCLGameInstance gameInstance = this.gameInstance.get();
|
||||
if (gameInstance == null) {
|
||||
return;
|
||||
|
||||
Controllers.dialog(new GameInstanceIconDialog(repository, instanceId, this::loadIcon));
|
||||
}
|
||||
Controllers.dialog(new GameInstanceIconDialog(gameInstance, this::loadIcon));
|
||||
}
|
||||
|
||||
private void onDeleteIcon() {
|
||||
@Nullable GameInstanceID loadedInstanceId = getLoadedInstanceId();
|
||||
if (repository == null || loadedInstanceId == null)
|
||||
HMCLGameInstance gameInstance = this.gameInstance.get();
|
||||
if (gameInstance == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
repository.deleteIconFile(loadedInstanceId);
|
||||
GameSettings.Instance localGameSettings = repository.getInstanceGameSettingsOrCreate(loadedInstanceId);
|
||||
gameInstance.deleteIconFile();
|
||||
GameSettings.Instance localGameSettings = gameInstance.getSettingsOrCreate();
|
||||
if (localGameSettings != null) {
|
||||
localGameSettings.iconProperty().setValue(GameInstanceIconType.DEFAULT);
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
@@ -84,38 +85,37 @@ public final class ImageUtils {
|
||||
LoaderParameters parameters = new LoaderParameters();
|
||||
parameters.autoStartAnimations = false;
|
||||
|
||||
@Nullable SVGImage image;
|
||||
Supplier<Image> supplier = () -> {
|
||||
SVGImage image = SVGLoader.load(content, parameters);
|
||||
var snapshotParameters = new SnapshotParameters();
|
||||
snapshotParameters.setFill(Color.TRANSPARENT);
|
||||
|
||||
if (requestedWidth <= 0. || requestedHeight <= 0.) {
|
||||
return image.toImage(snapshotParameters);
|
||||
}
|
||||
|
||||
double scaleX = requestedWidth / image.getScaledWidth();
|
||||
double scaleY = requestedHeight / image.getScaledHeight();
|
||||
|
||||
if (preserveRatio || scaleX == scaleY) {
|
||||
double scale = Math.min(scaleX, scaleY);
|
||||
return image.scale(scale).toImage(snapshotParameters);
|
||||
} else {
|
||||
// FIXME: Use DEFAULT_SVG_SNAPSHOT_PARAMS
|
||||
return image.toImageScaled(ScaleQuality.RENDER_QUALITY, scaleX, scaleY);
|
||||
}
|
||||
};
|
||||
|
||||
@Nullable Image image;
|
||||
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
image = SVGLoader.load(content, parameters);
|
||||
image = supplier.get();
|
||||
} else {
|
||||
// TODO: Currently, SVGLoader.load(...) requires the javafx.swing module if it operates on a non-JavaFX thread.
|
||||
image = CompletableFuture.supplyAsync(
|
||||
() -> SVGLoader.load(content, parameters),
|
||||
Schedulers.javafx()
|
||||
).get();
|
||||
image = CompletableFuture.supplyAsync(supplier, Schedulers.javafx()).get();
|
||||
}
|
||||
|
||||
if (image == null)
|
||||
throw new IOException("Failed to load SVG image");
|
||||
|
||||
var snapshotParameters = new SnapshotParameters();
|
||||
snapshotParameters.setFill(Color.TRANSPARENT);
|
||||
|
||||
if (requestedWidth <= 0. || requestedHeight <= 0.) {
|
||||
return image.toImage(snapshotParameters);
|
||||
}
|
||||
|
||||
double scaleX = requestedWidth / image.getScaledWidth();
|
||||
double scaleY = requestedHeight / image.getScaledHeight();
|
||||
|
||||
if (preserveRatio || scaleX == scaleY) {
|
||||
double scale = Math.min(scaleX, scaleY);
|
||||
return image.scale(scale).toImage(snapshotParameters);
|
||||
} else {
|
||||
// FIXME: Use DEFAULT_SVG_SNAPSHOT_PARAMS
|
||||
return image.toImageScaled(ScaleQuality.RENDER_QUALITY, scaleX, scaleY);
|
||||
}
|
||||
return image;
|
||||
};
|
||||
|
||||
public static final ImageLoader APNG = (input, requestedWidth, requestedHeight, preserveRatio, smooth) -> {
|
||||
|
||||
@@ -38,9 +38,7 @@ import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.*;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.GameInstanceManifest;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.addon.repository.ModrinthRemoteAddonRepository;
|
||||
@@ -68,12 +66,12 @@ import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.selectedItemPropertyFor;
|
||||
|
||||
public class DownloadListPage extends Control implements DecoratorPage, GameInstancePage.GameInstanceLoadable {
|
||||
public class DownloadListPage extends Control implements DecoratorPage {
|
||||
protected final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>();
|
||||
private final BooleanProperty loading = new SimpleBooleanProperty(false);
|
||||
private final BooleanProperty failed = new SimpleBooleanProperty(false);
|
||||
private final boolean instanceSelection;
|
||||
private final ObjectProperty<HMCLGameRepository.InstanceReference> instanceReference = new SimpleObjectProperty<>();
|
||||
private final ObjectProperty<HMCLGameInstance.Optional> instanceReference = new SimpleObjectProperty<>();
|
||||
private final IntegerProperty pageOffset = new SimpleIntegerProperty(0);
|
||||
private final IntegerProperty pageCount = new SimpleIntegerProperty(-1);
|
||||
private final ListProperty<RemoteAddon> items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList());
|
||||
@@ -111,9 +109,8 @@ public class DownloadListPage extends Control implements DecoratorPage, GameInst
|
||||
return actions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadInstance(HMCLGameRepository repository, @Nullable GameInstanceID instanceId) {
|
||||
this.instanceReference.set(new HMCLGameRepository.InstanceReference(repository, instanceId));
|
||||
public void loadInstance(HMCLGameInstance.Optional instance) {
|
||||
this.instanceReference.set(instance);
|
||||
|
||||
setLoading(false);
|
||||
setFailed(false);
|
||||
@@ -124,10 +121,12 @@ public class DownloadListPage extends Control implements DecoratorPage, GameInst
|
||||
}
|
||||
|
||||
if (instanceSelection) {
|
||||
instances.setAll(repository.getDisplayInstanceManifests()
|
||||
.map(GameInstanceManifest::id)
|
||||
HMCLGameRepository repository = instance.repository();
|
||||
instances.setAll(repository.getDisplayInstances()
|
||||
.map(DefaultGameInstance::getId)
|
||||
.toList());
|
||||
selectedInstance.set(repository.getSelectedInstance());
|
||||
@Nullable HMCLGameInstance repositorySelection = repository.getSelectedInstance();
|
||||
selectedInstance.set(repositorySelection != null ? repositorySelection.getId() : null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,11 +165,13 @@ public class DownloadListPage extends Control implements DecoratorPage, GameInst
|
||||
|
||||
int currentSearchID = searchID = searchID + 1;
|
||||
Task.supplyAsync(() -> {
|
||||
HMCLGameRepository.InstanceReference instanceReference = this.instanceReference.get();
|
||||
if (instanceReference.instanceId() == null) {
|
||||
HMCLGameInstance.Optional instanceReference = this.instanceReference.get();
|
||||
@Nullable HMCLGameInstance instance = instanceReference.instance();
|
||||
if (instance == null) {
|
||||
return userGameVersion;
|
||||
} else {
|
||||
return instanceReference.repository().getGameVersion(instanceReference.instanceId()).orElse("");
|
||||
GameVersionNumber version = instance.getVersion();
|
||||
return version != GameVersionNumber.unknown() ? version.toString() : "";
|
||||
}
|
||||
}).thenApplyAsync(
|
||||
gameVersion -> repository.search(downloadProvider, gameVersion, category, pageOffset, 50, searchFilter, sort, RemoteAddonRepository.SortOrder.DESC)
|
||||
@@ -217,10 +218,10 @@ public class DownloadListPage extends Control implements DecoratorPage, GameInst
|
||||
}
|
||||
}
|
||||
|
||||
protected HMCLGameRepository.InstanceReference getInstanceReference() {
|
||||
protected HMCLGameInstance.Optional getInstanceOptional() {
|
||||
if (instanceSelection) {
|
||||
@Nullable GameInstanceID instanceId = selectedInstance.get();
|
||||
return new HMCLGameRepository.InstanceReference(instanceReference.get().repository(), instanceId);
|
||||
return HMCLGameInstance.Optional.of(instanceReference.get().repository(), instanceId);
|
||||
} else {
|
||||
return instanceReference.get();
|
||||
}
|
||||
@@ -570,7 +571,7 @@ public class DownloadListPage extends Control implements DecoratorPage, GameInst
|
||||
FXUtils.onClicked(wrapper, () -> {
|
||||
RemoteAddon item = getItem();
|
||||
if (item != null)
|
||||
Controllers.navigate(new DownloadPage(getSkinnable(), item, getSkinnable().getInstanceReference(), getSkinnable().callback));
|
||||
Controllers.navigate(new DownloadPage(getSkinnable(), item, getSkinnable().getInstanceOptional(), getSkinnable().callback));
|
||||
});
|
||||
|
||||
setPrefWidth(0);
|
||||
@@ -581,7 +582,15 @@ public class DownloadListPage extends Control implements DecoratorPage, GameInst
|
||||
|
||||
@Override
|
||||
protected void updateItem(RemoteAddon item, boolean empty) {
|
||||
RemoteAddon oldItem = getItem();
|
||||
boolean oldEmpty = isEmpty();
|
||||
|
||||
super.updateItem(item, empty);
|
||||
|
||||
if (oldItem == item && oldEmpty == empty) return;
|
||||
|
||||
this.graphic.releaseRippleImmediately();
|
||||
|
||||
if (empty || item == null) {
|
||||
setGraphic(null);
|
||||
} else {
|
||||
|
||||
@@ -30,10 +30,7 @@ import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.GameInstanceManifest;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.addon.mod.ModLoaderType;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddonRepository;
|
||||
@@ -53,6 +50,7 @@ import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
@@ -69,14 +67,14 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
private final ModTranslations translations;
|
||||
private final RemoteAddon addon;
|
||||
private final ModTranslations.Mod mod;
|
||||
private final HMCLGameRepository.InstanceReference instanceReference;
|
||||
private final HMCLGameInstance.Optional instanceReference;
|
||||
private final DownloadCallback callback;
|
||||
private final DownloadListPage page;
|
||||
private final RemoteAddon.Type type;
|
||||
|
||||
private SimpleMultimap<String, RemoteAddon.Version, List<RemoteAddon.Version>> versions;
|
||||
|
||||
public DownloadPage(DownloadListPage page, RemoteAddon addon, HMCLGameRepository.InstanceReference instanceReference, @Nullable DownloadCallback callback) {
|
||||
public DownloadPage(DownloadListPage page, RemoteAddon addon, HMCLGameInstance.Optional instanceReference, @Nullable DownloadCallback callback) {
|
||||
this.page = page;
|
||||
this.repository = page.repository;
|
||||
this.addon = addon;
|
||||
@@ -130,7 +128,7 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
return addon;
|
||||
}
|
||||
|
||||
public HMCLGameRepository.InstanceReference getInstanceReference() {
|
||||
public HMCLGameInstance.Optional getInstanceOptional() {
|
||||
return instanceReference;
|
||||
}
|
||||
|
||||
@@ -271,15 +269,14 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
FXUtils.onChangeAndOperate(control.loaded, loaded -> {
|
||||
if (control.versions == null) return;
|
||||
|
||||
if (control.instanceReference.repository() != null && control.instanceReference.instanceId() != null) {
|
||||
HMCLGameRepository repository = control.instanceReference.repository();
|
||||
GameInstanceManifest.Resolved resolvedManifest = repository.getResolvedInstanceManifest(control.instanceReference.instanceId());
|
||||
String gameVersion = repository.getGameVersion(resolvedManifest.unresolved()).orElse(null);
|
||||
|
||||
if (gameVersion != null && control.versions.containsKey(gameVersion)) {
|
||||
@Nullable HMCLGameInstance instance = control.instanceReference.instance();
|
||||
if (instance != null) {
|
||||
String gameVersion = instance.getVersion().toString();
|
||||
if (!GameVersionNumber.unknown().equals(instance.getVersion())
|
||||
&& control.versions.containsKey(gameVersion)) {
|
||||
List<RemoteAddon.Version> addonVersions = control.versions.get(gameVersion);
|
||||
if (addonVersions != null && !addonVersions.isEmpty()) {
|
||||
Set<ModLoaderType> targetLoaders = LibraryAnalyzer.analyze(resolvedManifest, gameVersion).getModLoaders();
|
||||
Set<ModLoaderType> targetLoaders = instance.getModLoaders();
|
||||
|
||||
resolve:
|
||||
for (RemoteAddon.Version addonVersion : addonVersions) {
|
||||
@@ -375,7 +372,7 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
|
||||
public final RemoteAddon addon;
|
||||
|
||||
DependencyAddonItem(DownloadListPage page, RemoteAddon addon, HMCLGameRepository.InstanceReference instanceReference) {
|
||||
DependencyAddonItem(DownloadListPage page, RemoteAddon addon, HMCLGameInstance.Optional instanceReference) {
|
||||
this.addon = addon;
|
||||
|
||||
HBox pane = new HBox(8);
|
||||
@@ -401,21 +398,15 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
});
|
||||
setNode(IDX_LEADING, pane);
|
||||
|
||||
if (addon != RemoteAddon.BROKEN) {
|
||||
ModTranslations.Mod mod = ModTranslations.getTranslationsByAddonType(type).getModByCurseForgeId(addon.slug());
|
||||
content.setTitle(mod != null && I18n.isUseChinese() ? mod.getDisplayName() : addon.title());
|
||||
content.setSubtitle(addon.description());
|
||||
for (String category : addon.categories()) {
|
||||
if (page.shouldDisplayCategory(category))
|
||||
content.addTag(page.getLocalizedCategory(category, null));
|
||||
}
|
||||
if (StringUtils.isNotBlank(addon.iconUrl())) {
|
||||
imageView.imageProperty().bind(FXUtils.newRemoteImage(addon.iconUrl(), 80, 80, true, true));
|
||||
}
|
||||
} else {
|
||||
content.setTitle(i18n("addon.broken_dependency.title"));
|
||||
content.setSubtitle(i18n("addon.broken_dependency.desc"));
|
||||
imageView.setImage(FXUtils.newBuiltinImage("/assets/img/icon@4x.png"));
|
||||
ModTranslations.Mod mod = ModTranslations.getTranslationsByAddonType(type).getModByCurseForgeId(addon.slug());
|
||||
content.setTitle(mod != null && I18n.isUseChinese() ? mod.getDisplayName() : addon.title());
|
||||
content.setSubtitle(addon.description());
|
||||
for (String category : addon.categories()) {
|
||||
if (page.shouldDisplayCategory(category))
|
||||
content.addTag(page.getLocalizedCategory(category, null));
|
||||
}
|
||||
if (StringUtils.isNotBlank(addon.iconUrl())) {
|
||||
imageView.imageProperty().bind(FXUtils.newRemoteImage(addon.iconUrl(), 80, 80, true, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -549,6 +540,7 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
scrollPane.setContent(dependenciesList);
|
||||
scrollPane.setFitToWidth(true);
|
||||
scrollPane.setFitToHeight(true);
|
||||
FXUtils.onChangeAndOperate(scrollPane.widthProperty(), d -> FXUtils.setLimitWidth(dependenciesList, d.doubleValue()));
|
||||
FXUtils.smoothScrolling(scrollPane);
|
||||
FXUtils.setOverflowHidden(scrollPane, 8);
|
||||
spinnerPane.setContent(scrollPane);
|
||||
@@ -599,9 +591,12 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
Task.composeAsync(() -> {
|
||||
// TODO: Massive tasks may cause OOM.
|
||||
EnumMap<RemoteAddon.DependencyType, Pair<Label, List<DependencyAddonItem>>> dependencies = new EnumMap<>(RemoteAddon.DependencyType.class);
|
||||
AtomicBoolean hasBroken = new AtomicBoolean(false);
|
||||
List<Task<?>> queue = new ArrayList<>(version.dependencies().size());
|
||||
for (RemoteAddon.Dependency dependency : version.dependencies()) {
|
||||
if (dependency.getType() == RemoteAddon.DependencyType.INCOMPATIBLE || dependency.getType() == RemoteAddon.DependencyType.BROKEN) {
|
||||
if (dependency.getType() == RemoteAddon.DependencyType.INCOMPATIBLE) continue;
|
||||
if (dependency.getType() == RemoteAddon.DependencyType.BROKEN) {
|
||||
hasBroken.set(true);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -616,24 +611,39 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
.setSignificance(Task.TaskSignificance.MINOR)
|
||||
.thenAcceptAsync(Schedulers.javafx(), dep -> {
|
||||
if (dep == RemoteAddon.BROKEN) {
|
||||
hasBroken.set(true);
|
||||
return;
|
||||
}
|
||||
DependencyAddonItem dependencyAddonItem = new DependencyAddonItem(selfPage.page, dep, selfPage.instanceReference);
|
||||
var listener = FXUtils.onWeakChangeAndOperate(dependenciesList.widthProperty(), d -> FXUtils.setLimitWidth(dependencyAddonItem, d.doubleValue()));
|
||||
dependencyAddonItem.getProperties().put("DependencyAddonItem.width", listener);
|
||||
dependencies.get(dependency.getType()).value().add(dependencyAddonItem);
|
||||
})
|
||||
.setSignificance(Task.TaskSignificance.MINOR));
|
||||
}
|
||||
|
||||
return Task.allOf(queue).thenSupplyAsync(() ->
|
||||
dependencies.values().stream().flatMap(types ->
|
||||
Stream.concat(
|
||||
Stream.of(types.key()),
|
||||
types.value().stream().sorted(Comparator.comparing(item -> item.addon.slug(), String.CASE_INSENSITIVE_ORDER)))
|
||||
).toList()
|
||||
);
|
||||
return Task.allOf(queue).thenSupplyAsync(() -> {
|
||||
var dependenciesStream = dependencies.values().stream().flatMap(types -> {
|
||||
if (types.value().isEmpty()) return Stream.of();
|
||||
return Stream.concat(
|
||||
Stream.of(types.key()),
|
||||
types.value().stream().sorted(Comparator.comparing(item -> item.addon.slug(), String.CASE_INSENSITIVE_ORDER)));
|
||||
});
|
||||
if (!hasBroken.get()) {
|
||||
return dependenciesStream;
|
||||
} else {
|
||||
Label warning = new Label(i18n("addon.dependencies.has_broken"));
|
||||
warning.setWrapText(true);
|
||||
warning.setPadding(new Insets(0, 8, 0, 8));
|
||||
return Stream.concat(
|
||||
Stream.of(warning),
|
||||
dependenciesStream
|
||||
);
|
||||
}
|
||||
});
|
||||
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception == null) {
|
||||
dependenciesList.getContent().setAll(result);
|
||||
dependenciesList.getContent().setAll(result.toList());
|
||||
spinnerPane.setFailedReason(null);
|
||||
} else {
|
||||
dependenciesList.getContent().setAll();
|
||||
|
||||
@@ -17,35 +17,35 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.instances;
|
||||
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.WeakChangeListener;
|
||||
import javafx.geometry.Pos;
|
||||
import org.jackhuang.hmcl.event.Event;
|
||||
import org.jackhuang.hmcl.event.EventBus;
|
||||
import org.jackhuang.hmcl.event.RefreshedGameInstancesEvent;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import javafx.scene.image.Image;
|
||||
import org.jackhuang.hmcl.game.HMCLGameInstance;
|
||||
import org.jackhuang.hmcl.setting.GameDirectoryManager;
|
||||
import org.jackhuang.hmcl.setting.GameInstanceIconType;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
|
||||
import org.jackhuang.hmcl.ui.construct.ImageContainer;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class GameAdvancedListItem extends AdvancedListItem {
|
||||
private final ImageContainer imageContainer;
|
||||
private final WeakListenerHolder holder = new WeakListenerHolder();
|
||||
private HMCLGameRepository repository;
|
||||
@SuppressWarnings("unused")
|
||||
private Consumer<Event> onInstanceIconChangedListener;
|
||||
|
||||
@SuppressWarnings({"unused", "FieldCanBeLocal"})
|
||||
private Consumer<RefreshedGameInstancesEvent> onRefreshedInstancesListener;
|
||||
/// Strongly held so [WeakChangeListener] keeps delivering icon updates.
|
||||
private final ChangeListener<Image> iconListener;
|
||||
|
||||
private @Nullable WeakChangeListener<Image> weakIconListener;
|
||||
private @Nullable ReadOnlyObjectProperty<Image> observedIcon;
|
||||
|
||||
public GameAdvancedListItem() {
|
||||
this.imageContainer = new ImageContainer(LEFT_GRAPHIC_SIZE);
|
||||
this.iconListener = (observable, oldImage, newImage) -> imageContainer.setImage(newImage);
|
||||
imageContainer.setMouseTransparent(true);
|
||||
AdvancedListItem.setAlignment(imageContainer, Pos.CENTER);
|
||||
setLeftGraphic(imageContainer);
|
||||
@@ -53,31 +53,28 @@ public class GameAdvancedListItem extends AdvancedListItem {
|
||||
holder.add(FXUtils.onWeakChangeAndOperate(GameDirectoryManager.selectedInstanceProperty(), this::loadInstance));
|
||||
}
|
||||
|
||||
private void loadInstance(GameInstanceID instanceId) {
|
||||
if (GameDirectoryManager.getSelectedRepository() != repository) {
|
||||
repository = GameDirectoryManager.getSelectedRepository();
|
||||
if (repository != null) {
|
||||
onInstanceIconChangedListener = repository.onInstanceIconChanged.registerWeak(event -> {
|
||||
FXUtils.runInFX(() -> loadInstance(repository.getSelectedInstance()));
|
||||
});
|
||||
if (!repository.isLoaded()) {
|
||||
onRefreshedInstancesListener = EventBus.EVENT_BUS.channel(RefreshedGameInstancesEvent.class)
|
||||
.registerWeak(event -> FXUtils.runInFX(() -> loadInstance(repository.getSelectedInstance())));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (instanceId != null && repository != null) {
|
||||
if (repository.hasInstance(instanceId)) {
|
||||
setTitle(i18n("instance.manage.manage"));
|
||||
setSubtitle(instanceId.toString());
|
||||
imageContainer.setImage(repository.getInstanceIconImage(instanceId));
|
||||
return;
|
||||
}
|
||||
private void loadInstance(@Nullable HMCLGameInstance instance) {
|
||||
unbindIcon();
|
||||
if (instance != null) {
|
||||
setTitle(i18n("instance.manage.manage"));
|
||||
setSubtitle(instance.getId().toString());
|
||||
observedIcon = instance.iconImageProperty();
|
||||
weakIconListener = new WeakChangeListener<>(iconListener);
|
||||
observedIcon.addListener(weakIconListener);
|
||||
imageContainer.setImage(instance.getIconImage());
|
||||
return;
|
||||
}
|
||||
|
||||
setTitle(i18n("instance.empty"));
|
||||
setSubtitle(i18n("instance.empty.add"));
|
||||
imageContainer.setImage(GameInstanceIconType.DEFAULT.getIcon());
|
||||
}
|
||||
|
||||
private void unbindIcon() {
|
||||
if (observedIcon != null && weakIconListener != null) {
|
||||
observedIcon.removeListener(weakIconListener);
|
||||
}
|
||||
observedIcon = null;
|
||||
weakIconListener = null;
|
||||
}
|
||||
}
|
||||
@@ -21,9 +21,7 @@ import javafx.scene.Node;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.FlowPane;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.event.Event;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.HMCLGameInstance;
|
||||
import org.jackhuang.hmcl.setting.GameSettings;
|
||||
import org.jackhuang.hmcl.setting.GameInstanceIconType;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
@@ -31,6 +29,7 @@ import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
@@ -39,16 +38,14 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class GameInstanceIconDialog extends DialogPane {
|
||||
private final HMCLGameRepository repository;
|
||||
private final GameInstanceID instanceId;
|
||||
private final HMCLGameInstance gameInstance;
|
||||
private final Runnable onFinish;
|
||||
private final GameSettings.Instance setting;
|
||||
private final GameSettings.@Nullable Instance setting;
|
||||
|
||||
public GameInstanceIconDialog(HMCLGameRepository repository, GameInstanceID instanceId, Runnable onFinish) {
|
||||
this.repository = repository;
|
||||
this.instanceId = instanceId;
|
||||
public GameInstanceIconDialog(HMCLGameInstance gameInstance, Runnable onFinish) {
|
||||
this.gameInstance = gameInstance;
|
||||
this.onFinish = onFinish;
|
||||
this.setting = repository.getInstanceGameSettingsOrCreate(this.instanceId);
|
||||
this.setting = gameInstance.getSettingsOrCreate();
|
||||
|
||||
setTitle(i18n("settings.icon"));
|
||||
FlowPane pane = new FlowPane();
|
||||
@@ -79,7 +76,7 @@ public class GameInstanceIconDialog extends DialogPane {
|
||||
Path selectedFile = Controllers.showOpenDialog(chooser);
|
||||
if (selectedFile != null) {
|
||||
try {
|
||||
repository.setInstanceIconFile(instanceId, selectedFile);
|
||||
gameInstance.setIconFile(selectedFile);
|
||||
|
||||
if (setting != null) {
|
||||
setting.iconProperty().setValue(GameInstanceIconType.DEFAULT);
|
||||
@@ -119,7 +116,7 @@ public class GameInstanceIconDialog extends DialogPane {
|
||||
|
||||
@Override
|
||||
protected void onAccept() {
|
||||
repository.onInstanceIconChanged.fireEvent(new Event(this));
|
||||
// Icon file / settings.iconProperty updates already invalidate iconImageProperty.
|
||||
onFinish.run();
|
||||
super.onAccept();
|
||||
}
|
||||
|
||||
@@ -21,16 +21,15 @@ import com.jfoenix.controls.JFXPopup;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.event.Event;
|
||||
import javafx.event.EventType;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.event.EventBus;
|
||||
import org.jackhuang.hmcl.event.EventPriority;
|
||||
import org.jackhuang.hmcl.event.RefreshedGameInstancesEvent;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.HMCLGameInstance;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepositorySnapshot;
|
||||
import org.jackhuang.hmcl.setting.GameSettings;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
@@ -48,8 +47,6 @@ import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
@@ -65,10 +62,19 @@ public class GameInstancePage extends DecoratorAnimatedPage implements Decorator
|
||||
private final TabHeader.Tab<ResourcePackListPage> resourcePackTab = new TabHeader.Tab<>("resourcePackTab");
|
||||
private final TransitionPane transitionPane = new TransitionPane();
|
||||
private final BooleanProperty currentInstanceUpgradable = new SimpleBooleanProperty();
|
||||
private final ObjectProperty<HMCLGameRepository.InstanceReference> instanceReference = new SimpleObjectProperty<>();
|
||||
private final ObjectProperty<HMCLGameInstance.@Nullable Optional> instance =
|
||||
new SimpleObjectProperty<>(this, "instance");
|
||||
private final WeakListenerHolder listenerHolder = new WeakListenerHolder();
|
||||
|
||||
private GameInstanceID preferredInstanceId = null;
|
||||
/// Re-resolves the page context when its repository publishes a new snapshot.
|
||||
private final ChangeListener<? super HMCLGameRepositorySnapshot> repositorySnapshotListener =
|
||||
(observable, oldValue, newValue) -> checkSelectedInstance();
|
||||
|
||||
/// Repository currently observed for snapshot publications.
|
||||
private @Nullable HMCLGameRepository observedRepository;
|
||||
|
||||
/// Last concrete instance displayed by this page.
|
||||
private @Nullable GameInstanceID preferredInstanceId;
|
||||
|
||||
public static class WorkingDirChangedEvent extends Event {
|
||||
public static final EventType<WorkingDirChangedEvent> EVENT_TYPE = new EventType<>(Event.ANY, "WORKING_DIR_CHANGED");
|
||||
@@ -79,12 +85,13 @@ public class GameInstancePage extends DecoratorAnimatedPage implements Decorator
|
||||
}
|
||||
|
||||
public GameInstancePage() {
|
||||
gameSettingsTab.setNodeSupplier(loadInstanceFor(() -> new GameSettingsPage<>(GameSettings.Instance.class)));
|
||||
installerListTab.setNodeSupplier(loadInstanceFor(InstallerListPage::new));
|
||||
modListTab.setNodeSupplier(loadInstanceFor(ModListPage::new));
|
||||
resourcePackTab.setNodeSupplier(loadInstanceFor(ResourcePackListPage::new));
|
||||
worldListTab.setNodeSupplier(loadInstanceFor(WorldListPage::new));
|
||||
schematicsTab.setNodeSupplier(loadInstanceFor(SchematicsPage::new));
|
||||
// Child tabs subscribe to instanceProperty() themselves and reload on change.
|
||||
gameSettingsTab.setNodeSupplier(() -> new GameSettingsPage<>(GameSettings.Instance.class, instance));
|
||||
installerListTab.setNodeSupplier(() -> new InstallerListPage(instance));
|
||||
modListTab.setNodeSupplier(() -> new ModListPage(instance));
|
||||
resourcePackTab.setNodeSupplier(() -> new ResourcePackListPage(instance));
|
||||
worldListTab.setNodeSupplier(() -> new WorldListPage(instance));
|
||||
schematicsTab.setNodeSupplier(() -> new SchematicsPage(instance));
|
||||
|
||||
tab = new TabHeader(transitionPane, gameSettingsTab, installerListTab, modListTab, resourcePackTab, worldListTab, schematicsTab);
|
||||
tab.select(gameSettingsTab);
|
||||
@@ -92,31 +99,64 @@ public class GameInstancePage extends DecoratorAnimatedPage implements Decorator
|
||||
addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated);
|
||||
|
||||
addEventHandler(WorkingDirChangedEvent.EVENT_TYPE, event -> {
|
||||
if (this.instanceReference.get() != null) {
|
||||
if (installerListTab.isInitialized())
|
||||
installerListTab.getNode().loadInstance(getRepository(), getInstanceId());
|
||||
if (modListTab.isInitialized())
|
||||
modListTab.getNode().loadInstance(getRepository(), getInstanceId());
|
||||
if (resourcePackTab.isInitialized())
|
||||
resourcePackTab.getNode().loadInstance(getRepository(), getInstanceId());
|
||||
if (worldListTab.isInitialized())
|
||||
worldListTab.getNode().loadInstance(getRepository(), getInstanceId());
|
||||
if (schematicsTab.isInitialized())
|
||||
schematicsTab.getNode().loadInstance(getRepository(), getInstanceId());
|
||||
HMCLGameInstance.Optional current = this.instance.get();
|
||||
if (current != null) {
|
||||
// Re-resolve so subscribed tabs reload from the current snapshot.
|
||||
this.instance.set(current.refreshed());
|
||||
}
|
||||
});
|
||||
|
||||
listenerHolder.add(EventBus.EVENT_BUS.channel(RefreshedGameInstancesEvent.class).registerWeak(event -> checkSelectedInstance(), EventPriority.HIGHEST));
|
||||
// Page chrome that depends on the current instance.
|
||||
listenerHolder.add(FXUtils.onWeakChange(instance, current -> {
|
||||
observeRepository(current);
|
||||
if (current == null) {
|
||||
return;
|
||||
}
|
||||
HMCLGameInstance gameInstance = current.instance();
|
||||
currentInstanceUpgradable.set(gameInstance != null && gameInstance.isModpack());
|
||||
if (gameInstance != null) {
|
||||
preferredInstanceId = gameInstance.getId();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/// Observes snapshot publications for the repository associated with the current page context.
|
||||
///
|
||||
/// @param current the current page context, or `null` when the page has no context
|
||||
private void observeRepository(HMCLGameInstance.@Nullable Optional current) {
|
||||
@Nullable HMCLGameRepository repository = current != null ? current.repository() : null;
|
||||
if (repository == observedRepository) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (observedRepository != null) {
|
||||
observedRepository.snapshotProperty().removeListener(repositorySnapshotListener);
|
||||
}
|
||||
observedRepository = repository;
|
||||
if (repository != null) {
|
||||
repository.snapshotProperty().addListener(repositorySnapshotListener);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the current instance context for this page and its tabs.
|
||||
///
|
||||
/// Child tabs subscribe to this property and reload when it changes. The page only publishes
|
||||
/// context; it does not push `loadInstance` into children.
|
||||
///
|
||||
/// @return the observable instance context
|
||||
public ReadOnlyObjectProperty<HMCLGameInstance.@Nullable Optional> instanceProperty() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
private void checkSelectedInstance() {
|
||||
runInFX(() -> {
|
||||
if (this.instanceReference.get() == null) return;
|
||||
HMCLGameRepository repository = this.instanceReference.get().repository();
|
||||
@Nullable GameInstanceID instanceId = this.instanceReference.get().instanceId();
|
||||
if (instanceId == null || !repository.hasInstance(instanceId)) {
|
||||
HMCLGameInstance.Optional current = this.instance.get();
|
||||
if (current == null) return;
|
||||
current = current.refreshed();
|
||||
this.instance.set(current);
|
||||
if (current.isEmpty()) {
|
||||
if (preferredInstanceId != null) {
|
||||
loadInstance(preferredInstanceId, repository);
|
||||
loadInstance(preferredInstanceId, current.repository());
|
||||
} else {
|
||||
fireEvent(new PageCloseEvent());
|
||||
}
|
||||
@@ -124,56 +164,28 @@ public class GameInstancePage extends DecoratorAnimatedPage implements Decorator
|
||||
});
|
||||
}
|
||||
|
||||
private <T extends Node> Supplier<T> loadInstanceFor(Supplier<T> nodeSupplier) {
|
||||
return () -> {
|
||||
T node = nodeSupplier.get();
|
||||
if (instanceReference.get() != null) {
|
||||
if (node instanceof GameInstancePage.GameInstanceLoadable loadable) {
|
||||
@Nullable GameInstanceID instanceId = instanceReference.get().instanceId();
|
||||
loadable.loadInstance(instanceReference.get().repository(), instanceId);
|
||||
}
|
||||
}
|
||||
return node;
|
||||
};
|
||||
}
|
||||
|
||||
public void showInstanceSettings() {
|
||||
tab.select(gameSettingsTab, false);
|
||||
}
|
||||
|
||||
public void setInstance(GameInstanceID instanceId, HMCLGameRepository repository) {
|
||||
this.instanceReference.set(new HMCLGameRepository.InstanceReference(repository, instanceId));
|
||||
this.instance.set(HMCLGameInstance.Optional.of(repository, instanceId));
|
||||
}
|
||||
|
||||
public void loadInstance(GameInstanceID instanceId, HMCLGameRepository repository) {
|
||||
// If we jumped to game list page and deleted this version
|
||||
// and back to this page, we should return to main page.
|
||||
if (this.instanceReference.get() != null && (!getRepository().isLoaded() ||
|
||||
if (this.instance.get() != null && (!getRepository().isLoaded() ||
|
||||
!getRepository().hasInstance(instanceId))) {
|
||||
Platform.runLater(() -> fireEvent(new PageCloseEvent()));
|
||||
return;
|
||||
}
|
||||
|
||||
setInstance(instanceId, repository);
|
||||
preferredInstanceId = instanceId;
|
||||
|
||||
if (gameSettingsTab.isInitialized())
|
||||
gameSettingsTab.getNode().loadInstance(repository, instanceId);
|
||||
if (installerListTab.isInitialized())
|
||||
installerListTab.getNode().loadInstance(repository, instanceId);
|
||||
if (modListTab.isInitialized())
|
||||
modListTab.getNode().loadInstance(repository, instanceId);
|
||||
if (resourcePackTab.isInitialized())
|
||||
resourcePackTab.getNode().loadInstance(repository, instanceId);
|
||||
if (worldListTab.isInitialized())
|
||||
worldListTab.getNode().loadInstance(repository, instanceId);
|
||||
if (schematicsTab.isInitialized())
|
||||
schematicsTab.getNode().loadInstance(repository, instanceId);
|
||||
currentInstanceUpgradable.set(repository.isModpack(instanceId));
|
||||
this.instance.set(HMCLGameInstance.Optional.of(repository, instanceId));
|
||||
}
|
||||
|
||||
private void onNavigated(Navigator.NavigationEvent event) {
|
||||
if (this.instanceReference.get() == null)
|
||||
if (this.instance.get() == null)
|
||||
throw new IllegalStateException();
|
||||
|
||||
// If we jumped to game list page and deleted this version
|
||||
@@ -188,11 +200,18 @@ public class GameInstancePage extends DecoratorAnimatedPage implements Decorator
|
||||
}
|
||||
|
||||
private void onBrowse(String sub) {
|
||||
FXUtils.openFolder(getRepository().getRunDirectory(getInstanceId()).resolve(sub));
|
||||
HMCLGameInstance gameInstance = requireGameInstance();
|
||||
if (gameInstance == null) {
|
||||
return;
|
||||
}
|
||||
FXUtils.openFolder(gameInstance.getRunDirectory().resolve(sub));
|
||||
}
|
||||
|
||||
private void redownloadAssetIndex() {
|
||||
Instances.updateGameAssets(getRepository(), getInstanceId());
|
||||
HMCLGameInstance gameInstance = requireGameInstance();
|
||||
if (gameInstance != null) {
|
||||
Instances.updateGameAssets(gameInstance);
|
||||
}
|
||||
}
|
||||
|
||||
private void clearLibraries() {
|
||||
@@ -209,9 +228,9 @@ public class GameInstancePage extends DecoratorAnimatedPage implements Decorator
|
||||
private void clearAssets() {
|
||||
Path assetsDir = getRepository().getBaseDirectory().resolve("assets");
|
||||
|
||||
HMCLGameRepository.InstanceReference currentInstanceReference = instanceReference.get();
|
||||
Path resourcesDir = currentInstanceReference != null
|
||||
? getRepository().getRunDirectory(currentInstanceReference.instanceId()).resolve("resources")
|
||||
HMCLGameInstance.Optional current = instance.get();
|
||||
Path resourcesDir = current != null && current.isPresent()
|
||||
? current.instance().getRunDirectory().resolve("resources")
|
||||
: null;
|
||||
|
||||
Task.runAsync(Schedulers.io(), () -> {
|
||||
@@ -227,46 +246,79 @@ public class GameInstancePage extends DecoratorAnimatedPage implements Decorator
|
||||
}
|
||||
|
||||
private void clearJunkFiles() {
|
||||
Instances.cleanInstance(getRepository(), getInstanceId());
|
||||
HMCLGameInstance gameInstance = requireGameInstance();
|
||||
if (gameInstance != null) {
|
||||
Instances.cleanInstance(gameInstance);
|
||||
}
|
||||
}
|
||||
|
||||
private void testGame() {
|
||||
Instances.testGame(getRepository(), getInstanceId());
|
||||
HMCLGameInstance gameInstance = requireGameInstance();
|
||||
if (gameInstance != null) {
|
||||
Instances.testGame(gameInstance);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateGame() {
|
||||
Instances.updateInstance(getRepository(), getInstanceId());
|
||||
HMCLGameInstance gameInstance = requireGameInstance();
|
||||
if (gameInstance != null) {
|
||||
Instances.updateInstance(gameInstance);
|
||||
}
|
||||
}
|
||||
|
||||
private void generateLaunchScript() {
|
||||
Instances.generateLaunchScript(getRepository(), getInstanceId());
|
||||
HMCLGameInstance gameInstance = requireGameInstance();
|
||||
if (gameInstance != null) {
|
||||
Instances.generateLaunchScript(gameInstance);
|
||||
}
|
||||
}
|
||||
|
||||
private void export() {
|
||||
Instances.exportInstance(getRepository(), getInstanceId());
|
||||
HMCLGameInstance gameInstance = requireGameInstance();
|
||||
if (gameInstance != null) {
|
||||
Instances.exportInstance(gameInstance);
|
||||
}
|
||||
}
|
||||
|
||||
private void rename() {
|
||||
Instances.renameInstance(getRepository(), getInstanceId())
|
||||
.thenApply(newInstanceId -> this.preferredInstanceId = new GameInstanceID(newInstanceId));
|
||||
HMCLGameInstance gameInstance = requireGameInstance();
|
||||
if (gameInstance != null) {
|
||||
Instances.renameInstance(gameInstance)
|
||||
.thenApply(newInstanceId -> this.preferredInstanceId = new GameInstanceID(newInstanceId));
|
||||
}
|
||||
}
|
||||
|
||||
private void remove() {
|
||||
Instances.deleteInstance(getRepository(), getInstanceId());
|
||||
HMCLGameInstance gameInstance = requireGameInstance();
|
||||
if (gameInstance != null) {
|
||||
Instances.deleteInstance(gameInstance);
|
||||
}
|
||||
}
|
||||
|
||||
private void duplicate() {
|
||||
Instances.duplicateInstance(getRepository(), getInstanceId());
|
||||
HMCLGameInstance gameInstance = requireGameInstance();
|
||||
if (gameInstance != null) {
|
||||
Instances.duplicateInstance(gameInstance);
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable HMCLGameInstance requireGameInstance() {
|
||||
HMCLGameInstance.Optional current = instance.get();
|
||||
return current != null ? current.instance() : null;
|
||||
}
|
||||
|
||||
public HMCLGameRepository getRepository() {
|
||||
return Optional.ofNullable(instanceReference.get()).map(HMCLGameRepository.InstanceReference::repository).orElse(null);
|
||||
HMCLGameInstance.Optional current = instance.get();
|
||||
return current != null ? current.repository() : null;
|
||||
}
|
||||
|
||||
public @Nullable GameInstanceID getInstanceId() {
|
||||
return Optional.ofNullable(instanceReference.get())
|
||||
.map(HMCLGameRepository.InstanceReference::instanceId)
|
||||
.orElse(null);
|
||||
HMCLGameInstance.Optional current = instance.get();
|
||||
return current != null ? current.instanceId() : null;
|
||||
}
|
||||
|
||||
public HMCLGameInstance.Optional getInstance() {
|
||||
return instance.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -350,7 +402,7 @@ public class GameInstancePage extends DecoratorAnimatedPage implements Decorator
|
||||
|
||||
control.state.bind(Bindings.createObjectBinding(() ->
|
||||
State.fromTitle(i18n("instance.manage.manage.title", getSkinnable().getInstanceId()), -1),
|
||||
getSkinnable().instanceReference));
|
||||
getSkinnable().instance));
|
||||
|
||||
//control.transitionPane.getStyleClass().add("gray-background");
|
||||
//FXUtils.setOverflowHidden(control.transitionPane, 8);
|
||||
@@ -358,12 +410,4 @@ public class GameInstancePage extends DecoratorAnimatedPage implements Decorator
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads page content for a game instance in a repository.
|
||||
public interface GameInstanceLoadable {
|
||||
/// Loads page content for the given repository and game instance.
|
||||
///
|
||||
/// @param repository the repository containing the game instance
|
||||
/// @param instanceId the game instance ID, or `null` when only repository context is available
|
||||
void loadInstance(HMCLGameRepository repository, @Nullable GameInstanceID instanceId);
|
||||
}
|
||||
}
|
||||
@@ -19,23 +19,20 @@ package org.jackhuang.hmcl.ui.instances;
|
||||
|
||||
import javafx.beans.property.*;
|
||||
import javafx.scene.image.Image;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.modpack.ModpackConfiguration;
|
||||
import org.jackhuang.hmcl.setting.GameDirectory;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.MINECRAFT;
|
||||
import static org.jackhuang.hmcl.util.Lang.threadPool;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
@@ -43,9 +40,7 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
public class GameItem {
|
||||
private static final ThreadPoolExecutor POOL_VERSION_RESOLVE = threadPool("VersionResolve", true, 1, 10, TimeUnit.SECONDS);
|
||||
|
||||
protected final HMCLGameRepository repository;
|
||||
protected final String id;
|
||||
protected final GameInstanceID instanceId;
|
||||
protected final HMCLGameInstance gameInstance;
|
||||
|
||||
private boolean initialized = false;
|
||||
private StringProperty title;
|
||||
@@ -53,22 +48,28 @@ public class GameItem {
|
||||
private StringProperty subtitle;
|
||||
private ObjectProperty<Image> image;
|
||||
|
||||
public GameItem(HMCLGameRepository repository, GameInstanceID instanceId) {
|
||||
this.repository = repository;
|
||||
this.id = instanceId.toString();
|
||||
this.instanceId = instanceId;
|
||||
public GameItem(HMCLGameInstance gameInstance) {
|
||||
this.gameInstance = gameInstance;
|
||||
}
|
||||
|
||||
public GameDirectory getGameDirectory() {
|
||||
return repository.getGameDirectory();
|
||||
return gameInstance.getRepository().getGameDirectory();
|
||||
}
|
||||
|
||||
public HMCLGameRepository getRepository() {
|
||||
return repository;
|
||||
return gameInstance.getRepository();
|
||||
}
|
||||
|
||||
public GameInstanceID getInstanceId() {
|
||||
return gameInstance.getId();
|
||||
}
|
||||
|
||||
public HMCLGameInstance getGameInstance() {
|
||||
return gameInstance;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
return gameInstance.getId().toString();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
@@ -86,15 +87,16 @@ public class GameItem {
|
||||
|
||||
CompletableFuture.supplyAsync(() -> {
|
||||
// GameVersion.minecraftVersion() is a time-costing job (up to ~200 ms)
|
||||
Optional<String> gameVersion = repository.getGameVersion(instanceId);
|
||||
String modPackVersion = null;
|
||||
GameVersionNumber version = gameInstance.getVersion();
|
||||
@Nullable String gameVersion = version == GameVersionNumber.unknown() ? null : version.toString();
|
||||
@Nullable String modPackVersion = null;
|
||||
try {
|
||||
ModpackConfiguration<?> config = repository.readModpackConfiguration(instanceId);
|
||||
@Nullable ModpackConfiguration<?> config = gameInstance.readModpackConfiguration();
|
||||
modPackVersion = config != null ? config.getVersion() : null;
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to read modpack configuration from " + id, e);
|
||||
LOG.warning("Failed to read modpack configuration from " + getId(), e);
|
||||
}
|
||||
return new Result(gameVersion.orElse(null), modPackVersion);
|
||||
return new Result(gameVersion, modPackVersion);
|
||||
}, POOL_VERSION_RESOLVE).whenCompleteAsync((result, exception) -> {
|
||||
if (exception == null) {
|
||||
if (result.tag != null) {
|
||||
@@ -102,26 +104,25 @@ public class GameItem {
|
||||
}
|
||||
|
||||
StringBuilder libraries = new StringBuilder(Objects.requireNonNullElse(result.gameVersion, i18n("message.unknown")));
|
||||
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(repository.getResolvedInstanceManifest(instanceId), result.gameVersion);
|
||||
for (LibraryAnalyzer.LibraryMark mark : analyzer) {
|
||||
String libraryId = mark.getLibraryId();
|
||||
String libraryVersion = mark.getLibraryVersion();
|
||||
if (libraryId.equals(MINECRAFT.getPatchId())) continue;
|
||||
if (I18n.hasKey("install.installer." + libraryId)) {
|
||||
libraries.append(", ").append(i18n("install.installer." + libraryId));
|
||||
if (libraryVersion != null)
|
||||
libraries.append(": ").append(libraryVersion.replaceAll("(?i)" + libraryId, ""));
|
||||
GameComponentAnalyzer analyzer = gameInstance.getAnalyzer();
|
||||
for (GameComponentAnalyzer.Mark mark : analyzer) {
|
||||
if (mark.componentType() == GameComponentType.GAME) continue;
|
||||
|
||||
if (I18n.hasKey("install.installer." + mark.componentType().getPatchId())) {
|
||||
libraries.append(", ").append(i18n("install.installer." + mark.componentType().getPatchId()));
|
||||
if (mark.version() != null)
|
||||
libraries.append(": ").append(mark.version().replaceAll("(?i)" + mark.componentType().getPatchId(), ""));
|
||||
}
|
||||
}
|
||||
|
||||
subtitle.set(libraries.toString());
|
||||
} else {
|
||||
LOG.warning("Failed to read version info from " + id, exception);
|
||||
LOG.warning("Failed to read version info from " + getId(), exception);
|
||||
}
|
||||
}, Schedulers.javafx());
|
||||
|
||||
title.set(id);
|
||||
image.set(repository.getInstanceIconImage(instanceId));
|
||||
title.set(getId());
|
||||
image.set(gameInstance.getIconImage());
|
||||
}
|
||||
|
||||
public ReadOnlyStringProperty titleProperty() {
|
||||
|
||||
@@ -29,7 +29,6 @@ import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Region;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
@@ -40,7 +39,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class GameListCell extends ListCell<GameListItem> {
|
||||
|
||||
private final Region graphic;
|
||||
private final RipplerContainer graphic;
|
||||
|
||||
private final ImageContainer imageView;
|
||||
private final TwoLineListItem content;
|
||||
@@ -70,7 +69,7 @@ public final class GameListCell extends ListCell<GameListItem> {
|
||||
fireEvent(new ActionEvent());
|
||||
GameListItem item = GameListCell.this.getItem();
|
||||
if (item != null) {
|
||||
item.getRepository().setSelectedInstance(new GameInstanceID(item.getId()));
|
||||
item.getRepository().setSelectedInstance(item.getGameInstance());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,8 +161,15 @@ public final class GameListCell extends ListCell<GameListItem> {
|
||||
|
||||
@Override
|
||||
public void updateItem(GameListItem item, boolean empty) {
|
||||
GameListItem oldItem = getItem();
|
||||
boolean oldEmpty = isEmpty();
|
||||
|
||||
super.updateItem(item, empty);
|
||||
|
||||
if (oldItem == item && oldEmpty == empty) return;
|
||||
|
||||
this.graphic.releaseRippleImmediately();
|
||||
|
||||
this.imageView.imageProperty().unbind();
|
||||
this.content.titleProperty().unbind();
|
||||
this.content.subtitleProperty().unbind();
|
||||
|
||||
@@ -22,22 +22,25 @@ import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.HMCLGameInstance;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.setting.GameDirectoryManager;
|
||||
|
||||
import java.util.Objects;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class GameListItem extends GameItem {
|
||||
private final boolean isModpack;
|
||||
private final BooleanProperty selected = new SimpleBooleanProperty(this, "selected");
|
||||
|
||||
public GameListItem(HMCLGameRepository repository, GameInstanceID instanceId) {
|
||||
super(repository, instanceId);
|
||||
this.isModpack = repository.isModpack(instanceId);
|
||||
public GameListItem(HMCLGameInstance gameInstance) {
|
||||
super(gameInstance);
|
||||
HMCLGameRepository repository = gameInstance.getRepository();
|
||||
GameInstanceID instanceId = gameInstance.getId();
|
||||
this.isModpack = gameInstance.isModpack();
|
||||
selected.bind(Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
if (repository.getGameDirectory() != GameDirectoryManager.getSelectedGameDirectory()) return false;
|
||||
return Objects.equals(repository.getSelectedInstance(), instanceId);
|
||||
@Nullable HMCLGameInstance selectedInstance = repository.getSelectedInstance();
|
||||
return selectedInstance != null && selectedInstance.getId().equals(instanceId);
|
||||
},
|
||||
GameDirectoryManager.selectedGameDirectoryProperty(),
|
||||
GameDirectoryManager.selectedInstanceProperty()));
|
||||
@@ -48,39 +51,39 @@ public class GameListItem extends GameItem {
|
||||
}
|
||||
|
||||
public void rename() {
|
||||
Instances.renameInstance(repository, instanceId);
|
||||
Instances.renameInstance(gameInstance);
|
||||
}
|
||||
|
||||
public void duplicate() {
|
||||
Instances.duplicateInstance(repository, instanceId);
|
||||
Instances.duplicateInstance(gameInstance);
|
||||
}
|
||||
|
||||
public void remove() {
|
||||
Instances.deleteInstance(repository, instanceId);
|
||||
Instances.deleteInstance(gameInstance);
|
||||
}
|
||||
|
||||
public void export() {
|
||||
Instances.exportInstance(repository, instanceId);
|
||||
Instances.exportInstance(gameInstance);
|
||||
}
|
||||
|
||||
public void browse() {
|
||||
Instances.openFolder(repository, instanceId);
|
||||
Instances.openFolder(gameInstance);
|
||||
}
|
||||
|
||||
public void testGame() {
|
||||
Instances.testGame(repository, instanceId);
|
||||
Instances.testGame(gameInstance);
|
||||
}
|
||||
|
||||
public void launch() {
|
||||
Instances.launch(repository, instanceId);
|
||||
Instances.launch(gameInstance);
|
||||
}
|
||||
|
||||
public void modifyGameSettings() {
|
||||
Instances.modifyGameSettings(repository, instanceId);
|
||||
Instances.modifyGameSettings(gameInstance);
|
||||
}
|
||||
|
||||
public void generateLaunchScript() {
|
||||
Instances.generateLaunchScript(repository, instanceId);
|
||||
Instances.generateLaunchScript(gameInstance);
|
||||
}
|
||||
|
||||
public boolean canUpdate() {
|
||||
@@ -88,6 +91,6 @@ public class GameListItem extends GameItem {
|
||||
}
|
||||
|
||||
public void update() {
|
||||
Instances.updateInstance(repository, instanceId);
|
||||
Instances.updateInstance(gameInstance);
|
||||
}
|
||||
}
|
||||
@@ -156,11 +156,13 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
setLoading(true);
|
||||
setFailedReason(null);
|
||||
|
||||
List<GameListItem> versionItems = repository.getDisplayInstanceManifests().map(instance -> new GameListItem(repository, instance.id())).toList();
|
||||
List<GameListItem> instanceItems = repository.getDisplayInstances()
|
||||
.map(GameListItem::new)
|
||||
.toList();
|
||||
|
||||
sourceList.setAll(versionItems);
|
||||
sourceList.setAll(instanceItems);
|
||||
|
||||
if (versionItems.isEmpty()) {
|
||||
if (instanceItems.isEmpty()) {
|
||||
setFailedReason(i18n("instance.empty.hint"));
|
||||
}
|
||||
|
||||
@@ -176,12 +178,12 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
String regex = searchText.substring("regex:".length());
|
||||
try {
|
||||
Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
|
||||
return item -> pattern.matcher(item.id).find();
|
||||
return item -> pattern.matcher(item.getId()).find();
|
||||
} catch (PatternSyntaxException e) {
|
||||
return item -> false;
|
||||
}
|
||||
} else {
|
||||
return item -> item.id.toLowerCase(Locale.ROOT).contains(searchText.toLowerCase(Locale.ROOT));
|
||||
return item -> item.getId().toLowerCase(Locale.ROOT).contains(searchText.toLowerCase(Locale.ROOT));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,9 +34,7 @@ import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.stage.WindowEvent;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.GameInstanceManifest;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.HMCLGameInstance;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.ImageContainer;
|
||||
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
@@ -47,6 +45,8 @@ import java.util.List;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
/// Displays game instances in a popup selection list.
|
||||
///
|
||||
/// @author Glavo
|
||||
public final class GameListPopupMenu extends StackPane {
|
||||
|
||||
@@ -63,20 +63,34 @@ public final class GameListPopupMenu extends StackPane {
|
||||
}
|
||||
|
||||
/// Shows an instance selection popup relative to its owner.
|
||||
///
|
||||
/// @param owner the node used to position the popup
|
||||
/// @param vAlign the popup's vertical alignment relative to `owner`
|
||||
/// @param hAlign the popup's horizontal alignment relative to `owner`
|
||||
/// @param initOffsetX the horizontal offset from the aligned position
|
||||
/// @param initOffsetY the vertical offset from the aligned position
|
||||
/// @param instances the instances to copy into the popup, in display order
|
||||
public static void show(Node owner, JFXPopup.PopupVPosition vAlign, JFXPopup.PopupHPosition hAlign,
|
||||
double initOffsetX, double initOffsetY,
|
||||
HMCLGameRepository repository, List<GameInstanceManifest> versions) {
|
||||
showAndGetPopup(owner, vAlign, hAlign, initOffsetX, initOffsetY, repository, versions);
|
||||
List<HMCLGameInstance> instances) {
|
||||
showAndGetPopup(owner, vAlign, hAlign, initOffsetX, initOffsetY, instances);
|
||||
}
|
||||
|
||||
/// Shows and returns an instance selection popup relative to its owner.
|
||||
///
|
||||
/// @param owner the node used to position the popup
|
||||
/// @param vAlign the popup's vertical alignment relative to `owner`
|
||||
/// @param hAlign the popup's horizontal alignment relative to `owner`
|
||||
/// @param initOffsetX the horizontal offset from the aligned position
|
||||
/// @param initOffsetY the vertical offset from the aligned position
|
||||
/// @param instances the instances to copy into the popup, in display order
|
||||
/// @return the shown popup
|
||||
public static JFXPopup showAndGetPopup(Node owner, JFXPopup.PopupVPosition vAlign, JFXPopup.PopupHPosition hAlign,
|
||||
double initOffsetX, double initOffsetY,
|
||||
HMCLGameRepository repository, List<GameInstanceManifest> versions) {
|
||||
List<HMCLGameInstance> instances) {
|
||||
GameListPopupMenu menu = new GameListPopupMenu();
|
||||
menu.getItems().setAll(versions.stream()
|
||||
.filter(it -> repository.hasInstance(it.id()))
|
||||
.map(it -> new GameItem(repository, it.id()))
|
||||
menu.getItems().setAll(instances.stream()
|
||||
.map(GameItem::new)
|
||||
.toList());
|
||||
JFXPopup popup = new JFXPopup(menu);
|
||||
owner.getProperties().put(KEY, popup);
|
||||
@@ -115,6 +129,7 @@ public final class GameListPopupMenu extends StackPane {
|
||||
private static final class Cell extends ListCell<GameItem> {
|
||||
|
||||
private final Region graphic;
|
||||
private final RipplerContainer ripplerContainer;
|
||||
|
||||
private final ImageContainer imageView;
|
||||
private final TwoLineListItem content;
|
||||
@@ -143,7 +158,7 @@ public final class GameListPopupMenu extends StackPane {
|
||||
container.setLeft(imageView);
|
||||
container.setCenter(content);
|
||||
|
||||
RipplerContainer ripplerContainer = new RipplerContainer(container);
|
||||
this.ripplerContainer = new RipplerContainer(container);
|
||||
|
||||
StackPane rootPane = new StackPane();
|
||||
rootPane.getStyleClass().add("advanced-list-item");
|
||||
@@ -153,7 +168,7 @@ public final class GameListPopupMenu extends StackPane {
|
||||
FXUtils.onClicked(rootPane, () -> {
|
||||
GameItem item = getItem();
|
||||
if (item != null) {
|
||||
item.getRepository().setSelectedInstance(new GameInstanceID(item.getId()));
|
||||
item.getRepository().setSelectedInstance(item.getGameInstance());
|
||||
if (getScene().getWindow() instanceof JFXPopup popup)
|
||||
popup.hide();
|
||||
}
|
||||
@@ -164,8 +179,14 @@ public final class GameListPopupMenu extends StackPane {
|
||||
|
||||
@Override
|
||||
protected void updateItem(GameItem item, boolean empty) {
|
||||
GameItem oldItem = getItem();
|
||||
boolean oldEmpty = isEmpty();
|
||||
|
||||
super.updateItem(item, empty);
|
||||
|
||||
if (oldItem == item && oldEmpty == empty) return;
|
||||
|
||||
this.ripplerContainer.releaseRippleImmediately();
|
||||
this.imageView.imageProperty().unbind();
|
||||
this.content.titleProperty().unbind();
|
||||
this.content.subtitleProperty().unbind();
|
||||
|
||||
@@ -17,13 +17,12 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.instances;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.GameInstanceManifest;
|
||||
import org.jackhuang.hmcl.game.GameComponentAnalyzer;
|
||||
import org.jackhuang.hmcl.game.HMCLGameInstance;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
@@ -36,25 +35,30 @@ import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.*;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class InstallerListPage extends ListPageBase<InstallerItem> implements GameInstancePage.GameInstanceLoadable {
|
||||
private HMCLGameRepository repository;
|
||||
private GameInstanceID instanceId;
|
||||
private GameInstanceManifest manifest;
|
||||
private String gameVersion;
|
||||
public class InstallerListPage extends ListPageBase<InstallerItem> {
|
||||
private final WeakListenerHolder listenerHolder = new WeakListenerHolder();
|
||||
private @Nullable HMCLGameInstance gameInstance;
|
||||
|
||||
{
|
||||
FXUtils.applyDragListener(this, it -> Arrays.asList("jar", "exe").contains(FileUtils.getExtension(it)), mods -> {
|
||||
/// Creates an installer list that reloads when `instanceContext` changes.
|
||||
///
|
||||
/// @param instanceContext the parent page's instance property
|
||||
public InstallerListPage(ObservableValue<? extends HMCLGameInstance.Optional> instanceContext) {
|
||||
Objects.requireNonNull(instanceContext, "instanceContext");
|
||||
FXUtils.applyDragListener(this, it -> Set.of("jar", "exe").contains(FileUtils.getExtension(it)), mods -> {
|
||||
if (!mods.isEmpty())
|
||||
doInstallOffline(mods.get(0));
|
||||
});
|
||||
|
||||
listenerHolder.add(FXUtils.onWeakChangeAndOperate(instanceContext, current -> {
|
||||
if (current != null) {
|
||||
loadInstance(current);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -62,78 +66,79 @@ public class InstallerListPage extends ListPageBase<InstallerItem> implements Ga
|
||||
return new InstallerListPageSkin();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadInstance(HMCLGameRepository repository, @Nullable GameInstanceID instanceId) {
|
||||
this.repository = repository;
|
||||
this.instanceId = instanceId;
|
||||
this.manifest = repository.getInstanceManifest(instanceId);
|
||||
this.gameVersion = null;
|
||||
|
||||
CompletableFuture.supplyAsync(() -> {
|
||||
gameVersion = repository.getGameVersion(manifest).orElse(null);
|
||||
|
||||
return LibraryAnalyzer.analyze(repository.getResolvedInstanceManifest(instanceId), gameVersion);
|
||||
}).thenAcceptAsync(analyzer -> {
|
||||
public void loadInstance(HMCLGameInstance.Optional instance) {
|
||||
HMCLGameInstance gameInstance = instance.instance();
|
||||
this.gameInstance = gameInstance;
|
||||
if (gameInstance == null) {
|
||||
itemsProperty().clear();
|
||||
return;
|
||||
}
|
||||
|
||||
InstallerItem.InstallerItemGroup group = new InstallerItem.InstallerItemGroup(gameVersion, InstallerItem.Style.LIST_ITEM);
|
||||
HMCLGameRepository repository = gameInstance.getRepository();
|
||||
|
||||
// Conventional libraries: game, fabric, legacyfabric, forge, cleanroom, neoforge, liteloader, optifine
|
||||
for (InstallerItem item : group.getLibraries()) {
|
||||
String libraryId = item.getLibraryId();
|
||||
itemsProperty().clear();
|
||||
InstallerItem.InstallerItemGroup group = new InstallerItem.InstallerItemGroup(gameInstance.getVersion(), InstallerItem.Style.LIST_ITEM);
|
||||
|
||||
// Skip fabric-api and quilt-api and legacyfabric-api
|
||||
if (libraryId.endsWith("-api")) {
|
||||
continue;
|
||||
}
|
||||
// Conventional libraries: game, fabric, legacyfabric, forge, cleanroom, neoforge, liteloader, optifine
|
||||
for (InstallerItem component : group.getComponents()) {
|
||||
|
||||
String libraryVersion = analyzer.getVersion(libraryId).orElse(null);
|
||||
|
||||
if (libraryVersion != null) {
|
||||
item.versionProperty().set(new InstallerItem.InstalledState(
|
||||
libraryVersion,
|
||||
analyzer.getLibraryStatus(libraryId) != LibraryAnalyzer.LibraryMark.LibraryStatus.CLEAR,
|
||||
false
|
||||
));
|
||||
} else {
|
||||
item.versionProperty().set(null);
|
||||
}
|
||||
|
||||
item.setOnInstall(() -> {
|
||||
Controllers.getDecorator().startWizard(new UpdateInstallerWizardProvider(repository, gameVersion, manifest, libraryId, libraryVersion));
|
||||
});
|
||||
|
||||
item.setOnRemove(() -> repository.getDependency().removeLibraryAsync(manifest, libraryId)
|
||||
.thenComposeAsync(repository::saveAsync)
|
||||
.withComposeAsync(repository.refreshAsync())
|
||||
.withRunAsync(Schedulers.javafx(), () -> loadInstance(this.repository, this.instanceId))
|
||||
.start());
|
||||
|
||||
itemsProperty().add(item);
|
||||
// Skip fabric-api and quilt-api and legacyfabric-api
|
||||
if (component.getComponentType().getPatchId().endsWith("-api")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// other third-party libraries which are unable to manage.
|
||||
for (LibraryAnalyzer.LibraryMark mark : analyzer) {
|
||||
String libraryId = mark.getLibraryId();
|
||||
String libraryVersion = mark.getLibraryVersion();
|
||||
if ("mcbbs".equals(libraryId))
|
||||
continue;
|
||||
@Nullable String libraryVersion = gameInstance.getComponentVersion(component.getComponentType());
|
||||
|
||||
// we have done this library above.
|
||||
if (LibraryAnalyzer.LibraryType.fromPatchId(libraryId) != null)
|
||||
continue;
|
||||
|
||||
InstallerItem installerItem = new InstallerItem(libraryId, InstallerItem.Style.LIST_ITEM);
|
||||
installerItem.versionProperty().set(new InstallerItem.InstalledState(libraryVersion, false, false));
|
||||
installerItem.setOnRemove(() -> repository.getDependency().removeLibraryAsync(manifest, libraryId)
|
||||
.thenComposeAsync(repository::saveAsync)
|
||||
.withComposeAsync(repository.refreshAsync())
|
||||
.withRunAsync(Schedulers.javafx(), () -> loadInstance(this.repository, this.instanceId))
|
||||
.start());
|
||||
|
||||
itemsProperty().add(installerItem);
|
||||
if (libraryVersion != null) {
|
||||
component.versionProperty().set(new InstallerItem.InstalledState(
|
||||
libraryVersion,
|
||||
!gameInstance.getAnalyzer().isClear(component.getComponentType()),
|
||||
false
|
||||
));
|
||||
} else {
|
||||
component.versionProperty().set(null);
|
||||
}
|
||||
}, Platform::runLater);
|
||||
|
||||
component.setOnInstall(() -> {
|
||||
Controllers.getDecorator().startWizard(new UpdateInstallerWizardProvider(gameInstance, component.getComponentType(), libraryVersion));
|
||||
});
|
||||
|
||||
component.setOnRemove(() -> repository.updateInstanceAsync(
|
||||
gameInstance.getId(),
|
||||
publishedInstance -> repository.getDependency().removeComponentAsync(
|
||||
publishedInstance,
|
||||
component.getComponentType()))
|
||||
.withRunAsync(Schedulers.javafx(), this::reloadCurrentInstance)
|
||||
.start());
|
||||
|
||||
itemsProperty().add(component);
|
||||
}
|
||||
|
||||
var currentItems = getItems().stream().map(InstallerItem::getComponentType).toList();
|
||||
// other third-party libraries which are unable to manage.
|
||||
for (GameComponentAnalyzer.Mark mark : gameInstance.getAnalyzer()) {
|
||||
if (currentItems.contains(mark.componentType())) continue;
|
||||
|
||||
// we have done this library above.
|
||||
|
||||
InstallerItem installerItem = new InstallerItem(mark.componentType(), InstallerItem.Style.LIST_ITEM);
|
||||
installerItem.versionProperty().set(new InstallerItem.InstalledState(mark.version(), false, false));
|
||||
installerItem.setOnRemove(() -> repository.updateInstanceAsync(
|
||||
gameInstance.getId(),
|
||||
publishedInstance -> repository.getDependency().removeComponentAsync(
|
||||
publishedInstance,
|
||||
mark.componentType()))
|
||||
.withRunAsync(Schedulers.javafx(), this::reloadCurrentInstance)
|
||||
.start());
|
||||
|
||||
itemsProperty().add(installerItem);
|
||||
}
|
||||
}
|
||||
|
||||
private void reloadCurrentInstance() {
|
||||
if (gameInstance != null) {
|
||||
loadInstance(HMCLGameInstance.Optional.of(gameInstance.getRepository(), gameInstance.getId()));
|
||||
}
|
||||
}
|
||||
|
||||
public void installOffline() {
|
||||
@@ -144,16 +149,21 @@ public class InstallerListPage extends ListPageBase<InstallerItem> implements Ga
|
||||
}
|
||||
|
||||
private void doInstallOffline(Path file) {
|
||||
Task<?> task = repository.getDependency().installLibraryAsync(manifest, file)
|
||||
.thenComposeAsync(repository::saveAsync)
|
||||
.thenComposeAsync(repository.refreshAsync());
|
||||
if (gameInstance == null || !gameInstance.getManifest().isModifiable()) {
|
||||
return;
|
||||
}
|
||||
|
||||
HMCLGameRepository repository = gameInstance.getRepository();
|
||||
Task<?> task = repository.updateInstanceAsync(
|
||||
gameInstance.getId(),
|
||||
publishedInstance -> repository.getDependency().installComponentLocalAsync(publishedInstance, file));
|
||||
task.setName(i18n("install.installer.install_offline"));
|
||||
TaskExecutor executor = task.executor(new TaskListener() {
|
||||
@Override
|
||||
public void onStop(boolean success, TaskExecutor executor) {
|
||||
runInFX(() -> {
|
||||
if (success) {
|
||||
loadInstance(repository, instanceId);
|
||||
reloadCurrentInstance();
|
||||
Controllers.dialog(i18n("install.success"));
|
||||
} else {
|
||||
if (executor.getException() == null)
|
||||
|
||||
@@ -28,11 +28,7 @@ import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
|
||||
import org.jackhuang.hmcl.download.game.GameDownloadTask;
|
||||
import org.jackhuang.hmcl.download.game.GameLibrariesTask;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.GameInstanceManifest;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.LauncherHelper;
|
||||
import org.jackhuang.hmcl.game.QuickPlayOption;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.setting.*;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
@@ -52,6 +48,7 @@ import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
@@ -60,6 +57,7 @@ import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
@@ -106,9 +104,7 @@ public final class Instances {
|
||||
if (StringUtils.isNotBlank(mod.iconUrl()))
|
||||
installWizardProvider.setIconUrl(mod.iconUrl());
|
||||
Controllers.getDecorator().startWizard(installWizardProvider);
|
||||
} else if (e instanceof CancellationException) {
|
||||
Controllers.showToast(i18n("message.cancelled"));
|
||||
} else {
|
||||
} else if (!(e instanceof CancellationException)) {
|
||||
Controllers.dialog(
|
||||
i18n("install.failed.downloading.detail", file.file().url()) + "\n" + StringUtils.getStackTrace(e),
|
||||
i18n("download.failed.no_code"), MessageDialogPane.MessageType.ERROR);
|
||||
@@ -119,9 +115,11 @@ public final class Instances {
|
||||
);
|
||||
}
|
||||
|
||||
public static void deleteInstance(HMCLGameRepository repository, GameInstanceID instanceId) {
|
||||
boolean isIndependent = repository.getRunDirectory(instanceId).toAbsolutePath().normalize()
|
||||
.equals(repository.getInstanceRoot(instanceId).toAbsolutePath().normalize());
|
||||
public static void deleteInstance(HMCLGameInstance gameInstance) {
|
||||
HMCLGameRepository repository = gameInstance.getRepository();
|
||||
GameInstanceID instanceId = gameInstance.getId();
|
||||
boolean isIndependent = gameInstance.getRunDirectory().toAbsolutePath().normalize()
|
||||
.equals(gameInstance.getInstanceRoot().toAbsolutePath().normalize());
|
||||
String message = isIndependent ? i18n("instance.manage.remove.confirm.independent", instanceId) :
|
||||
i18n("instance.manage.remove.confirm.trash", instanceId, instanceId + "_removed");
|
||||
|
||||
@@ -139,7 +137,9 @@ public final class Instances {
|
||||
Controllers.confirmAction(message, i18n("message.warning"), MessageDialogPane.MessageType.WARNING, deleteButton);
|
||||
}
|
||||
|
||||
public static CompletableFuture<String> renameInstance(HMCLGameRepository repository, GameInstanceID instanceId) {
|
||||
public static CompletableFuture<String> renameInstance(HMCLGameInstance gameInstance) {
|
||||
HMCLGameRepository repository = gameInstance.getRepository();
|
||||
GameInstanceID instanceId = gameInstance.getId();
|
||||
return Controllers.prompt(i18n("instance.manage.rename.message"), (newName, handler) -> {
|
||||
if (newName.equals(instanceId.toString())) {
|
||||
handler.resolve();
|
||||
@@ -152,7 +152,7 @@ public final class Instances {
|
||||
repository.refreshAsync()
|
||||
.thenRunAsync(Schedulers.javafx(), () -> {
|
||||
if (repository.hasInstance(newInstanceId)) {
|
||||
repository.setSelectedInstance(newInstanceId);
|
||||
repository.setSelectedInstance(repository.getInstance(newInstanceId));
|
||||
}
|
||||
}).start();
|
||||
} else {
|
||||
@@ -163,22 +163,24 @@ public final class Instances {
|
||||
new Validator(i18n("install.new_game.already_exists"), newVersionName -> !repository.instanceIdConflicts(newVersionName) || newVersionName.equals(instanceId.toString())));
|
||||
}
|
||||
|
||||
public static void exportInstance(HMCLGameRepository repository, GameInstanceID instanceId) {
|
||||
Controllers.getDecorator().startWizard(new ExportWizardProvider(repository, instanceId), i18n("modpack.wizard"));
|
||||
public static void exportInstance(HMCLGameInstance gameInstance) {
|
||||
Controllers.getDecorator().startWizard(new ExportWizardProvider(gameInstance), i18n("modpack.wizard"));
|
||||
}
|
||||
|
||||
public static void openFolder(HMCLGameRepository repository, GameInstanceID instanceId) {
|
||||
FXUtils.openFolder(repository.getRunDirectory(instanceId));
|
||||
public static void openFolder(HMCLGameInstance gameInstance) {
|
||||
FXUtils.openFolder(gameInstance.getRunDirectory());
|
||||
}
|
||||
|
||||
public static void installFromJson(HMCLGameRepository repository, Path file) {
|
||||
GameInstanceManifest manifest;
|
||||
try {
|
||||
manifest = JsonUtils.fromJsonFile(file, GameInstanceManifest.class);
|
||||
if (manifest == null) {
|
||||
if (manifest == null)
|
||||
throw new IllegalArgumentException("Missing game manifest");
|
||||
}
|
||||
if (manifest.inheritsFrom() != null)
|
||||
throw new IllegalArgumentException("Game manifest inherits from another manifest");
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to read game manifest from " + file, e);
|
||||
Controllers.dialog(i18n("install.new_game.malformed_json"), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
|
||||
return;
|
||||
}
|
||||
@@ -189,21 +191,48 @@ public final class Instances {
|
||||
GameInstanceID instanceId = new GameInstanceID(result);
|
||||
|
||||
DefaultDependencyManager dependencyManager = repository.getDependency();
|
||||
GameInstanceManifest newVersion = manifest.withId(instanceId).withJar(instanceId);
|
||||
GameInstanceManifest newManifest = manifest.withId(instanceId).withJar(instanceId);
|
||||
GameDownloadTask gameDownloadTask = new GameDownloadTask(
|
||||
dependencyManager,
|
||||
newManifest);
|
||||
AtomicReference<GameRepositoryDraft> activeDraft = new AtomicReference<>();
|
||||
|
||||
Controllers.taskDialog(
|
||||
Task.allOf(new GameDownloadTask(dependencyManager, null, newVersion),
|
||||
Task.supplyAsync(() -> {
|
||||
GameRepositoryDraft draft = repository.openDraft();
|
||||
activeDraft.set(draft);
|
||||
return draft;
|
||||
})
|
||||
.thenComposeAsync(draft -> Task.allOf(
|
||||
gameDownloadTask,
|
||||
Task.allOf(
|
||||
new GameAssetDownloadTask(dependencyManager, newVersion, GameAssetDownloadTask.DOWNLOAD_INDEX_FORCIBLY, true),
|
||||
new GameLibrariesTask(dependencyManager, newVersion, true)
|
||||
).withRunAsync(() -> {
|
||||
// ignore failure
|
||||
}))
|
||||
.thenComposeAsync(repository.saveAsync(newVersion))
|
||||
.thenRunAsync(repository::refresh)
|
||||
new GameAssetDownloadTask(
|
||||
dependencyManager,
|
||||
newManifest,
|
||||
GameAssetDownloadTask.DOWNLOAD_INDEX_FORCIBLY,
|
||||
true),
|
||||
new GameLibrariesTask(dependencyManager, newManifest, true))
|
||||
.withRunAsync(() -> {
|
||||
// ignore failure
|
||||
})))
|
||||
.thenAcceptAsync(ignored -> {
|
||||
GameRepositoryDraft draft = activeDraft.get();
|
||||
if (draft == null) {
|
||||
throw new IllegalStateException("Game repository draft is unavailable");
|
||||
}
|
||||
draft.put(newManifest);
|
||||
draft.putPrimaryJar(instanceId, gameDownloadTask.getResult());
|
||||
draft.commit();
|
||||
})
|
||||
.whenComplete(exception -> {
|
||||
GameRepositoryDraft draft = activeDraft.getAndSet(null);
|
||||
if (draft != null && draft.isOpen()) {
|
||||
draft.abort();
|
||||
}
|
||||
})
|
||||
.whenComplete(Schedulers.javafx(), (exception) -> {
|
||||
if (exception == null) {
|
||||
repository.setSelectedInstance(new GameInstanceID(result));
|
||||
repository.setSelectedInstance(repository.getInstance(instanceId));
|
||||
} else {
|
||||
Controllers.dialog(
|
||||
DownloadProviders.localizeErrorMessage(exception), i18n("install.failed"), MessageDialogPane.MessageType.ERROR);
|
||||
@@ -212,7 +241,9 @@ public final class Instances {
|
||||
}, FileUtils.getNameWithoutExtension(file), new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidInstanceId), new Validator(i18n("install.new_game.already_exists"), newVersionName -> !repository.instanceIdConflicts(newVersionName)));
|
||||
}
|
||||
|
||||
public static void duplicateInstance(HMCLGameRepository repository, GameInstanceID instanceId) {
|
||||
public static void duplicateInstance(HMCLGameInstance gameInstance) {
|
||||
HMCLGameRepository repository = gameInstance.getRepository();
|
||||
GameInstanceID instanceId = gameInstance.getId();
|
||||
Controllers.prompt(
|
||||
new PromptDialogPane.Builder(i18n("instance.manage.duplicate.prompt"), (res, handler) -> {
|
||||
String newInstanceName = ((PromptDialogPane.Builder.StringQuestion) res.get(1)).getValue();
|
||||
@@ -238,33 +269,35 @@ public final class Instances {
|
||||
.addQuestion(new PromptDialogPane.Builder.BooleanQuestion(i18n("instance.manage.duplicate.duplicate_save"), false)));
|
||||
}
|
||||
|
||||
public static void updateInstance(HMCLGameRepository repository, GameInstanceID instanceId) {
|
||||
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(repository, instanceId));
|
||||
public static void updateInstance(HMCLGameInstance gameInstance) {
|
||||
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(gameInstance.getRepository(), gameInstance.getId()));
|
||||
}
|
||||
|
||||
public static void updateGameAssets(HMCLGameRepository repository, GameInstanceID instanceId) {
|
||||
TaskExecutor executor = new GameAssetDownloadTask(repository.getDependency(), repository.getInstanceManifest(instanceId), GameAssetDownloadTask.DOWNLOAD_INDEX_FORCIBLY, true)
|
||||
.executor();
|
||||
public static void updateGameAssets(HMCLGameInstance gameInstance) {
|
||||
TaskExecutor executor = new GameAssetDownloadTask(
|
||||
gameInstance.getRepository().getDependency(),
|
||||
gameInstance.getResolvedManifest(),
|
||||
GameAssetDownloadTask.DOWNLOAD_INDEX_FORCIBLY,
|
||||
true).executor();
|
||||
Controllers.taskDialog(executor, i18n("instance.manage.redownload_assets_index"), TaskCancellationAction.NO_CANCEL);
|
||||
executor.start();
|
||||
}
|
||||
|
||||
public static void cleanInstance(HMCLGameRepository repository, GameInstanceID instanceId) {
|
||||
public static void cleanInstance(HMCLGameInstance gameInstance) {
|
||||
try {
|
||||
repository.clean(instanceId);
|
||||
gameInstance.getRepository().clean(gameInstance.getId());
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Unable to clean game directory", e);
|
||||
}
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public static void generateLaunchScript(HMCLGameRepository repository, GameInstanceID instanceId, Consumer<LauncherHelper>... injecters) {
|
||||
if (!checkVersionForLaunching(repository, instanceId))
|
||||
return;
|
||||
public static void generateLaunchScript(HMCLGameInstance gameInstance, Consumer<LauncherHelper>... injecters) {
|
||||
ensureSelectedAccount(account -> {
|
||||
Path runDirectory = gameInstance.getRunDirectory();
|
||||
FileChooser chooser = new FileChooser();
|
||||
if (Files.isDirectory(repository.getRunDirectory(instanceId)))
|
||||
chooser.setInitialDirectory(repository.getRunDirectory(instanceId).toFile());
|
||||
if (Files.isDirectory(runDirectory))
|
||||
chooser.setInitialDirectory(runDirectory.toFile());
|
||||
chooser.setTitle(i18n("instance.launch_script.save"));
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
|
||||
chooser.getExtensionFilters().add(
|
||||
@@ -282,7 +315,7 @@ public final class Instances {
|
||||
file = file.resolveSibling(file.getFileName().toString() + "." + defaultExt);
|
||||
}
|
||||
|
||||
LauncherHelper launcherHelper = new LauncherHelper(repository, account, instanceId);
|
||||
LauncherHelper launcherHelper = new LauncherHelper(gameInstance, account);
|
||||
for (Consumer<LauncherHelper> injecter : injecters) {
|
||||
injecter.accept(launcherHelper);
|
||||
}
|
||||
@@ -306,42 +339,16 @@ public final class Instances {
|
||||
};
|
||||
}
|
||||
|
||||
/// Launches the given instance after ensuring that an account is selected.
|
||||
///
|
||||
/// If `gameInstance` is `null`, an error dialog is shown and no account selection or launch is
|
||||
/// attempted.
|
||||
///
|
||||
/// @param gameInstance the instance to launch, or `null` when no instance is available
|
||||
/// @param injecters callbacks that configure the launcher before launch
|
||||
@SafeVarargs
|
||||
public static void launch(HMCLGameRepository repository, GameInstanceID instanceId, Consumer<LauncherHelper>... injecters) {
|
||||
if (!checkVersionForLaunching(repository, instanceId))
|
||||
return;
|
||||
ensureSelectedAccount(account -> {
|
||||
LauncherHelper launcherHelper = new LauncherHelper(repository, account, instanceId);
|
||||
for (Consumer<LauncherHelper> injecter : injecters) {
|
||||
injecter.accept(launcherHelper);
|
||||
}
|
||||
launcherHelper.launch();
|
||||
});
|
||||
}
|
||||
|
||||
public static void testGame(HMCLGameRepository repository, GameInstanceID instanceId) {
|
||||
launch(repository, instanceId, LauncherHelper::setTestMode);
|
||||
}
|
||||
|
||||
public static void launchAndEnterWorld(HMCLGameRepository repository, GameInstanceID instanceId, String worldFolderName) {
|
||||
launch(repository, instanceId, launcherHelper ->
|
||||
launcherHelper.setQuickPlayOption(new QuickPlayOption.SinglePlayer(worldFolderName)));
|
||||
}
|
||||
|
||||
public static void generateLaunchScriptForQuickEnterWorld(HMCLGameRepository repository, GameInstanceID instanceId, String worldFolderName) {
|
||||
generateLaunchScript(repository, instanceId, launcherHelper ->
|
||||
launcherHelper.setQuickPlayOption(new QuickPlayOption.SinglePlayer(worldFolderName)));
|
||||
}
|
||||
|
||||
private static boolean checkVersionForLaunching(HMCLGameRepository repository, GameInstanceID instanceId) {
|
||||
boolean unavailable;
|
||||
if (instanceId == null || !repository.isLoaded()) {
|
||||
unavailable = true;
|
||||
} else {
|
||||
unavailable = !repository.hasInstance(instanceId);
|
||||
}
|
||||
|
||||
if (unavailable) {
|
||||
public static void launch(@Nullable HMCLGameInstance gameInstance, Consumer<LauncherHelper>... injecters) {
|
||||
if (gameInstance == null) {
|
||||
JFXButton gotoDownload = new JFXButton(i18n("instance.empty.launch.goto_download"));
|
||||
gotoDownload.getStyleClass().add("dialog-accept");
|
||||
gotoDownload.setOnAction(e -> Controllers.navigate(Controllers.getDownloadPage()));
|
||||
@@ -350,10 +357,30 @@ public final class Instances {
|
||||
MessageDialogPane.MessageType.ERROR,
|
||||
gotoDownload,
|
||||
null);
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
|
||||
ensureSelectedAccount(account -> {
|
||||
LauncherHelper launcherHelper = new LauncherHelper(gameInstance, account);
|
||||
for (Consumer<LauncherHelper> injecter : injecters) {
|
||||
injecter.accept(launcherHelper);
|
||||
}
|
||||
launcherHelper.launch();
|
||||
});
|
||||
}
|
||||
|
||||
public static void testGame(HMCLGameInstance gameInstance) {
|
||||
launch(gameInstance, LauncherHelper::setTestMode);
|
||||
}
|
||||
|
||||
public static void launchAndEnterWorld(HMCLGameInstance gameInstance, String worldFolderName) {
|
||||
launch(gameInstance, launcherHelper ->
|
||||
launcherHelper.setQuickPlayOption(new QuickPlayOption.SinglePlayer(worldFolderName)));
|
||||
}
|
||||
|
||||
public static void generateLaunchScriptForQuickEnterWorld(HMCLGameInstance gameInstance, String worldFolderName) {
|
||||
generateLaunchScript(gameInstance, launcherHelper ->
|
||||
launcherHelper.setQuickPlayOption(new QuickPlayOption.SinglePlayer(worldFolderName)));
|
||||
}
|
||||
|
||||
private static void ensureSelectedAccount(Consumer<Account> action) {
|
||||
@@ -391,10 +418,11 @@ public final class Instances {
|
||||
Controllers.navigate(Controllers.getSettingsPage());
|
||||
}
|
||||
|
||||
public static void modifyGameSettings(HMCLGameRepository repository, GameInstanceID instanceId) {
|
||||
Controllers.getGameInstancePage().setInstance(instanceId, repository);
|
||||
public static void modifyGameSettings(HMCLGameInstance gameInstance) {
|
||||
Controllers.getGameInstancePage().setInstance(gameInstance.getId(), gameInstance.getRepository());
|
||||
Controllers.getGameInstancePage().showInstanceSettings();
|
||||
// VersionPage.loadVersion will be invoked after navigation
|
||||
Controllers.navigate(Controllers.getGameInstancePage());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,51 +17,93 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.instances;
|
||||
|
||||
import com.jfoenix.controls.*;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.DoubleBinding;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.GameInstanceManifest;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.addon.repository.CurseForgeRemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.addon.repository.ModrinthRemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.addon.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.addon.mod.ModLoaderType;
|
||||
import org.jackhuang.hmcl.addon.mod.ModManager;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.setting.GameDirectory;
|
||||
import org.jackhuang.hmcl.setting.GameInstanceIconType;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.ListPageBase;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.PageAware;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.ui.*;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.lang.ref.SoftReference;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.ignoreEvent;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2;
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObject> implements GameInstancePage.GameInstanceLoadable, PageAware {
|
||||
public final class ModListPage extends ListPageBase<ModListPage.ModInfoObject> implements PageAware {
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
private final WeakListenerHolder listenerHolder = new WeakListenerHolder();
|
||||
|
||||
private ModManager modManager;
|
||||
private HMCLGameRepository repository;
|
||||
private GameInstanceID instanceId;
|
||||
private @Nullable HMCLGameInstance gameInstance;
|
||||
private String gameVersion;
|
||||
|
||||
final EnumSet<ModLoaderType> supportedLoaders = EnumSet.noneOf(ModLoaderType.class);
|
||||
|
||||
public ModListPage() {
|
||||
/// Creates a mod list that reloads when `instanceContext` changes.
|
||||
///
|
||||
/// @param instanceContext the parent page's instance property
|
||||
public ModListPage(ObservableValue<? extends HMCLGameInstance.Optional> instanceContext) {
|
||||
Objects.requireNonNull(instanceContext, "instanceContext");
|
||||
FXUtils.applyDragListener(this, it -> ModManager.MOD_EXTENSIONS.contains(FileUtils.getExtension(it).toLowerCase(Locale.ROOT)), mods -> {
|
||||
mods.forEach(it -> {
|
||||
try {
|
||||
@@ -72,6 +114,12 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
});
|
||||
loadMods(modManager);
|
||||
});
|
||||
|
||||
listenerHolder.add(FXUtils.onWeakChangeAndOperate(instanceContext, current -> {
|
||||
if (current != null) {
|
||||
loadInstance(current);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -83,15 +131,15 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
loadMods(modManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadInstance(HMCLGameRepository repository, @Nullable GameInstanceID instanceId) {
|
||||
this.repository = repository;
|
||||
this.instanceId = instanceId;
|
||||
public void loadInstance(HMCLGameInstance.Optional instance) {
|
||||
this.gameInstance = instance.instance();
|
||||
if (gameInstance == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
GameInstanceManifest resolved = repository.getResolvedInstanceManifest(instanceId).standaloneManifest();
|
||||
this.gameVersion = repository.getGameVersion(resolved).orElse(null);
|
||||
this.gameVersion = gameInstance.getVersion().toString();
|
||||
|
||||
loadMods(repository.getModManager(instanceId));
|
||||
loadMods(gameInstance.getModManager());
|
||||
}
|
||||
|
||||
private void loadMods(ModManager modManager) {
|
||||
@@ -105,7 +153,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
lock.lock();
|
||||
try {
|
||||
modManager.refresh();
|
||||
return modManager.getLocalFiles().stream().map(ModListPageSkin.ModInfoObject::new).toList();
|
||||
return modManager.getLocalFiles().stream().map(ModInfoObject::new).toList();
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
} finally {
|
||||
@@ -131,13 +179,13 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
private void updateSupportedLoaders(ModManager modManager) {
|
||||
supportedLoaders.clear();
|
||||
|
||||
LibraryAnalyzer analyzer = modManager.getLibraryAnalyzer();
|
||||
GameComponentAnalyzer analyzer = modManager.getComponentAnalyzer();
|
||||
if (analyzer == null) {
|
||||
Collections.addAll(supportedLoaders, ModLoaderType.values());
|
||||
return;
|
||||
}
|
||||
|
||||
for (LibraryAnalyzer.LibraryType type : LibraryAnalyzer.LibraryType.values()) {
|
||||
for (GameComponentType type : GameComponentType.MOD_LOADERS) {
|
||||
if (type.isModLoader() && analyzer.has(type)) {
|
||||
ModLoaderType modLoaderType = type.getModLoaderType();
|
||||
if (modLoaderType != null) {
|
||||
@@ -149,26 +197,26 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
}
|
||||
}
|
||||
|
||||
if (analyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE) && "1.20.1".equals(gameVersion)) {
|
||||
if (analyzer.has(GameComponentType.NEO_FORGE) && "1.20.1".equals(gameVersion)) {
|
||||
supportedLoaders.add(ModLoaderType.FORGE);
|
||||
}
|
||||
|
||||
if (analyzer.has(LibraryAnalyzer.LibraryType.QUILT)) {
|
||||
if (analyzer.has(GameComponentType.QUILT)) {
|
||||
supportedLoaders.add(ModLoaderType.FABRIC);
|
||||
}
|
||||
|
||||
if (analyzer.has(LibraryAnalyzer.LibraryType.LEGACY_FABRIC)) {
|
||||
if (analyzer.has(GameComponentType.LEGACY_FABRIC)) {
|
||||
supportedLoaders.add(ModLoaderType.FABRIC);
|
||||
}
|
||||
|
||||
if (analyzer.has(LibraryAnalyzer.LibraryType.FABRIC) && modManager.hasMod("kilt", ModLoaderType.FABRIC)) {
|
||||
if (analyzer.has(GameComponentType.FABRIC) && modManager.hasMod("kilt", ModLoaderType.FABRIC)) {
|
||||
supportedLoaders.add(ModLoaderType.FORGE);
|
||||
supportedLoaders.add(ModLoaderType.NEO_FORGE);
|
||||
}
|
||||
|
||||
// Sinytra Connector
|
||||
if (analyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE) && (modManager.hasMod("connector", ModLoaderType.NEO_FORGE) || modManager.hasMod("connectormod", ModLoaderType.NEO_FORGE))
|
||||
|| "1.20.1".equals(gameVersion) && analyzer.has(LibraryAnalyzer.LibraryType.FORGE) && modManager.hasMod("connectormod", ModLoaderType.FORGE)) {
|
||||
if (analyzer.has(GameComponentType.NEO_FORGE) && (modManager.hasMod("connector", ModLoaderType.NEO_FORGE) || modManager.hasMod("connectormod", ModLoaderType.NEO_FORGE))
|
||||
|| "1.20.1".equals(gameVersion) && analyzer.has(GameComponentType.FORGE) && modManager.hasMod("connectormod", ModLoaderType.FORGE)) {
|
||||
supportedLoaders.add(ModLoaderType.FABRIC);
|
||||
}
|
||||
}
|
||||
@@ -176,7 +224,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
public void add() {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.setTitle(i18n("mods.add.title"));
|
||||
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("extension.mod"), "*.jar", "*.litemod"));
|
||||
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("extension.mod"), "*.jar", "*.zip", "*.litemod"));
|
||||
List<Path> res = Controllers.showOpenMultipleDialog(chooser);
|
||||
|
||||
if (res == null) return;
|
||||
@@ -208,11 +256,11 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
}).start();
|
||||
}
|
||||
|
||||
void removeSelected(ObservableList<ModListPageSkin.ModInfoObject> selectedItems) {
|
||||
void removeSelected(ObservableList<ModInfoObject> selectedItems) {
|
||||
try {
|
||||
modManager.removeMods(selectedItems.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(ModListPageSkin.ModInfoObject::getModInfo)
|
||||
.map(ModInfoObject::getModInfo)
|
||||
.toArray(LocalModFile[]::new));
|
||||
loadMods(modManager);
|
||||
} catch (IOException ignore) {
|
||||
@@ -220,34 +268,40 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
}
|
||||
}
|
||||
|
||||
void enableSelected(ObservableList<ModListPageSkin.ModInfoObject> selectedItems) {
|
||||
void enableSelected(ObservableList<ModInfoObject> selectedItems) {
|
||||
selectedItems.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(ModListPageSkin.ModInfoObject::getModInfo)
|
||||
.map(ModInfoObject::getModInfo)
|
||||
.forEach(info -> info.setActive(true));
|
||||
}
|
||||
|
||||
void disableSelected(ObservableList<ModListPageSkin.ModInfoObject> selectedItems) {
|
||||
void disableSelected(ObservableList<ModInfoObject> selectedItems) {
|
||||
selectedItems.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(ModListPageSkin.ModInfoObject::getModInfo)
|
||||
.map(ModInfoObject::getModInfo)
|
||||
.forEach(info -> info.setActive(false));
|
||||
}
|
||||
|
||||
public void openModFolder() {
|
||||
FXUtils.openFolder(repository.getRunDirectory(instanceId).resolve("mods"));
|
||||
if (gameInstance != null) {
|
||||
FXUtils.openFolder(gameInstance.getModsDirectory());
|
||||
}
|
||||
}
|
||||
|
||||
public void checkUpdates(Collection<LocalModFile> mods) {
|
||||
Objects.requireNonNull(mods);
|
||||
if (isLoading()) {
|
||||
if (isLoading() || gameInstance == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
HMCLGameInstance gameInstance = this.gameInstance;
|
||||
Runnable action = () -> Controllers.taskDialog(Task
|
||||
.composeAsync(() -> {
|
||||
Optional<String> gameVersion = repository.getGameVersion(instanceId);
|
||||
return gameVersion.map(g -> new AddonCheckUpdatesTask<>(DownloadProviders.getDownloadProvider(), g, mods)).orElse(null);
|
||||
GameVersionNumber version = gameInstance.getVersion();
|
||||
return version != GameVersionNumber.unknown()
|
||||
? new AddonCheckUpdatesTask<>(
|
||||
DownloadProviders.getDownloadProvider(), version.toString(), mods)
|
||||
: null;
|
||||
})
|
||||
.whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception instanceof CancellationException) return;
|
||||
@@ -262,7 +316,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
.withStagesHints("update.checking"),
|
||||
i18n("addon.check_update"), TaskCancellationAction.NORMAL);
|
||||
|
||||
if (repository.isModpack(instanceId)) {
|
||||
if (gameInstance.isModpack()) {
|
||||
Controllers.confirm(
|
||||
i18n("mods.update_modpack_mod.warning"), null,
|
||||
MessageDialogPane.MessageType.WARNING,
|
||||
@@ -273,7 +327,10 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
}
|
||||
|
||||
public void download() {
|
||||
Controllers.getDownloadPage().showModDownloads().selectInstance(instanceId);
|
||||
if (gameInstance == null) {
|
||||
return;
|
||||
}
|
||||
Controllers.getDownloadPage().showModDownloads().selectInstance(gameInstance.getId());
|
||||
Controllers.navigate(Controllers.getDownloadPage());
|
||||
}
|
||||
|
||||
@@ -287,14 +344,597 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
}
|
||||
|
||||
public GameDirectory getGameDirectory() {
|
||||
return this.repository.getGameDirectory();
|
||||
return gameInstance != null ? gameInstance.getRepository().getGameDirectory() : null;
|
||||
}
|
||||
|
||||
public HMCLGameRepository getRepository() {
|
||||
return this.repository;
|
||||
return gameInstance != null ? gameInstance.getRepository() : null;
|
||||
}
|
||||
|
||||
public GameInstanceID getInstanceId() {
|
||||
return this.instanceId;
|
||||
return gameInstance != null ? gameInstance.getId() : null;
|
||||
}
|
||||
|
||||
@NotNullByDefault
|
||||
private static final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
|
||||
private final TransitionPane toolbarPane;
|
||||
private final HBox searchBar;
|
||||
private final HBox toolbarNormal;
|
||||
private final HBox toolbarSelecting;
|
||||
|
||||
private final JFXListView<ModListPage.ModInfoObject> listView;
|
||||
|
||||
/// Whether the search mechanism is currently active.
|
||||
private final BooleanProperty isSearching = new SimpleBooleanProperty(false);
|
||||
|
||||
private final JFXTextField searchField;
|
||||
|
||||
/// Timer for debouncing search input to avoid executing search on every keystroke.
|
||||
private final PauseTransition searchPause = new PauseTransition(Duration.millis(100));
|
||||
|
||||
public ModListPageSkin(ModListPage skinnable) {
|
||||
super(skinnable);
|
||||
|
||||
StackPane pane = new StackPane();
|
||||
pane.setPadding(new Insets(10));
|
||||
pane.getStyleClass().addAll("notice-pane");
|
||||
|
||||
ComponentList root = new ComponentList();
|
||||
pane.getChildren().setAll(root);
|
||||
root.getStyleClass().add("no-padding");
|
||||
listView = new JFXListView<>();
|
||||
listView.getStyleClass().add("no-horizontal-scrollbar");
|
||||
|
||||
{
|
||||
toolbarPane = new TransitionPane();
|
||||
|
||||
searchBar = new HBox();
|
||||
toolbarNormal = new HBox();
|
||||
toolbarSelecting = new HBox();
|
||||
|
||||
// Search Bar
|
||||
searchBar.setAlignment(Pos.CENTER);
|
||||
searchBar.setPadding(new Insets(0, 5, 0, 5));
|
||||
searchField = new JFXTextField();
|
||||
searchField.setPromptText(i18n("search"));
|
||||
HBox.setHgrow(searchField, Priority.ALWAYS);
|
||||
searchPause.setOnFinished(e -> search());
|
||||
searchField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (isSearching.get() || !StringUtils.isBlank(newValue)) {
|
||||
searchPause.setRate(1);
|
||||
searchPause.playFromStart();
|
||||
}
|
||||
});
|
||||
|
||||
JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE,
|
||||
() -> {
|
||||
changeToolbar(toolbarNormal);
|
||||
|
||||
searchField.clear();
|
||||
searchPause.stop();
|
||||
|
||||
isSearching.set(false);
|
||||
Bindings.bindContent(listView.getItems(), getSkinnable().getItems());
|
||||
});
|
||||
|
||||
onEscPressed(searchField, closeSearchBar::fire);
|
||||
|
||||
searchBar.getChildren().setAll(searchField, closeSearchBar);
|
||||
|
||||
// Toolbar Normal
|
||||
toolbarNormal.getChildren().setAll(
|
||||
createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh),
|
||||
createToolbarButton2(i18n("mods.add"), SVG.ADD, skinnable::add),
|
||||
createToolbarButton2(i18n("button.reveal_dir"), SVG.FOLDER_OPEN, skinnable::openModFolder),
|
||||
createToolbarButton2(i18n("addon.check_update.button"), SVG.UPDATE, () ->
|
||||
skinnable.checkUpdates(
|
||||
listView.getItems().stream()
|
||||
.map(ModListPage.ModInfoObject::getModInfo)
|
||||
.toList()
|
||||
)
|
||||
),
|
||||
createToolbarButton2(i18n("mods.download"), SVG.DOWNLOAD, skinnable::download),
|
||||
createToolbarButton2(i18n("search"), SVG.SEARCH, () -> changeToolbar(searchBar))
|
||||
);
|
||||
|
||||
// Toolbar Selecting
|
||||
|
||||
// reason for not using selectAll() is that selectAll() first clears all selected then selects all, causing the toolbar to flicker
|
||||
var selectAll = createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () -> listView.getSelectionModel().selectRange(0, listView.getItems().size()));
|
||||
|
||||
ListChangeListener<Object> listener = change -> {
|
||||
selectAll.setDisable(!listView.getItems().isEmpty()
|
||||
&& listView.getSelectionModel().getSelectedItems().size() == listView.getItems().size());
|
||||
};
|
||||
|
||||
listView.getSelectionModel().getSelectedItems().addListener(listener);
|
||||
listView.getItems().addListener(listener);
|
||||
|
||||
toolbarSelecting.getChildren().setAll(
|
||||
createToolbarButton2(i18n("button.remove"), SVG.DELETE_FOREVER, () -> {
|
||||
Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> {
|
||||
skinnable.removeSelected(listView.getSelectionModel().getSelectedItems());
|
||||
}, null);
|
||||
}),
|
||||
createToolbarButton2(i18n("mods.enable"), SVG.CHECK, () ->
|
||||
skinnable.enableSelected(listView.getSelectionModel().getSelectedItems())),
|
||||
createToolbarButton2(i18n("mods.disable"), SVG.CLOSE, () ->
|
||||
skinnable.disableSelected(listView.getSelectionModel().getSelectedItems())),
|
||||
createToolbarButton2(i18n("addon.check_update.button"), SVG.UPDATE, () ->
|
||||
skinnable.checkUpdates(
|
||||
listView.getSelectionModel().getSelectedItems().stream()
|
||||
.map(ModListPage.ModInfoObject::getModInfo)
|
||||
.toList()
|
||||
)
|
||||
),
|
||||
selectAll,
|
||||
createToolbarButton2(i18n("button.cancel"), SVG.CANCEL, () ->
|
||||
listView.getSelectionModel().clearSelection())
|
||||
);
|
||||
|
||||
FXUtils.onChangeAndOperate(listView.getSelectionModel().selectedItemProperty(),
|
||||
selectedItem -> {
|
||||
if (selectedItem == null)
|
||||
changeToolbar(isSearching.get() ? searchBar : toolbarNormal);
|
||||
else
|
||||
changeToolbar(toolbarSelecting);
|
||||
});
|
||||
|
||||
FXUtils.setOverflowHidden(toolbarPane, 8);
|
||||
|
||||
root.getContent().add(toolbarPane);
|
||||
|
||||
// Clear selection when pressing ESC
|
||||
root.addEventHandler(KeyEvent.KEY_PRESSED, e -> {
|
||||
if (e.getCode() == KeyCode.ESCAPE) {
|
||||
if (listView.getSelectionModel().getSelectedItem() != null) {
|
||||
listView.getSelectionModel().clearSelection();
|
||||
e.consume();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
SpinnerPane center = new SpinnerPane();
|
||||
ComponentList.setVgrow(center, Priority.ALWAYS);
|
||||
center.loadingProperty().bind(skinnable.loadingProperty());
|
||||
|
||||
listView.setCellFactory(x -> new ModListPage.ModInfoListCell(listView, skinnable));
|
||||
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
||||
|
||||
StackPane placeholderContainer = new StackPane();
|
||||
placeholderContainer.getStyleClass().add("notice-pane");
|
||||
Label placeholderLabel = new Label(i18n("mods.empty"));
|
||||
placeholderLabel.textProperty().bind(
|
||||
Bindings.createStringBinding(() -> {
|
||||
if (isSearching.get()) {
|
||||
return i18n("search.no_results_found");
|
||||
} else {
|
||||
return i18n("mods.empty");
|
||||
}
|
||||
},
|
||||
isSearching)
|
||||
);
|
||||
placeholderContainer.getChildren().add(placeholderLabel);
|
||||
listView.setPlaceholder(placeholderContainer);
|
||||
|
||||
Bindings.bindContent(listView.getItems(), skinnable.getItems());
|
||||
skinnable.getItems().addListener((ListChangeListener<? super ModListPage.ModInfoObject>) c -> {
|
||||
if (isSearching.get()) {
|
||||
search();
|
||||
}
|
||||
});
|
||||
|
||||
listView.setOnContextMenuRequested(event -> {
|
||||
ModListPage.ModInfoObject selectedItem = listView.getSelectionModel().getSelectedItem();
|
||||
if (selectedItem != null && listView.getSelectionModel().getSelectedItems().size() == 1) {
|
||||
listView.getSelectionModel().clearSelection();
|
||||
Controllers.dialog(new ModListPage.ModInfoDialog(selectedItem));
|
||||
}
|
||||
});
|
||||
|
||||
// ListViewBehavior would consume ESC pressed event, preventing us from handling it
|
||||
// So we ignore it here
|
||||
ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
|
||||
|
||||
center.setContent(listView);
|
||||
root.getContent().add(center);
|
||||
}
|
||||
|
||||
getChildren().setAll(pane);
|
||||
}
|
||||
|
||||
private void changeToolbar(HBox newToolbar) {
|
||||
Node oldToolbar = toolbarPane.getCurrentNode();
|
||||
if (newToolbar != oldToolbar) {
|
||||
toolbarPane.setContent(newToolbar, ContainerAnimations.FADE);
|
||||
if (newToolbar == searchBar) {
|
||||
Platform.runLater(searchField::requestFocus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void search() {
|
||||
isSearching.set(true);
|
||||
|
||||
Bindings.unbindContent(listView.getItems(), getSkinnable().getItems());
|
||||
|
||||
String queryString = searchField.getText();
|
||||
if (StringUtils.isBlank(queryString)) {
|
||||
listView.getItems().setAll(getSkinnable().getItems());
|
||||
} else {
|
||||
listView.getItems().clear();
|
||||
|
||||
Predicate<@Nullable String> predicate;
|
||||
try {
|
||||
predicate = StringUtils.compileQuery(queryString);
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Illegal regular expression", e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Do we need to search in the background thread?
|
||||
for (ModListPage.ModInfoObject item : getSkinnable().getItems()) {
|
||||
LocalModFile modInfo = item.getModInfo();
|
||||
if (predicate.test(modInfo.getFileName())
|
||||
|| predicate.test(modInfo.getName())
|
||||
|| predicate.test(modInfo.getVersion())
|
||||
|| predicate.test(modInfo.getGameVersion())
|
||||
|| predicate.test(modInfo.getId())
|
||||
|| predicate.test(Objects.toString(modInfo.getModLoaderType()))
|
||||
|| predicate.test((item.getModTranslations() != null ? item.getModTranslations().getDisplayName() : null))) {
|
||||
listView.getItems().add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static final class ModInfoObject {
|
||||
private final BooleanProperty active;
|
||||
private final LocalModFile localModFile;
|
||||
private final @Nullable ModTranslations.Mod modTranslations;
|
||||
|
||||
private SoftReference<CompletableFuture<Image>> iconCache;
|
||||
|
||||
ModInfoObject(LocalModFile localModFile) {
|
||||
this.localModFile = localModFile;
|
||||
this.active = localModFile.activeProperty();
|
||||
|
||||
this.modTranslations = ModTranslations.MOD.getMod(localModFile.getId(), localModFile.getName());
|
||||
}
|
||||
|
||||
public LocalModFile getModInfo() {
|
||||
return localModFile;
|
||||
}
|
||||
|
||||
public @Nullable ModTranslations.Mod getModTranslations() {
|
||||
return modTranslations;
|
||||
}
|
||||
|
||||
@FXThread
|
||||
private Image loadIcon() {
|
||||
List<String> iconPaths = new ArrayList<>();
|
||||
|
||||
if (StringUtils.isNotBlank(this.localModFile.getLogoPath())) {
|
||||
iconPaths.add(this.localModFile.getLogoPath());
|
||||
}
|
||||
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(this.localModFile.getFile())) {
|
||||
for (String path : iconPaths) {
|
||||
Path iconPath = fs.getPath(path);
|
||||
if (Files.exists(iconPath)) {
|
||||
Image image = FXUtils.loadImage(iconPath, 80, 80, true, true);
|
||||
if (!image.isError() && image.getWidth() > 0 && image.getHeight() > 0 &&
|
||||
Math.abs(image.getWidth() - image.getHeight()) < 1) {
|
||||
return image;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to load mod icons", e);
|
||||
}
|
||||
|
||||
return GameInstanceIconType.getIconType(this.localModFile.getModLoaderType()).getIcon();
|
||||
}
|
||||
|
||||
public void loadIcon(ImageContainer imageContainer, @Nullable WeakReference<ObjectProperty<ModInfoObject>> current) {
|
||||
SoftReference<CompletableFuture<Image>> iconCache = this.iconCache;
|
||||
CompletableFuture<Image> imageFuture;
|
||||
if (iconCache != null && (imageFuture = iconCache.get()) != null) {
|
||||
Image image = imageFuture.getNow(null);
|
||||
if (image != null) {
|
||||
imageContainer.setImage(image);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
imageFuture = CompletableFuture.supplyAsync(this::loadIcon, Schedulers.io());
|
||||
this.iconCache = new SoftReference<>(imageFuture);
|
||||
}
|
||||
imageContainer.setImage(GameInstanceIconType.getIconType(localModFile.getModLoaderType()).getIcon());
|
||||
imageFuture.thenAcceptAsync(image -> {
|
||||
if (current != null) {
|
||||
ObjectProperty<ModInfoObject> infoObjectProperty = current.get();
|
||||
if (infoObjectProperty == null || infoObjectProperty.get() != this) {
|
||||
// The current ListCell has already switched to another object
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
imageContainer.setImage(image);
|
||||
}, Schedulers.javafx());
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ModInfoDialog extends JFXDialogLayout {
|
||||
|
||||
ModInfoDialog(ModInfoObject modInfo) {
|
||||
HBox titleContainer = new HBox();
|
||||
titleContainer.setSpacing(8);
|
||||
titleContainer.setPadding(new Insets(0, 0, 12, 0));
|
||||
|
||||
DoubleBinding widthBinding = Controllers.getDecorator().contentWidthProperty().multiply(0.7);
|
||||
prefWidthProperty().bind(widthBinding);
|
||||
maxWidthProperty().bind(widthBinding);
|
||||
|
||||
var imageContainer = new ImageContainer(40);
|
||||
titleContainer.setAlignment(Pos.CENTER_LEFT);
|
||||
modInfo.loadIcon(imageContainer, null);
|
||||
|
||||
TwoLineListItem title = new TwoLineListItem();
|
||||
if (modInfo.getModTranslations() != null && I18n.isUseChinese())
|
||||
title.setTitle(modInfo.getModTranslations().getDisplayName());
|
||||
else
|
||||
title.setTitle(modInfo.getModInfo().getName());
|
||||
|
||||
StringJoiner subtitle = new StringJoiner("\n");
|
||||
subtitle.add(i18n("archive.file.name") + ": " + FileUtils.getName(modInfo.getModInfo().getFile()));
|
||||
if (StringUtils.isNotBlank(modInfo.getModInfo().getGameVersion())) {
|
||||
subtitle.add(i18n("mods.game.version") + ": " + modInfo.getModInfo().getGameVersion());
|
||||
}
|
||||
if (StringUtils.isNotBlank(modInfo.getModInfo().getVersion())) {
|
||||
subtitle.add(i18n("archive.version") + ": " + modInfo.getModInfo().getVersion());
|
||||
}
|
||||
if (StringUtils.isNotBlank(modInfo.getModInfo().getAuthors())) {
|
||||
subtitle.add(i18n("archive.author") + ": " + modInfo.getModInfo().getAuthors());
|
||||
}
|
||||
title.setSubtitle(subtitle.toString());
|
||||
|
||||
titleContainer.getChildren().setAll(imageContainer, title);
|
||||
setHeading(titleContainer);
|
||||
|
||||
Label description = new Label(modInfo.getModInfo().getDescription().toString());
|
||||
description.setWrapText(true);
|
||||
FXUtils.copyOnDoubleClick(description);
|
||||
|
||||
ScrollPane descriptionPane = new ScrollPane(description);
|
||||
FXUtils.smoothScrolling(descriptionPane);
|
||||
descriptionPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
descriptionPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
|
||||
descriptionPane.setFitToWidth(true);
|
||||
description.heightProperty().addListener((obs, oldVal, newVal) -> {
|
||||
double maxHeight = Controllers.getDecorator().contentHeightProperty().get() * 0.5;
|
||||
double targetHeight = Math.min(newVal.doubleValue(), maxHeight);
|
||||
descriptionPane.setPrefViewportHeight(targetHeight);
|
||||
});
|
||||
|
||||
setBody(descriptionPane);
|
||||
|
||||
if (StringUtils.isNotBlank(modInfo.getModInfo().getId())) {
|
||||
for (Pair<String, ? extends RemoteAddonRepository> item : Arrays.asList(
|
||||
pair("addon.curseforge", CurseForgeRemoteAddonRepository.MODS),
|
||||
pair("addon.modrinth", ModrinthRemoteAddonRepository.MODS)
|
||||
)) {
|
||||
RemoteAddonRepository repository = item.getValue();
|
||||
JFXHyperlink button = new JFXHyperlink(i18n(item.getKey()));
|
||||
Task.runAsync(() -> {
|
||||
Optional<RemoteAddon.Version> versionOptional = repository.getRemoteVersionByLocalFile(modInfo.getModInfo().getFile());
|
||||
if (versionOptional.isPresent()) {
|
||||
RemoteAddon remoteAddon = repository.getAddonById(DownloadProviders.getDownloadProvider(), versionOptional.get().projectId());
|
||||
FXUtils.runInFX(() -> {
|
||||
for (ModLoaderType modLoaderType : versionOptional.get().loaders()) {
|
||||
String loaderName = switch (modLoaderType) {
|
||||
case FORGE -> i18n("install.installer.forge");
|
||||
case CLEANROOM -> i18n("install.installer.cleanroom");
|
||||
case LEGACY_FABRIC -> i18n("install.installer.legacyfabric");
|
||||
case NEO_FORGE -> i18n("install.installer.neoforge");
|
||||
case FABRIC -> i18n("install.installer.fabric");
|
||||
case LITE_LOADER -> i18n("install.installer.liteloader");
|
||||
case QUILT -> i18n("install.installer.quilt");
|
||||
default -> null;
|
||||
};
|
||||
if (loaderName == null)
|
||||
continue;
|
||||
if (title.getTags()
|
||||
.stream()
|
||||
.noneMatch(it -> it.getText().equals(loaderName))) {
|
||||
title.addTag(loaderName);
|
||||
}
|
||||
}
|
||||
|
||||
button.setExternalLink(remoteAddon.pageUrl());
|
||||
button.setDisable(false);
|
||||
});
|
||||
}
|
||||
}).start();
|
||||
button.setDisable(true);
|
||||
getActions().add(button);
|
||||
}
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(modInfo.getModInfo().getUrl())) {
|
||||
JFXHyperlink officialPageButton = new JFXHyperlink(i18n("mods.url"));
|
||||
officialPageButton.setExternalLink(modInfo.getModInfo().getUrl());
|
||||
getActions().add(officialPageButton);
|
||||
}
|
||||
|
||||
if (modInfo.getModTranslations() == null || StringUtils.isBlank(modInfo.getModTranslations().getMcmod())) {
|
||||
JFXHyperlink searchButton = new JFXHyperlink(i18n("mods.mcmod.search"));
|
||||
searchButton.setExternalLink(NetworkUtils.withQuery("https://search.mcmod.cn/s", mapOf(
|
||||
pair("key", modInfo.getModInfo().getName()),
|
||||
pair("site", "all"),
|
||||
pair("filter", "0")
|
||||
)));
|
||||
getActions().add(searchButton);
|
||||
} else {
|
||||
JFXHyperlink mcmodButton = new JFXHyperlink(i18n("mods.mcmod.page"));
|
||||
mcmodButton.setExternalLink(ModTranslations.MOD.getMcmodUrl(modInfo.getModTranslations()));
|
||||
getActions().add(mcmodButton);
|
||||
}
|
||||
|
||||
JFXButton okButton = new JFXButton();
|
||||
okButton.getStyleClass().add("dialog-accept");
|
||||
okButton.setText(i18n("button.ok"));
|
||||
okButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
|
||||
getActions().add(okButton);
|
||||
|
||||
onEscPressed(this, okButton::fire);
|
||||
}
|
||||
}
|
||||
|
||||
private static final Lazy<PopupMenu> menu = new Lazy<>(PopupMenu::new);
|
||||
private static final Lazy<JFXPopup> popup = new Lazy<>(() -> new JFXPopup(menu.get()));
|
||||
|
||||
private static final class ModInfoListCell extends MDListCell<ModInfoObject> {
|
||||
private static final PseudoClass WARNING = PseudoClass.getPseudoClass("warning");
|
||||
|
||||
private final ModListPage page;
|
||||
|
||||
private final JFXCheckBox checkBox = new JFXCheckBox();
|
||||
private final ImageContainer imageContainer = new ImageContainer(32);
|
||||
private final TwoLineListItem content = new TwoLineListItem();
|
||||
private final JFXButton restoreButton = FXUtils.newToggleButton4(SVG.RESTORE);
|
||||
private final JFXButton infoButton = FXUtils.newToggleButton4(SVG.INFO);
|
||||
private final JFXButton revealButton = FXUtils.newToggleButton4(SVG.FOLDER);
|
||||
|
||||
private BooleanProperty booleanProperty;
|
||||
|
||||
private Tooltip warningTooltip;
|
||||
|
||||
ModInfoListCell(JFXListView<ModInfoObject> listView, ModListPage page) {
|
||||
super(listView);
|
||||
this.page = page;
|
||||
|
||||
this.getStyleClass().add("mod-info-list-cell");
|
||||
|
||||
HBox container = new HBox(8);
|
||||
container.setPickOnBounds(false);
|
||||
container.setAlignment(Pos.CENTER_LEFT);
|
||||
HBox.setHgrow(content, Priority.ALWAYS);
|
||||
content.setMouseTransparent(true);
|
||||
setSelectable();
|
||||
|
||||
imageContainer.setImage(GameInstanceIconType.COMMAND.getIcon());
|
||||
|
||||
FXUtils.installFastTooltip(restoreButton, i18n("mods.restore"));
|
||||
|
||||
container.getChildren().setAll(checkBox, imageContainer, content, restoreButton, revealButton, infoButton);
|
||||
|
||||
StackPane.setMargin(container, new Insets(8));
|
||||
getContainer().getChildren().setAll(container);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateControl(ModInfoObject dataItem, boolean empty) {
|
||||
pseudoClassStateChanged(WARNING, false);
|
||||
if (warningTooltip != null) {
|
||||
Tooltip.uninstall(this, warningTooltip);
|
||||
warningTooltip = null;
|
||||
}
|
||||
|
||||
if (empty) return;
|
||||
|
||||
List<String> warning = new ArrayList<>();
|
||||
|
||||
content.getTags().clear();
|
||||
|
||||
LocalModFile modInfo = dataItem.getModInfo();
|
||||
ModTranslations.Mod modTranslations = dataItem.getModTranslations();
|
||||
|
||||
ModLoaderType modLoaderType = modInfo.getModLoaderType();
|
||||
|
||||
dataItem.loadIcon(imageContainer, new WeakReference<>(this.itemProperty()));
|
||||
|
||||
String displayName = modInfo.getName();
|
||||
if (modTranslations != null && I18n.isUseChinese()) {
|
||||
String chineseName = modTranslations.getName();
|
||||
if (StringUtils.containsChinese(chineseName)) {
|
||||
if (StringUtils.containsEmoji(chineseName)) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
chineseName.codePoints().forEach(ch -> {
|
||||
if (ch < 0x1F300 || ch > 0x1FAFF)
|
||||
builder.appendCodePoint(ch);
|
||||
});
|
||||
|
||||
chineseName = builder.toString().trim();
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(chineseName) && !displayName.equalsIgnoreCase(chineseName)) {
|
||||
displayName = displayName + " (" + chineseName + ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
content.setTitle(displayName);
|
||||
|
||||
StringJoiner joiner = new StringJoiner(" | ");
|
||||
if (modLoaderType != ModLoaderType.UNKNOWN && StringUtils.isNotBlank(modInfo.getId()))
|
||||
joiner.add(modInfo.getId());
|
||||
|
||||
joiner.add(FileUtils.getName(modInfo.getFile()));
|
||||
|
||||
content.setSubtitle(joiner.toString());
|
||||
|
||||
if (modLoaderType == ModLoaderType.UNKNOWN) {
|
||||
content.addTagWarning(i18n("mods.unknown"));
|
||||
} else if (!page.supportedLoaders.contains(modLoaderType)) {
|
||||
warning.add(i18n("mods.warning.loader_mismatch"));
|
||||
switch (dataItem.getModInfo().getModLoaderType()) {
|
||||
case FORGE -> content.addTagWarning(i18n("install.installer.forge"));
|
||||
case LEGACY_FABRIC -> content.addTagWarning(i18n("install.installer.legacyfabric"));
|
||||
case CLEANROOM -> content.addTagWarning(i18n("install.installer.cleanroom"));
|
||||
case NEO_FORGE -> content.addTagWarning(i18n("install.installer.neoforge"));
|
||||
case FABRIC -> content.addTagWarning(i18n("install.installer.fabric"));
|
||||
case LITE_LOADER -> content.addTagWarning(i18n("install.installer.liteloader"));
|
||||
case QUILT -> content.addTagWarning(i18n("install.installer.quilt"));
|
||||
}
|
||||
}
|
||||
|
||||
String modVersion = modInfo.getVersion();
|
||||
if (StringUtils.isNotBlank(modVersion) && !"${version}".equals(modVersion)) {
|
||||
content.addTag(modVersion);
|
||||
}
|
||||
|
||||
if (booleanProperty != null) {
|
||||
checkBox.selectedProperty().unbindBidirectional(booleanProperty);
|
||||
}
|
||||
checkBox.selectedProperty().bindBidirectional(booleanProperty = dataItem.active);
|
||||
restoreButton.setVisible(!modInfo.getMod().getOldFiles().isEmpty());
|
||||
restoreButton.setOnAction(e -> {
|
||||
menu.get().getContent().setAll(modInfo.getMod().getOldFiles().stream()
|
||||
.map(localModFile -> new IconedMenuItem(null, localModFile.getVersion(),
|
||||
() -> page.rollback(modInfo, localModFile),
|
||||
popup.get()))
|
||||
.toList()
|
||||
);
|
||||
|
||||
popup.get().show(restoreButton, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, 0, restoreButton.getHeight());
|
||||
});
|
||||
revealButton.setOnAction(e -> FXUtils.showFileInExplorer(modInfo.getFile()));
|
||||
infoButton.setOnAction(e -> Controllers.dialog(new ModInfoDialog(dataItem)));
|
||||
|
||||
if (!warning.isEmpty()) {
|
||||
pseudoClassStateChanged(WARNING, true);
|
||||
|
||||
//noinspection ConstantValue
|
||||
this.warningTooltip = warning.size() == 1
|
||||
? new Tooltip(warning.get(0))
|
||||
: new Tooltip(String.join("\n", warning));
|
||||
FXUtils.installFastTooltip(this, warningTooltip);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,661 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.instances;
|
||||
|
||||
import com.jfoenix.controls.*;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.DoubleBinding;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.addon.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.addon.mod.ModLoaderType;
|
||||
import org.jackhuang.hmcl.addon.repository.CurseForgeRemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.addon.repository.ModrinthRemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.setting.GameInstanceIconType;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.util.FXThread;
|
||||
import org.jackhuang.hmcl.util.Lazy;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.lang.ref.SoftReference;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.ignoreEvent;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2;
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
@NotNullByDefault
|
||||
final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
|
||||
private final TransitionPane toolbarPane;
|
||||
private final HBox searchBar;
|
||||
private final HBox toolbarNormal;
|
||||
private final HBox toolbarSelecting;
|
||||
|
||||
private final JFXListView<ModInfoObject> listView;
|
||||
|
||||
/// Whether the search mechanism is currently active.
|
||||
private final BooleanProperty isSearching = new SimpleBooleanProperty(false);
|
||||
|
||||
private final JFXTextField searchField;
|
||||
|
||||
/// Timer for debouncing search input to avoid executing search on every keystroke.
|
||||
private final PauseTransition searchPause = new PauseTransition(Duration.millis(100));
|
||||
|
||||
ModListPageSkin(ModListPage skinnable) {
|
||||
super(skinnable);
|
||||
|
||||
StackPane pane = new StackPane();
|
||||
pane.setPadding(new Insets(10));
|
||||
pane.getStyleClass().addAll("notice-pane");
|
||||
|
||||
ComponentList root = new ComponentList();
|
||||
pane.getChildren().setAll(root);
|
||||
root.getStyleClass().add("no-padding");
|
||||
listView = new JFXListView<>();
|
||||
listView.getStyleClass().add("no-horizontal-scrollbar");
|
||||
|
||||
{
|
||||
toolbarPane = new TransitionPane();
|
||||
|
||||
searchBar = new HBox();
|
||||
toolbarNormal = new HBox();
|
||||
toolbarSelecting = new HBox();
|
||||
|
||||
// Search Bar
|
||||
searchBar.setAlignment(Pos.CENTER);
|
||||
searchBar.setPadding(new Insets(0, 5, 0, 5));
|
||||
searchField = new JFXTextField();
|
||||
searchField.setPromptText(i18n("search"));
|
||||
HBox.setHgrow(searchField, Priority.ALWAYS);
|
||||
searchPause.setOnFinished(e -> search());
|
||||
searchField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (isSearching.get() || !StringUtils.isBlank(newValue)) {
|
||||
searchPause.setRate(1);
|
||||
searchPause.playFromStart();
|
||||
}
|
||||
});
|
||||
|
||||
JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE,
|
||||
() -> {
|
||||
changeToolbar(toolbarNormal);
|
||||
|
||||
searchField.clear();
|
||||
searchPause.stop();
|
||||
|
||||
isSearching.set(false);
|
||||
Bindings.bindContent(listView.getItems(), getSkinnable().getItems());
|
||||
});
|
||||
|
||||
onEscPressed(searchField, closeSearchBar::fire);
|
||||
|
||||
searchBar.getChildren().setAll(searchField, closeSearchBar);
|
||||
|
||||
// Toolbar Normal
|
||||
toolbarNormal.getChildren().setAll(
|
||||
createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh),
|
||||
createToolbarButton2(i18n("mods.add"), SVG.ADD, skinnable::add),
|
||||
createToolbarButton2(i18n("button.reveal_dir"), SVG.FOLDER_OPEN, skinnable::openModFolder),
|
||||
createToolbarButton2(i18n("addon.check_update.button"), SVG.UPDATE, () ->
|
||||
skinnable.checkUpdates(
|
||||
listView.getItems().stream()
|
||||
.map(ModInfoObject::getModInfo)
|
||||
.toList()
|
||||
)
|
||||
),
|
||||
createToolbarButton2(i18n("mods.download"), SVG.DOWNLOAD, skinnable::download),
|
||||
createToolbarButton2(i18n("search"), SVG.SEARCH, () -> changeToolbar(searchBar))
|
||||
);
|
||||
|
||||
// Toolbar Selecting
|
||||
|
||||
// reason for not using selectAll() is that selectAll() first clears all selected then selects all, causing the toolbar to flicker
|
||||
var selectAll = createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () -> listView.getSelectionModel().selectRange(0, listView.getItems().size()));
|
||||
|
||||
ListChangeListener<Object> listener = change -> {
|
||||
selectAll.setDisable(!listView.getItems().isEmpty()
|
||||
&& listView.getSelectionModel().getSelectedItems().size() == listView.getItems().size());
|
||||
};
|
||||
|
||||
listView.getSelectionModel().getSelectedItems().addListener(listener);
|
||||
listView.getItems().addListener(listener);
|
||||
|
||||
toolbarSelecting.getChildren().setAll(
|
||||
createToolbarButton2(i18n("button.remove"), SVG.DELETE_FOREVER, () -> {
|
||||
Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> {
|
||||
skinnable.removeSelected(listView.getSelectionModel().getSelectedItems());
|
||||
}, null);
|
||||
}),
|
||||
createToolbarButton2(i18n("mods.enable"), SVG.CHECK, () ->
|
||||
skinnable.enableSelected(listView.getSelectionModel().getSelectedItems())),
|
||||
createToolbarButton2(i18n("mods.disable"), SVG.CLOSE, () ->
|
||||
skinnable.disableSelected(listView.getSelectionModel().getSelectedItems())),
|
||||
createToolbarButton2(i18n("addon.check_update.button"), SVG.UPDATE, () ->
|
||||
skinnable.checkUpdates(
|
||||
listView.getSelectionModel().getSelectedItems().stream()
|
||||
.map(ModInfoObject::getModInfo)
|
||||
.toList()
|
||||
)
|
||||
),
|
||||
selectAll,
|
||||
createToolbarButton2(i18n("button.cancel"), SVG.CANCEL, () ->
|
||||
listView.getSelectionModel().clearSelection())
|
||||
);
|
||||
|
||||
FXUtils.onChangeAndOperate(listView.getSelectionModel().selectedItemProperty(),
|
||||
selectedItem -> {
|
||||
if (selectedItem == null)
|
||||
changeToolbar(isSearching.get() ? searchBar : toolbarNormal);
|
||||
else
|
||||
changeToolbar(toolbarSelecting);
|
||||
});
|
||||
|
||||
FXUtils.setOverflowHidden(toolbarPane, 8);
|
||||
|
||||
root.getContent().add(toolbarPane);
|
||||
|
||||
// Clear selection when pressing ESC
|
||||
root.addEventHandler(KeyEvent.KEY_PRESSED, e -> {
|
||||
if (e.getCode() == KeyCode.ESCAPE) {
|
||||
if (listView.getSelectionModel().getSelectedItem() != null) {
|
||||
listView.getSelectionModel().clearSelection();
|
||||
e.consume();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
SpinnerPane center = new SpinnerPane();
|
||||
ComponentList.setVgrow(center, Priority.ALWAYS);
|
||||
center.loadingProperty().bind(skinnable.loadingProperty());
|
||||
|
||||
listView.setCellFactory(x -> new ModInfoListCell(listView));
|
||||
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
||||
|
||||
StackPane placeholderContainer = new StackPane();
|
||||
placeholderContainer.getStyleClass().add("notice-pane");
|
||||
Label placeholderLabel = new Label(i18n("mods.empty"));
|
||||
placeholderLabel.textProperty().bind(
|
||||
Bindings.createStringBinding(() -> {
|
||||
if (isSearching.get()) {
|
||||
return i18n("search.no_results_found");
|
||||
} else {
|
||||
return i18n("mods.empty");
|
||||
}
|
||||
},
|
||||
isSearching)
|
||||
);
|
||||
placeholderContainer.getChildren().add(placeholderLabel);
|
||||
listView.setPlaceholder(placeholderContainer);
|
||||
|
||||
Bindings.bindContent(listView.getItems(), skinnable.getItems());
|
||||
skinnable.getItems().addListener((ListChangeListener<? super ModInfoObject>) c -> {
|
||||
if (isSearching.get()) {
|
||||
search();
|
||||
}
|
||||
});
|
||||
|
||||
listView.setOnContextMenuRequested(event -> {
|
||||
ModInfoObject selectedItem = listView.getSelectionModel().getSelectedItem();
|
||||
if (selectedItem != null && listView.getSelectionModel().getSelectedItems().size() == 1) {
|
||||
listView.getSelectionModel().clearSelection();
|
||||
Controllers.dialog(new ModInfoDialog(selectedItem));
|
||||
}
|
||||
});
|
||||
|
||||
// ListViewBehavior would consume ESC pressed event, preventing us from handling it
|
||||
// So we ignore it here
|
||||
ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
|
||||
|
||||
center.setContent(listView);
|
||||
root.getContent().add(center);
|
||||
}
|
||||
|
||||
getChildren().setAll(pane);
|
||||
}
|
||||
|
||||
private void changeToolbar(HBox newToolbar) {
|
||||
Node oldToolbar = toolbarPane.getCurrentNode();
|
||||
if (newToolbar != oldToolbar) {
|
||||
toolbarPane.setContent(newToolbar, ContainerAnimations.FADE);
|
||||
if (newToolbar == searchBar) {
|
||||
Platform.runLater(searchField::requestFocus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void search() {
|
||||
isSearching.set(true);
|
||||
|
||||
Bindings.unbindContent(listView.getItems(), getSkinnable().getItems());
|
||||
|
||||
String queryString = searchField.getText();
|
||||
if (StringUtils.isBlank(queryString)) {
|
||||
listView.getItems().setAll(getSkinnable().getItems());
|
||||
} else {
|
||||
listView.getItems().clear();
|
||||
|
||||
Predicate<@Nullable String> predicate;
|
||||
try {
|
||||
predicate = StringUtils.compileQuery(queryString);
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Illegal regular expression", e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Do we need to search in the background thread?
|
||||
for (ModInfoObject item : getSkinnable().getItems()) {
|
||||
LocalModFile modInfo = item.getModInfo();
|
||||
if (predicate.test(modInfo.getFileName())
|
||||
|| predicate.test(modInfo.getName())
|
||||
|| predicate.test(modInfo.getVersion())
|
||||
|| predicate.test(modInfo.getGameVersion())
|
||||
|| predicate.test(modInfo.getId())
|
||||
|| predicate.test(Objects.toString(modInfo.getModLoaderType()))
|
||||
|| predicate.test((item.getModTranslations() != null ? item.getModTranslations().getDisplayName() : null))) {
|
||||
listView.getItems().add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static final class ModInfoObject {
|
||||
private final BooleanProperty active;
|
||||
private final LocalModFile localModFile;
|
||||
private final @Nullable ModTranslations.Mod modTranslations;
|
||||
|
||||
private SoftReference<CompletableFuture<Image>> iconCache;
|
||||
|
||||
ModInfoObject(LocalModFile localModFile) {
|
||||
this.localModFile = localModFile;
|
||||
this.active = localModFile.activeProperty();
|
||||
|
||||
this.modTranslations = ModTranslations.MOD.getMod(localModFile.getId(), localModFile.getName());
|
||||
}
|
||||
|
||||
LocalModFile getModInfo() {
|
||||
return localModFile;
|
||||
}
|
||||
|
||||
public @Nullable ModTranslations.Mod getModTranslations() {
|
||||
return modTranslations;
|
||||
}
|
||||
|
||||
@FXThread
|
||||
private Image loadIcon() {
|
||||
List<String> iconPaths = new ArrayList<>();
|
||||
|
||||
if (StringUtils.isNotBlank(this.localModFile.getLogoPath())) {
|
||||
iconPaths.add(this.localModFile.getLogoPath());
|
||||
}
|
||||
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(this.localModFile.getFile())) {
|
||||
for (String path : iconPaths) {
|
||||
Path iconPath = fs.getPath(path);
|
||||
if (Files.exists(iconPath)) {
|
||||
Image image = FXUtils.loadImage(iconPath, 80, 80, true, true);
|
||||
if (!image.isError() && image.getWidth() > 0 && image.getHeight() > 0 &&
|
||||
Math.abs(image.getWidth() - image.getHeight()) < 1) {
|
||||
return image;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to load mod icons", e);
|
||||
}
|
||||
|
||||
return GameInstanceIconType.getIconType(this.localModFile.getModLoaderType()).getIcon();
|
||||
}
|
||||
|
||||
public void loadIcon(ImageContainer imageContainer, @Nullable WeakReference<ObjectProperty<ModInfoObject>> current) {
|
||||
SoftReference<CompletableFuture<Image>> iconCache = this.iconCache;
|
||||
CompletableFuture<Image> imageFuture;
|
||||
if (iconCache != null && (imageFuture = iconCache.get()) != null) {
|
||||
Image image = imageFuture.getNow(null);
|
||||
if (image != null) {
|
||||
imageContainer.setImage(image);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
imageFuture = CompletableFuture.supplyAsync(this::loadIcon, Schedulers.io());
|
||||
this.iconCache = new SoftReference<>(imageFuture);
|
||||
}
|
||||
imageContainer.setImage(GameInstanceIconType.getIconType(localModFile.getModLoaderType()).getIcon());
|
||||
imageFuture.thenAcceptAsync(image -> {
|
||||
if (current != null) {
|
||||
ObjectProperty<ModInfoObject> infoObjectProperty = current.get();
|
||||
if (infoObjectProperty == null || infoObjectProperty.get() != this) {
|
||||
// The current ListCell has already switched to another object
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
imageContainer.setImage(image);
|
||||
}, Schedulers.javafx());
|
||||
}
|
||||
}
|
||||
|
||||
final class ModInfoDialog extends JFXDialogLayout {
|
||||
|
||||
ModInfoDialog(ModInfoObject modInfo) {
|
||||
HBox titleContainer = new HBox();
|
||||
titleContainer.setSpacing(8);
|
||||
titleContainer.setPadding(new Insets(0, 0, 12, 0));
|
||||
|
||||
DoubleBinding widthBinding = Controllers.getDecorator().contentWidthProperty().multiply(0.7);
|
||||
prefWidthProperty().bind(widthBinding);
|
||||
maxWidthProperty().bind(widthBinding);
|
||||
|
||||
var imageContainer = new ImageContainer(40);
|
||||
titleContainer.setAlignment(Pos.CENTER_LEFT);
|
||||
modInfo.loadIcon(imageContainer, null);
|
||||
|
||||
TwoLineListItem title = new TwoLineListItem();
|
||||
if (modInfo.getModTranslations() != null && I18n.isUseChinese())
|
||||
title.setTitle(modInfo.getModTranslations().getDisplayName());
|
||||
else
|
||||
title.setTitle(modInfo.getModInfo().getName());
|
||||
|
||||
StringJoiner subtitle = new StringJoiner("\n");
|
||||
subtitle.add(i18n("archive.file.name") + ": " + FileUtils.getName(modInfo.getModInfo().getFile()));
|
||||
if (StringUtils.isNotBlank(modInfo.getModInfo().getGameVersion())) {
|
||||
subtitle.add(i18n("mods.game.version") + ": " + modInfo.getModInfo().getGameVersion());
|
||||
}
|
||||
if (StringUtils.isNotBlank(modInfo.getModInfo().getVersion())) {
|
||||
subtitle.add(i18n("archive.version") + ": " + modInfo.getModInfo().getVersion());
|
||||
}
|
||||
if (StringUtils.isNotBlank(modInfo.getModInfo().getAuthors())) {
|
||||
subtitle.add(i18n("archive.author") + ": " + modInfo.getModInfo().getAuthors());
|
||||
}
|
||||
title.setSubtitle(subtitle.toString());
|
||||
|
||||
titleContainer.getChildren().setAll(imageContainer, title);
|
||||
setHeading(titleContainer);
|
||||
|
||||
Label description = new Label(modInfo.getModInfo().getDescription().toString());
|
||||
description.setWrapText(true);
|
||||
FXUtils.copyOnDoubleClick(description);
|
||||
|
||||
ScrollPane descriptionPane = new ScrollPane(description);
|
||||
FXUtils.smoothScrolling(descriptionPane);
|
||||
descriptionPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
descriptionPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
|
||||
descriptionPane.setFitToWidth(true);
|
||||
description.heightProperty().addListener((obs, oldVal, newVal) -> {
|
||||
double maxHeight = Controllers.getDecorator().contentHeightProperty().get() * 0.5;
|
||||
double targetHeight = Math.min(newVal.doubleValue(), maxHeight);
|
||||
descriptionPane.setPrefViewportHeight(targetHeight);
|
||||
});
|
||||
|
||||
setBody(descriptionPane);
|
||||
|
||||
if (StringUtils.isNotBlank(modInfo.getModInfo().getId())) {
|
||||
for (Pair<String, ? extends RemoteAddonRepository> item : Arrays.asList(
|
||||
pair("addon.curseforge", CurseForgeRemoteAddonRepository.MODS),
|
||||
pair("addon.modrinth", ModrinthRemoteAddonRepository.MODS)
|
||||
)) {
|
||||
RemoteAddonRepository repository = item.getValue();
|
||||
JFXHyperlink button = new JFXHyperlink(i18n(item.getKey()));
|
||||
Task.runAsync(() -> {
|
||||
Optional<RemoteAddon.Version> versionOptional = repository.getRemoteVersionByLocalFile(modInfo.getModInfo().getFile());
|
||||
if (versionOptional.isPresent()) {
|
||||
RemoteAddon remoteAddon = repository.getAddonById(DownloadProviders.getDownloadProvider(), versionOptional.get().projectId());
|
||||
FXUtils.runInFX(() -> {
|
||||
for (ModLoaderType modLoaderType : versionOptional.get().loaders()) {
|
||||
String loaderName = switch (modLoaderType) {
|
||||
case FORGE -> i18n("install.installer.forge");
|
||||
case CLEANROOM -> i18n("install.installer.cleanroom");
|
||||
case LEGACY_FABRIC -> i18n("install.installer.legacyfabric");
|
||||
case NEO_FORGE -> i18n("install.installer.neoforge");
|
||||
case FABRIC -> i18n("install.installer.fabric");
|
||||
case LITE_LOADER -> i18n("install.installer.liteloader");
|
||||
case QUILT -> i18n("install.installer.quilt");
|
||||
default -> null;
|
||||
};
|
||||
if (loaderName == null)
|
||||
continue;
|
||||
if (title.getTags()
|
||||
.stream()
|
||||
.noneMatch(it -> it.getText().equals(loaderName))) {
|
||||
title.addTag(loaderName);
|
||||
}
|
||||
}
|
||||
|
||||
button.setExternalLink(remoteAddon.pageUrl());
|
||||
button.setDisable(false);
|
||||
});
|
||||
}
|
||||
}).start();
|
||||
button.setDisable(true);
|
||||
getActions().add(button);
|
||||
}
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(modInfo.getModInfo().getUrl())) {
|
||||
JFXHyperlink officialPageButton = new JFXHyperlink(i18n("mods.url"));
|
||||
officialPageButton.setExternalLink(modInfo.getModInfo().getUrl());
|
||||
getActions().add(officialPageButton);
|
||||
}
|
||||
|
||||
if (modInfo.getModTranslations() == null || StringUtils.isBlank(modInfo.getModTranslations().getMcmod())) {
|
||||
JFXHyperlink searchButton = new JFXHyperlink(i18n("mods.mcmod.search"));
|
||||
searchButton.setExternalLink(NetworkUtils.withQuery("https://search.mcmod.cn/s", mapOf(
|
||||
pair("key", modInfo.getModInfo().getName()),
|
||||
pair("site", "all"),
|
||||
pair("filter", "0")
|
||||
)));
|
||||
getActions().add(searchButton);
|
||||
} else {
|
||||
JFXHyperlink mcmodButton = new JFXHyperlink(i18n("mods.mcmod.page"));
|
||||
mcmodButton.setExternalLink(ModTranslations.MOD.getMcmodUrl(modInfo.getModTranslations()));
|
||||
getActions().add(mcmodButton);
|
||||
}
|
||||
|
||||
JFXButton okButton = new JFXButton();
|
||||
okButton.getStyleClass().add("dialog-accept");
|
||||
okButton.setText(i18n("button.ok"));
|
||||
okButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
|
||||
getActions().add(okButton);
|
||||
|
||||
onEscPressed(this, okButton::fire);
|
||||
}
|
||||
}
|
||||
|
||||
private static final Lazy<PopupMenu> menu = new Lazy<>(PopupMenu::new);
|
||||
private static final Lazy<JFXPopup> popup = new Lazy<>(() -> new JFXPopup(menu.get()));
|
||||
|
||||
final class ModInfoListCell extends MDListCell<ModInfoObject> {
|
||||
private static final PseudoClass WARNING = PseudoClass.getPseudoClass("warning");
|
||||
|
||||
JFXCheckBox checkBox = new JFXCheckBox();
|
||||
ImageContainer imageContainer = new ImageContainer(32);
|
||||
TwoLineListItem content = new TwoLineListItem();
|
||||
JFXButton restoreButton = FXUtils.newToggleButton4(SVG.RESTORE);
|
||||
JFXButton infoButton = FXUtils.newToggleButton4(SVG.INFO);
|
||||
JFXButton revealButton = FXUtils.newToggleButton4(SVG.FOLDER);
|
||||
BooleanProperty booleanProperty;
|
||||
|
||||
Tooltip warningTooltip;
|
||||
|
||||
ModInfoListCell(JFXListView<ModInfoObject> listView) {
|
||||
super(listView);
|
||||
|
||||
this.getStyleClass().add("mod-info-list-cell");
|
||||
|
||||
HBox container = new HBox(8);
|
||||
container.setPickOnBounds(false);
|
||||
container.setAlignment(Pos.CENTER_LEFT);
|
||||
HBox.setHgrow(content, Priority.ALWAYS);
|
||||
content.setMouseTransparent(true);
|
||||
setSelectable();
|
||||
|
||||
imageContainer.setImage(GameInstanceIconType.COMMAND.getIcon());
|
||||
|
||||
FXUtils.installFastTooltip(restoreButton, i18n("mods.restore"));
|
||||
|
||||
container.getChildren().setAll(checkBox, imageContainer, content, restoreButton, revealButton, infoButton);
|
||||
|
||||
StackPane.setMargin(container, new Insets(8));
|
||||
getContainer().getChildren().setAll(container);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateControl(ModInfoObject dataItem, boolean empty) {
|
||||
pseudoClassStateChanged(WARNING, false);
|
||||
if (warningTooltip != null) {
|
||||
Tooltip.uninstall(this, warningTooltip);
|
||||
warningTooltip = null;
|
||||
}
|
||||
|
||||
if (empty) return;
|
||||
|
||||
List<String> warning = new ArrayList<>();
|
||||
|
||||
content.getTags().clear();
|
||||
|
||||
LocalModFile modInfo = dataItem.getModInfo();
|
||||
ModTranslations.Mod modTranslations = dataItem.getModTranslations();
|
||||
|
||||
ModLoaderType modLoaderType = modInfo.getModLoaderType();
|
||||
|
||||
dataItem.loadIcon(imageContainer, new WeakReference<>(this.itemProperty()));
|
||||
|
||||
String displayName = modInfo.getName();
|
||||
if (modTranslations != null && I18n.isUseChinese()) {
|
||||
String chineseName = modTranslations.getName();
|
||||
if (StringUtils.containsChinese(chineseName)) {
|
||||
if (StringUtils.containsEmoji(chineseName)) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
chineseName.codePoints().forEach(ch -> {
|
||||
if (ch < 0x1F300 || ch > 0x1FAFF)
|
||||
builder.appendCodePoint(ch);
|
||||
});
|
||||
|
||||
chineseName = builder.toString().trim();
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(chineseName) && !displayName.equalsIgnoreCase(chineseName)) {
|
||||
displayName = displayName + " (" + chineseName + ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
content.setTitle(displayName);
|
||||
|
||||
StringJoiner joiner = new StringJoiner(" | ");
|
||||
if (modLoaderType != ModLoaderType.UNKNOWN && StringUtils.isNotBlank(modInfo.getId()))
|
||||
joiner.add(modInfo.getId());
|
||||
|
||||
joiner.add(FileUtils.getName(modInfo.getFile()));
|
||||
|
||||
content.setSubtitle(joiner.toString());
|
||||
|
||||
if (modLoaderType == ModLoaderType.UNKNOWN) {
|
||||
content.addTagWarning(i18n("mods.unknown"));
|
||||
} else if (!ModListPageSkin.this.getSkinnable().supportedLoaders.contains(modLoaderType)) {
|
||||
warning.add(i18n("mods.warning.loader_mismatch"));
|
||||
switch (dataItem.getModInfo().getModLoaderType()) {
|
||||
case FORGE -> content.addTagWarning(i18n("install.installer.forge"));
|
||||
case LEGACY_FABRIC -> content.addTagWarning(i18n("install.installer.legacyfabric"));
|
||||
case CLEANROOM -> content.addTagWarning(i18n("install.installer.cleanroom"));
|
||||
case NEO_FORGE -> content.addTagWarning(i18n("install.installer.neoforge"));
|
||||
case FABRIC -> content.addTagWarning(i18n("install.installer.fabric"));
|
||||
case LITE_LOADER -> content.addTagWarning(i18n("install.installer.liteloader"));
|
||||
case QUILT -> content.addTagWarning(i18n("install.installer.quilt"));
|
||||
}
|
||||
}
|
||||
|
||||
String modVersion = modInfo.getVersion();
|
||||
if (StringUtils.isNotBlank(modVersion) && !"${version}".equals(modVersion)) {
|
||||
content.addTag(modVersion);
|
||||
}
|
||||
|
||||
if (booleanProperty != null) {
|
||||
checkBox.selectedProperty().unbindBidirectional(booleanProperty);
|
||||
}
|
||||
checkBox.selectedProperty().bindBidirectional(booleanProperty = dataItem.active);
|
||||
restoreButton.setVisible(!modInfo.getMod().getOldFiles().isEmpty());
|
||||
restoreButton.setOnAction(e -> {
|
||||
menu.get().getContent().setAll(modInfo.getMod().getOldFiles().stream()
|
||||
.map(localModFile -> new IconedMenuItem(null, localModFile.getVersion(),
|
||||
() -> getSkinnable().rollback(modInfo, localModFile),
|
||||
popup.get()))
|
||||
.toList()
|
||||
);
|
||||
|
||||
popup.get().show(restoreButton, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, 0, restoreButton.getHeight());
|
||||
});
|
||||
revealButton.setOnAction(e -> FXUtils.showFileInExplorer(modInfo.getFile()));
|
||||
infoButton.setOnAction(e -> Controllers.dialog(new ModInfoDialog(dataItem)));
|
||||
|
||||
if (!warning.isEmpty()) {
|
||||
pseudoClassStateChanged(WARNING, true);
|
||||
|
||||
//noinspection ConstantValue
|
||||
this.warningTooltip = warning.size() == 1
|
||||
? new Tooltip(warning.get(0))
|
||||
: new Tooltip(String.join("\n", warning));
|
||||
FXUtils.installFastTooltip(this, warningTooltip);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -43,8 +44,7 @@ import org.jackhuang.hmcl.addon.repository.CurseForgeRemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.addon.repository.ModrinthRemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.addon.resourcepack.ResourcePackFile;
|
||||
import org.jackhuang.hmcl.addon.resourcepack.ResourcePackManager;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.HMCLGameInstance;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.setting.SettingsManager;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
@@ -53,18 +53,21 @@ import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.ListPageBase;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
@@ -76,7 +79,7 @@ import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class ResourcePackListPage extends ListPageBase<ResourcePackListPage.ResourcePackInfoObject> implements GameInstancePage.GameInstanceLoadable {
|
||||
public final class ResourcePackListPage extends ListPageBase<ResourcePackListPage.ResourcePackInfoObject> {
|
||||
|
||||
private static final String TIP_KEY = "resourcePackWarning";
|
||||
private static @Nullable String getWarning(ResourcePackFile.Compatibility compatibility) {
|
||||
@@ -90,16 +93,26 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
|
||||
};
|
||||
}
|
||||
|
||||
private HMCLGameRepository repository;
|
||||
private GameInstanceID instanceId;
|
||||
private final WeakListenerHolder listenerHolder = new WeakListenerHolder();
|
||||
private @Nullable HMCLGameInstance gameInstance;
|
||||
|
||||
private Path resourcePackDirectory;
|
||||
private ResourcePackManager resourcePackManager;
|
||||
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
|
||||
public ResourcePackListPage() {
|
||||
/// Creates a resource-pack list that reloads when `instanceContext` changes.
|
||||
///
|
||||
/// @param instanceContext the parent page's instance property
|
||||
public ResourcePackListPage(ObservableValue<? extends HMCLGameInstance.Optional> instanceContext) {
|
||||
Objects.requireNonNull(instanceContext, "instanceContext");
|
||||
FXUtils.applyDragListener(this, ResourcePackFile::isFileResourcePack, this::addFiles);
|
||||
|
||||
listenerHolder.add(FXUtils.onWeakChangeAndOperate(instanceContext, current -> {
|
||||
if (current != null) {
|
||||
loadInstance(current);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -107,11 +120,16 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
|
||||
return new ResourcePackListPageSkin(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadInstance(HMCLGameRepository repository, @Nullable GameInstanceID instanceId) {
|
||||
this.repository = repository;
|
||||
this.instanceId = instanceId;
|
||||
this.resourcePackManager = new ResourcePackManager(repository, instanceId);
|
||||
public void loadInstance(HMCLGameInstance.Optional instance) {
|
||||
this.gameInstance = instance.instance();
|
||||
if (gameInstance == null) {
|
||||
this.resourcePackManager = null;
|
||||
this.resourcePackDirectory = null;
|
||||
getItems().clear();
|
||||
return;
|
||||
}
|
||||
|
||||
this.resourcePackManager = gameInstance.getResourcePackManager();
|
||||
this.resourcePackDirectory = this.resourcePackManager.getDirectory();
|
||||
|
||||
refresh();
|
||||
@@ -185,7 +203,10 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
|
||||
}
|
||||
|
||||
private void onDownload() {
|
||||
Controllers.getDownloadPage().showResourcePackDownloads().selectInstance(instanceId);
|
||||
if (gameInstance == null) {
|
||||
return;
|
||||
}
|
||||
Controllers.getDownloadPage().showResourcePackDownloads().selectInstance(gameInstance.getId());
|
||||
Controllers.navigate(Controllers.getDownloadPage());
|
||||
}
|
||||
|
||||
@@ -232,10 +253,18 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
|
||||
}
|
||||
|
||||
public void checkUpdates(Collection<ResourcePackFile> resourcePacks) {
|
||||
HMCLGameInstance gameInstance = this.gameInstance;
|
||||
if (gameInstance == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Runnable action = () -> Controllers.taskDialog(Task
|
||||
.composeAsync(() -> {
|
||||
Optional<String> gameVersion = repository.getGameVersion(instanceId);
|
||||
return gameVersion.map(g -> new AddonCheckUpdatesTask<>(DownloadProviders.getDownloadProvider(), g, resourcePacks)).orElse(null);
|
||||
GameVersionNumber version = gameInstance.getVersion();
|
||||
return version != GameVersionNumber.unknown()
|
||||
? new AddonCheckUpdatesTask<>(DownloadProviders.getDownloadProvider(),
|
||||
version.toString(), resourcePacks)
|
||||
: null;
|
||||
})
|
||||
.whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception != null || result == null) {
|
||||
@@ -249,7 +278,7 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
|
||||
.withStagesHints("update.checking"),
|
||||
i18n("addon.check_update"), TaskCancellationAction.NORMAL);
|
||||
|
||||
if (repository.isModpack(instanceId)) {
|
||||
if (gameInstance.isModpack()) {
|
||||
Controllers.confirm(
|
||||
i18n("resourcepack.update_in_modpack.warning"), null,
|
||||
MessageDialogPane.MessageType.WARNING,
|
||||
@@ -404,7 +433,7 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
|
||||
ResourcePackInfoObject selectedItem = listView.getSelectionModel().getSelectedItem();
|
||||
if (selectedItem != null && listView.getSelectionModel().getSelectedItems().size() == 1) {
|
||||
listView.getSelectionModel().clearSelection();
|
||||
Controllers.dialog(new ResourcePackInfoDialog(control, selectedItem));
|
||||
Controllers.dialog(new ResourcePackInfoDialog(selectedItem));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -503,8 +532,6 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
|
||||
private final JFXButton btnReveal = FXUtils.newToggleButton4(SVG.FOLDER);
|
||||
private final JFXButton btnInfo = FXUtils.newToggleButton4(SVG.INFO);
|
||||
|
||||
private ResourcePackInfoObject object = null;
|
||||
|
||||
private BooleanProperty booleanProperty = null;
|
||||
|
||||
public ResourcePackListCell(JFXListView<ResourcePackInfoObject> listView, ResourcePackListPage page) {
|
||||
@@ -520,7 +547,7 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
|
||||
checkBox = new JFXCheckBox() {
|
||||
@Override
|
||||
public void fire() {
|
||||
if (!Boolean.TRUE.equals(SettingsManager.state().getShownTips().get(TIP_KEY)) && !isSelected() && object != null && !object.getFile().isCompatible()) {
|
||||
if (!Boolean.TRUE.equals(SettingsManager.state().getShownTips().get(TIP_KEY)) && !isSelected() && getItem() != null && !getItem().getFile().isCompatible()) {
|
||||
Controllers.confirm(
|
||||
i18n("resourcepack.warning.manipulate"),
|
||||
i18n("message.info"),
|
||||
@@ -535,7 +562,7 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
|
||||
}
|
||||
};
|
||||
checkBox.setOnAction(e -> {
|
||||
if (object != null) object.file.setEnabled(checkBox.isSelected());
|
||||
if (getItem() != null) getItem().getFile().setEnabled(checkBox.isSelected());
|
||||
});
|
||||
|
||||
HBox.setHgrow(content, Priority.ALWAYS);
|
||||
@@ -555,7 +582,6 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
|
||||
|
||||
if (empty || item == null) return;
|
||||
|
||||
this.object = item;
|
||||
ResourcePackFile file = item.getFile();
|
||||
imageContainer.setImage(item.getIcon());
|
||||
|
||||
@@ -569,7 +595,7 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
|
||||
FXUtils.installFastTooltip(btnReveal, i18n("reveal.in_file_manager"));
|
||||
btnReveal.setOnAction(event -> FXUtils.showFileInExplorer(file.getFile()));
|
||||
|
||||
btnInfo.setOnAction(e -> Controllers.dialog(new ResourcePackInfoDialog(this.page, item)));
|
||||
btnInfo.setOnAction(e -> Controllers.dialog(new ResourcePackInfoDialog(item)));
|
||||
|
||||
if (booleanProperty != null) {
|
||||
checkBox.selectedProperty().unbindBidirectional(booleanProperty);
|
||||
@@ -588,7 +614,7 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
|
||||
|
||||
private static final class ResourcePackInfoDialog extends JFXDialogLayout {
|
||||
|
||||
ResourcePackInfoDialog(ResourcePackListPage page, ResourcePackInfoObject packInfoObject) {
|
||||
ResourcePackInfoDialog(ResourcePackInfoObject packInfoObject) {
|
||||
ResourcePackFile pack = packInfoObject.getFile();
|
||||
|
||||
HBox titleContainer = new HBox();
|
||||
|
||||
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.ui.instances;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXDialogLayout;
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
@@ -34,13 +35,13 @@ import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.HMCLGameInstance;
|
||||
import org.jackhuang.hmcl.schematic.LitematicFile;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.*;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.util.FileNameSet;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
@@ -54,6 +55,7 @@ import java.nio.file.Files;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
@@ -63,7 +65,7 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> implements GameInstancePage.GameInstanceLoadable {
|
||||
public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> {
|
||||
|
||||
private static String translateAuthorName(String author) {
|
||||
if (I18n.isUseChinese() && "hsds".equals(author)) {
|
||||
@@ -72,14 +74,25 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
|
||||
return author;
|
||||
}
|
||||
|
||||
private final WeakListenerHolder listenerHolder = new WeakListenerHolder();
|
||||
private Path schematicsDirectory;
|
||||
private DirItem currentDirectory;
|
||||
|
||||
public SchematicsPage() {
|
||||
/// Creates a schematics list that reloads when `instanceContext` changes.
|
||||
///
|
||||
/// @param instanceContext the parent page's instance property
|
||||
public SchematicsPage(ObservableValue<? extends HMCLGameInstance.Optional> instanceContext) {
|
||||
Objects.requireNonNull(instanceContext, "instanceContext");
|
||||
FXUtils.applyDragListener(this,
|
||||
file -> currentDirectory != null && Files.isRegularFile(file) && FileUtils.getName(file).endsWith(".litematic"),
|
||||
this::addFiles
|
||||
);
|
||||
|
||||
listenerHolder.add(FXUtils.onWeakChangeAndOperate(instanceContext, current -> {
|
||||
if (current != null) {
|
||||
loadInstance(current);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -87,9 +100,9 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
|
||||
return new SchematicsPageSkin();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadInstance(HMCLGameRepository repository, @Nullable GameInstanceID instanceId) {
|
||||
this.schematicsDirectory = repository.getSchematicsDirectory(instanceId);
|
||||
public void loadInstance(HMCLGameInstance.Optional instance) {
|
||||
HMCLGameInstance gameInstance = instance.instance();
|
||||
this.schematicsDirectory = gameInstance != null ? gameInstance.getSchematicsDirectory() : null;
|
||||
|
||||
refresh();
|
||||
}
|
||||
@@ -161,10 +174,20 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
|
||||
return;
|
||||
|
||||
Path parent = currentDirectory.path;
|
||||
|
||||
FileNameSet existingPaths;
|
||||
try {
|
||||
existingPaths = FileNameSet.list(parent, null);
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to list folders in " + parent, e);
|
||||
existingPaths = new FileNameSet(false);
|
||||
}
|
||||
|
||||
Controllers.prompt(
|
||||
i18n("schematics.create_directory.prompt"),
|
||||
(result, handler) -> {
|
||||
Path targetDir = parent.resolve(result);
|
||||
|
||||
if (Files.exists(targetDir)) {
|
||||
handler.reject(i18n("schematics.create_directory.failed.already_exists"));
|
||||
return;
|
||||
@@ -178,7 +201,11 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
|
||||
LOG.warning("Failed to create directory: " + targetDir, e);
|
||||
handler.reject(i18n("schematics.create_directory.failed", targetDir));
|
||||
}
|
||||
}, "", new RequiredValidator(), new Validator(i18n("schematics.create_directory.failed.invalid_name"), FileUtils::isNameValid));
|
||||
},
|
||||
"",
|
||||
new RequiredValidator(),
|
||||
new Validator(i18n("schematics.create_directory.failed.invalid_name"), FileUtils::isNameValid),
|
||||
new Validator(i18n("schematics.create_directory.failed.already_exists"), existingPaths::notContains));
|
||||
}
|
||||
|
||||
private DirItem loadAll(Path dir, @Nullable DirItem parent) {
|
||||
@@ -598,8 +625,14 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
|
||||
|
||||
@Override
|
||||
protected void updateItem(Item item, boolean empty) {
|
||||
Item oldItem = getItem();
|
||||
boolean oldEmpty = isEmpty();
|
||||
|
||||
super.updateItem(item, empty);
|
||||
|
||||
if (oldItem == item && oldEmpty == empty) return;
|
||||
|
||||
graphics.releaseRippleImmediately();
|
||||
iconImageView.setImage(null);
|
||||
|
||||
if (empty || item == null) {
|
||||
|
||||
@@ -24,6 +24,7 @@ import com.jfoenix.controls.JFXPopup;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
@@ -36,8 +37,7 @@ import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.HMCLGameInstance;
|
||||
import org.jackhuang.hmcl.game.World;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
@@ -56,7 +56,7 @@ import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.determineOptimalPopupPosition;
|
||||
import static org.jackhuang.hmcl.util.StringUtils.parseColorEscapes;
|
||||
@@ -64,23 +64,33 @@ import static org.jackhuang.hmcl.util.i18n.I18n.formatDateTime;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class WorldListPage extends ListPageBase<World> implements GameInstancePage.GameInstanceLoadable {
|
||||
public final class WorldListPage extends ListPageBase<World> {
|
||||
private final BooleanProperty showAll = new SimpleBooleanProperty(this, "showAll", false);
|
||||
private final WeakListenerHolder listenerHolder = new WeakListenerHolder();
|
||||
|
||||
private Path savesDir;
|
||||
private List<World> worlds;
|
||||
private HMCLGameRepository repository;
|
||||
private GameInstanceID instanceId;
|
||||
private @Nullable HMCLGameInstance gameInstance;
|
||||
private final BooleanProperty supportQuickPlay = new SimpleBooleanProperty(this, "supportQuickPlay", false);
|
||||
|
||||
private int refreshCount = 0;
|
||||
|
||||
public WorldListPage() {
|
||||
/// Creates a world list that reloads when `instanceContext` changes.
|
||||
///
|
||||
/// @param instanceContext the parent page's instance property
|
||||
public WorldListPage(ObservableValue<? extends HMCLGameInstance.Optional> instanceContext) {
|
||||
Objects.requireNonNull(instanceContext, "instanceContext");
|
||||
FXUtils.applyDragListener(this, it -> "zip".equals(FileUtils.getExtension(it)), modpacks -> {
|
||||
installWorld(modpacks.get(0));
|
||||
});
|
||||
|
||||
showAll.addListener(e -> updateWorldList());
|
||||
|
||||
listenerHolder.add(FXUtils.onWeakChangeAndOperate(instanceContext, current -> {
|
||||
if (current != null) {
|
||||
loadInstance(current);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -88,21 +98,19 @@ public final class WorldListPage extends ListPageBase<World> implements GameInst
|
||||
return new WorldListPageSkin();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadInstance(HMCLGameRepository repository, @Nullable GameInstanceID instanceId) {
|
||||
this.repository = repository;
|
||||
this.instanceId = instanceId;
|
||||
this.savesDir = repository.getSavesDirectory(instanceId);
|
||||
public void loadInstance(HMCLGameInstance.Optional instance) {
|
||||
this.gameInstance = instance.instance();
|
||||
this.savesDir = gameInstance != null ? gameInstance.getSavesDirectory() : null;
|
||||
refresh();
|
||||
}
|
||||
|
||||
private void updateWorldList() {
|
||||
if (worlds == null) {
|
||||
if (worlds == null || gameInstance == null) {
|
||||
getItems().clear();
|
||||
} else if (showAll.get()) {
|
||||
getItems().setAll(worlds);
|
||||
} else {
|
||||
GameVersionNumber gameVersion = repository.getGameVersion(instanceId).map(GameVersionNumber::asGameVersion).orElse(null);
|
||||
GameVersionNumber gameVersion = gameInstance.getVersion();
|
||||
getItems().setAll(worlds.stream()
|
||||
.filter(world -> world.getGameVersion() == null || world.getGameVersion().equals(gameVersion))
|
||||
.toList());
|
||||
@@ -110,15 +118,16 @@ public final class WorldListPage extends ListPageBase<World> implements GameInst
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
if (repository == null || instanceId == null)
|
||||
if (gameInstance == null || savesDir == null)
|
||||
return;
|
||||
|
||||
int currentRefresh = ++refreshCount;
|
||||
HMCLGameInstance gameInstance = this.gameInstance;
|
||||
|
||||
setLoading(true);
|
||||
Task.supplyAsync(Schedulers.io(), () -> {
|
||||
// Ensure the game version number is parsed
|
||||
repository.getGameVersion(instanceId);
|
||||
gameInstance.getVersion();
|
||||
return World.getWorlds(savesDir);
|
||||
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (refreshCount != currentRefresh) {
|
||||
@@ -126,8 +135,7 @@ public final class WorldListPage extends ListPageBase<World> implements GameInst
|
||||
return;
|
||||
}
|
||||
|
||||
Optional<String> gameVersion = repository.getGameVersion(instanceId);
|
||||
supportQuickPlay.set(World.supportQuickPlay(GameVersionNumber.asGameVersion(gameVersion)));
|
||||
supportQuickPlay.set(World.supportQuickPlay(gameInstance.getVersion()));
|
||||
|
||||
worlds = result;
|
||||
updateWorldList();
|
||||
@@ -180,7 +188,9 @@ public final class WorldListPage extends ListPageBase<World> implements GameInst
|
||||
}
|
||||
|
||||
private void showManagePage(World world) {
|
||||
Controllers.navigate(new WorldManagePage(world, repository, instanceId));
|
||||
if (gameInstance != null) {
|
||||
Controllers.navigate(new WorldManagePage(world, gameInstance));
|
||||
}
|
||||
}
|
||||
|
||||
public void export(World world) {
|
||||
@@ -200,11 +210,15 @@ public final class WorldListPage extends ListPageBase<World> implements GameInst
|
||||
}
|
||||
|
||||
public void launch(World world) {
|
||||
Instances.launchAndEnterWorld(repository, instanceId, world.getFileName());
|
||||
if (gameInstance != null) {
|
||||
Instances.launchAndEnterWorld(gameInstance, world.getFileName());
|
||||
}
|
||||
}
|
||||
|
||||
public void generateLaunchScript(World world) {
|
||||
Instances.generateLaunchScriptForQuickEnterWorld(repository, instanceId, world.getFileName());
|
||||
if (gameInstance != null) {
|
||||
Instances.generateLaunchScriptForQuickEnterWorld(gameInstance, world.getFileName());
|
||||
}
|
||||
}
|
||||
|
||||
public BooleanProperty showAllProperty() {
|
||||
@@ -330,6 +344,7 @@ public final class WorldListPage extends ListPageBase<World> implements GameInst
|
||||
|
||||
if (oldWorld == world && oldEmpty == empty) return;
|
||||
|
||||
this.graphic.releaseRippleImmediately();
|
||||
this.content.getTags().clear();
|
||||
|
||||
if (empty || world == null) {
|
||||
|
||||
@@ -24,8 +24,7 @@ import javafx.geometry.Insets;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.HMCLGameInstance;
|
||||
import org.jackhuang.hmcl.game.World;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
@@ -36,13 +35,11 @@ import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.ChunkBaseApp;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
@@ -54,8 +51,7 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
|
||||
|
||||
private final World world;
|
||||
private final Path backupsDir;
|
||||
private final HMCLGameRepository repository;
|
||||
private final GameInstanceID instanceId;
|
||||
private final HMCLGameInstance gameInstance;
|
||||
private final boolean supportQuickPlay;
|
||||
private FileChannel sessionLockChannel;
|
||||
|
||||
@@ -70,11 +66,10 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
|
||||
private final TabHeader.Tab<WorldBackupsPage> worldBackupsTab = new TabHeader.Tab<>("worldBackupsPage");
|
||||
private final TabHeader.Tab<DataPackListPage> dataPackTab = new TabHeader.Tab<>("dataPackListPage");
|
||||
|
||||
public WorldManagePage(World world, HMCLGameRepository repository, GameInstanceID instanceId) {
|
||||
public WorldManagePage(World world, HMCLGameInstance gameInstance) {
|
||||
this.world = world;
|
||||
this.backupsDir = repository.getBackupsDirectory(instanceId);
|
||||
this.repository = repository;
|
||||
this.instanceId = instanceId;
|
||||
this.gameInstance = gameInstance;
|
||||
this.backupsDir = gameInstance.getBackupsDirectory();
|
||||
|
||||
updateSessionLockChannel();
|
||||
|
||||
@@ -91,8 +86,7 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
|
||||
|
||||
this.state = new SimpleObjectProperty<>(new State(i18n("world.manage.title", StringUtils.parseColorEscapes(world.getWorldName())), null, true, true, true));
|
||||
|
||||
Optional<String> gameVersion = repository.getGameVersion(instanceId);
|
||||
supportQuickPlay = World.supportQuickPlay(GameVersionNumber.asGameVersion(gameVersion));
|
||||
supportQuickPlay = World.supportQuickPlay(gameInstance.getVersion());
|
||||
|
||||
this.addEventHandler(Navigator.NavigationEvent.EXITED, this::onExited);
|
||||
this.addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated);
|
||||
@@ -151,11 +145,11 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
|
||||
|
||||
public void launch() {
|
||||
fireEvent(new PageCloseEvent());
|
||||
Instances.launchAndEnterWorld(repository, instanceId, world.getFileName());
|
||||
Instances.launchAndEnterWorld(gameInstance, world.getFileName());
|
||||
}
|
||||
|
||||
public void generateLaunchScript() {
|
||||
Instances.generateLaunchScriptForQuickEnterWorld(repository, instanceId, world.getFileName());
|
||||
Instances.generateLaunchScriptForQuickEnterWorld(gameInstance, world.getFileName());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -21,7 +21,6 @@ import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
@@ -184,7 +183,10 @@ public final class JavaManagementPage extends ListPageBase<JavaRuntime> {
|
||||
@FXThread
|
||||
private void loadJava(Collection<JavaRuntime> javaRuntimes) {
|
||||
if (javaRuntimes != null) {
|
||||
this.setItems(FXCollections.observableArrayList(javaRuntimes));
|
||||
// JavaRuntime equality is path-based. setAll can therefore retain a stale item when the
|
||||
// Java installation at an existing path has been upgraded.
|
||||
this.getItems().clear();
|
||||
this.getItems().addAll(javaRuntimes);
|
||||
this.setLoading(false);
|
||||
} else {
|
||||
this.setLoading(true);
|
||||
@@ -227,7 +229,7 @@ public final class JavaManagementPage extends ListPageBase<JavaRuntime> {
|
||||
}
|
||||
|
||||
private static final class JavaItemCell extends ListCell<JavaRuntime> {
|
||||
private final Node graphic;
|
||||
private final RipplerContainer graphic;
|
||||
private final Label label = new Label();
|
||||
private final TwoLineListItem content;
|
||||
|
||||
@@ -298,7 +300,14 @@ public final class JavaManagementPage extends ListPageBase<JavaRuntime> {
|
||||
@Override
|
||||
protected void updateItem(JavaRuntime item, boolean empty) {
|
||||
JavaRuntime oldItem = getItem();
|
||||
boolean oldEmpty = isEmpty();
|
||||
|
||||
super.updateItem(item, empty);
|
||||
|
||||
if (oldItem == item && oldEmpty == empty) return;
|
||||
|
||||
this.graphic.releaseRippleImmediately();
|
||||
|
||||
if (empty || item == null) {
|
||||
setGraphic(null);
|
||||
} else {
|
||||
|
||||
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.ui.main;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.HMCLGameInstance;
|
||||
import org.jackhuang.hmcl.setting.GameSettings;
|
||||
import org.jackhuang.hmcl.setting.GameDirectoryManager;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
@@ -48,7 +49,7 @@ public class LauncherSettingsPage extends DecoratorAnimatedPage implements Decor
|
||||
private final TransitionPane transitionPane = new TransitionPane();
|
||||
|
||||
public LauncherSettingsPage() {
|
||||
gameTab.setNodeSupplier(() -> new GameSettingsPage<>(GameSettings.Preset.class));
|
||||
gameTab.setNodeSupplier(() -> new GameSettingsPage<>(GameSettings.Preset.class, null));
|
||||
javaManagementTab.setNodeSupplier(JavaManagementPage::new);
|
||||
settingsTab.setNodeSupplier(SettingsPage::new);
|
||||
personalizationTab.setNodeSupplier(PersonalizationPage::new);
|
||||
@@ -59,7 +60,7 @@ public class LauncherSettingsPage extends DecoratorAnimatedPage implements Decor
|
||||
tab = new TabHeader(transitionPane, gameTab, javaManagementTab, settingsTab, personalizationTab, downloadTab, helpTab, feedbackTab, aboutTab);
|
||||
|
||||
tab.select(gameTab);
|
||||
addEventHandler(Navigator.NavigationEvent.NAVIGATED, event -> gameTab.getNode().loadInstance(GameDirectoryManager.getSelectedRepository(), null));
|
||||
addEventHandler(Navigator.NavigationEvent.NAVIGATED, event -> gameTab.getNode().loadInstance(HMCLGameInstance.Optional.empty(GameDirectoryManager.getSelectedRepository())));
|
||||
|
||||
AdvancedListBox sideBar = new AdvancedListBox()
|
||||
.addNavigationDrawerTab(tab, gameTab, i18n("settings.type.global.manage"), SVG.STADIA_CONTROLLER, SVG.STADIA_CONTROLLER_FILL)
|
||||
@@ -89,7 +90,7 @@ public class LauncherSettingsPage extends DecoratorAnimatedPage implements Decor
|
||||
}
|
||||
|
||||
public void showGameSettings(HMCLGameRepository repository) {
|
||||
gameTab.getNode().loadInstance(repository, null);
|
||||
gameTab.getNode().loadInstance(HMCLGameInstance.Optional.empty(repository));
|
||||
tab.select(gameTab, false);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import javafx.animation.KeyValue;
|
||||
import javafx.animation.RotateTransition;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.event.EventHandler;
|
||||
@@ -43,14 +44,10 @@ import javafx.scene.layout.VBox;
|
||||
import javafx.scene.text.TextFlow;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.VersionList;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.GameInstanceManifest;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.download.ComponentVersionList;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.setting.GameDirectory;
|
||||
import org.jackhuang.hmcl.setting.GameDirectoryManager;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
@@ -76,30 +73,40 @@ import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.Platform;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.UnmodifiableView;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.jackhuang.hmcl.download.RemoteVersion.Type.RELEASE;
|
||||
import static org.jackhuang.hmcl.download.ComponentRemoteVersion.Type.RELEASE;
|
||||
import static org.jackhuang.hmcl.setting.SettingsManager.state;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.SINE;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/// Displays the launcher home controls for the currently selected game repository.
|
||||
public final class MainPage extends StackPane implements DecoratorPage {
|
||||
private static final String ANNOUNCEMENT = "announcement";
|
||||
|
||||
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>();
|
||||
|
||||
private final ObjectProperty<@Nullable GameInstanceID> currentGame = new SimpleObjectProperty<>(this, "currentGame");
|
||||
private final ObjectProperty<@Nullable HMCLGameInstance> currentGame = new SimpleObjectProperty<>(this, "currentGame");
|
||||
private final BooleanProperty showUpdate = new SimpleBooleanProperty(this, "showUpdate");
|
||||
private final BooleanProperty showUpdateDialog = new SimpleBooleanProperty(this, "showUpdateDialog");
|
||||
private final ObjectProperty<RemoteVersion> latestVersion = new SimpleObjectProperty<>(this, "latestVersion");
|
||||
private final ObservableList<GameInstanceManifest> versions = FXCollections.observableArrayList();
|
||||
private HMCLGameRepository repository;
|
||||
/// Mutable storage for visible instances from the selected repository's current snapshot.
|
||||
private final ObservableList<HMCLGameInstance> mutableInstances = FXCollections.observableArrayList();
|
||||
|
||||
/// Read-only observable view of [#mutableInstances].
|
||||
private final @UnmodifiableView ObservableList<HMCLGameInstance> instances =
|
||||
FXCollections.unmodifiableObservableList(mutableInstances);
|
||||
|
||||
/// Current snapshot of the repository selected by [GameDirectoryManager].
|
||||
private final ObservableValue<HMCLGameRepositorySnapshot> selectedRepositorySnapshot =
|
||||
BindingMapping.of(GameDirectoryManager.selectedRepositoryProperty())
|
||||
.flatMap(HMCLGameRepository::snapshotProperty);
|
||||
|
||||
private TransitionPane announcementPane;
|
||||
private final StackPane updatePane;
|
||||
@@ -211,10 +218,12 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
|
||||
HBox launchPane = new HBox();
|
||||
launchPane.getStyleClass().add("launch-pane");
|
||||
FXUtils.onScroll(launchPane, versions, list -> {
|
||||
GameInstanceID currentId = getCurrentGame();
|
||||
return Lang.indexWhere(list, instance -> instance.id().equals(currentId));
|
||||
}, it -> repository.setSelectedInstance(it.id()));
|
||||
FXUtils.onChangeAndOperate(selectedRepositorySnapshot, ignored -> mutableInstances.setAll(GameDirectoryManager.getSelectedRepository().getDisplayInstances().toList()));
|
||||
FXUtils.onScroll(launchPane, instances, list -> {
|
||||
@Nullable HMCLGameInstance currentGame = getCurrentGame();
|
||||
@Nullable GameInstanceID currentId = currentGame != null ? currentGame.getId() : null;
|
||||
return Lang.indexWhere(list, instance -> instance.getId().equals(currentId));
|
||||
}, instance -> instance.getRepository().setSelectedInstance(instance));
|
||||
|
||||
StackPane.setAlignment(launchPane, Pos.BOTTOM_RIGHT);
|
||||
{
|
||||
@@ -233,7 +242,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
private Tooltip tooltip;
|
||||
|
||||
@Override
|
||||
public void accept(@Nullable GameInstanceID currentGame) {
|
||||
public void accept(@Nullable HMCLGameInstance currentGame) {
|
||||
if (currentGame == null) {
|
||||
launchLabel.setText(i18n("instance.launch.empty"));
|
||||
currentLabel.setText(null);
|
||||
@@ -244,7 +253,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
FXUtils.installFastTooltip(launchButton, tooltip);
|
||||
} else {
|
||||
launchLabel.setText(i18n("instance.launch"));
|
||||
currentLabel.setText(currentGame.toString());
|
||||
currentLabel.setText(currentGame.getId().toString());
|
||||
graphic.getChildren().setAll(launchLabel, currentLabel);
|
||||
FXUtils.setOnActionWithCooldown(launchButton, MainPage.this::launch);
|
||||
if (tooltip != null)
|
||||
@@ -269,7 +278,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
JFXPopup.PopupHPosition.RIGHT,
|
||||
0,
|
||||
-menuButton.getHeight(),
|
||||
repository, versions
|
||||
instances
|
||||
);
|
||||
|
||||
Node graphic = menuButton.getGraphic();
|
||||
@@ -346,12 +355,12 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
|
||||
private void launch() {
|
||||
HMCLGameRepository repository = GameDirectoryManager.getSelectedRepository();
|
||||
Instances.launch(repository, repository.getSelectedInstance());
|
||||
Instances.launch(repository.getSelectedInstance());
|
||||
}
|
||||
|
||||
private void launchNoGame() {
|
||||
DownloadProvider downloadProvider = DownloadProviders.getDownloadProvider();
|
||||
VersionList<?> versionList = downloadProvider.getVersionListById("game");
|
||||
ComponentVersionList<?> versionList = downloadProvider.getVersionList(GameComponentType.GAME);
|
||||
|
||||
Holder<GameInstanceID> instanceHolder = new Holder<>();
|
||||
Task<?> task = versionList.refreshAsync("")
|
||||
@@ -363,26 +372,25 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
.orElseThrow(() -> new IOException("No versions found")))
|
||||
.thenComposeAsync(version -> {
|
||||
HMCLGameRepository repository = GameDirectoryManager.getSelectedRepository();
|
||||
DefaultDependencyManager dependency = repository.getDependency();
|
||||
HMCLDependencyManager dependency = repository.getDependency();
|
||||
|
||||
String gameVersion = version.getGameVersion();
|
||||
GameInstanceID instanceId = new GameInstanceID(gameVersion);
|
||||
|
||||
instanceHolder.value = instanceId;
|
||||
|
||||
return dependency.newGameBuilder()
|
||||
.name(instanceId)
|
||||
.gameVersion(gameVersion)
|
||||
.buildAsync();
|
||||
try (HMCLGameBuilder builder = dependency.newGameBuilder(instanceId)) {
|
||||
return builder
|
||||
.component(GameComponentType.GAME, gameVersion)
|
||||
.buildAsync();
|
||||
}
|
||||
})
|
||||
.whenComplete(any -> GameDirectoryManager.getSelectedRepository().refresh())
|
||||
.whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception == null) {
|
||||
GameDirectoryManager.getSelectedRepository().setSelectedInstance(instanceHolder.value);
|
||||
HMCLGameRepository repository = GameDirectoryManager.getSelectedRepository();
|
||||
repository.setSelectedInstance(repository.getInstance(instanceHolder.value));
|
||||
launch();
|
||||
} else if (exception instanceof CancellationException) {
|
||||
Controllers.showToast(i18n("message.cancelled"));
|
||||
} else {
|
||||
} else if (!(exception instanceof CancellationException)) {
|
||||
LOG.warning("Failed to install game", exception);
|
||||
Controllers.dialog(StringUtils.getStackTrace(exception),
|
||||
i18n("install.failed"),
|
||||
@@ -410,28 +418,35 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
return state;
|
||||
}
|
||||
|
||||
public GameDirectory getGameDirectory() {
|
||||
return repository.getGameDirectory();
|
||||
}
|
||||
|
||||
public HMCLGameRepository getRepository() {
|
||||
return repository;
|
||||
}
|
||||
|
||||
public GameInstanceID getCurrentGame() {
|
||||
/// Returns the instance shown by the launch controls.
|
||||
///
|
||||
/// @return the current instance, or `null` when no instance is selected
|
||||
public @Nullable HMCLGameInstance getCurrentGame() {
|
||||
return currentGame.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<@Nullable GameInstanceID> currentGameProperty() {
|
||||
/// Returns the property for the instance shown by the launch controls.
|
||||
///
|
||||
/// @return the current-instance property
|
||||
public ObjectProperty<@Nullable HMCLGameInstance> currentGameProperty() {
|
||||
return currentGame;
|
||||
}
|
||||
|
||||
public void setCurrentGame(@Nullable GameInstanceID currentGame) {
|
||||
/// Sets the instance shown by the launch controls.
|
||||
///
|
||||
/// @param currentGame the instance to show, or `null` to show the empty state
|
||||
public void setCurrentGame(@Nullable HMCLGameInstance currentGame) {
|
||||
this.currentGame.set(currentGame);
|
||||
}
|
||||
|
||||
public ObservableList<GameInstanceManifest> getVersions() {
|
||||
return versions;
|
||||
/// Returns the observable instances displayed by launch-selection controls.
|
||||
///
|
||||
/// The list is updated from the selected repository's published snapshot and contains no hidden
|
||||
/// instances. The returned view cannot be mutated.
|
||||
///
|
||||
/// @return the observable launch-menu instances
|
||||
public @UnmodifiableView ObservableList<HMCLGameInstance> getInstances() {
|
||||
return instances;
|
||||
}
|
||||
|
||||
public boolean isShowUpdate() {
|
||||
@@ -470,9 +485,4 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
this.latestVersion.set(latestVersion);
|
||||
}
|
||||
|
||||
public void initVersions(HMCLGameRepository repository, List<GameInstanceManifest> versions) {
|
||||
FXUtils.checkFxUserThread();
|
||||
this.repository = repository;
|
||||
this.versions.setAll(versions);
|
||||
}
|
||||
}
|
||||
@@ -25,19 +25,15 @@ import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.StringBinding;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ColorPicker;
|
||||
import javafx.scene.control.ContentDisplay;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.scene.text.Font;
|
||||
import javafx.scene.text.FontSmoothingType;
|
||||
import javafx.stage.FileChooser;
|
||||
@@ -45,23 +41,13 @@ import org.glavo.monetfx.Brightness;
|
||||
import org.glavo.monetfx.ColorStyle;
|
||||
import org.glavo.uuid.UUIDs;
|
||||
import org.jackhuang.hmcl.setting.*;
|
||||
import org.jackhuang.hmcl.theme.BackgroundLoadPolicy;
|
||||
import org.jackhuang.hmcl.theme.BuiltinBackground;
|
||||
import org.jackhuang.hmcl.theme.NetworkBackgroundImageCachePolicy;
|
||||
import org.jackhuang.hmcl.theme.Theme;
|
||||
import org.jackhuang.hmcl.theme.ThemeColor;
|
||||
import org.jackhuang.hmcl.theme.ThemeColorSource;
|
||||
import org.jackhuang.hmcl.theme.ThemePackExporter;
|
||||
import org.jackhuang.hmcl.theme.ThemePackManifest;
|
||||
import org.jackhuang.hmcl.theme.ThemePackManager;
|
||||
import org.jackhuang.hmcl.theme.ThemeReference;
|
||||
import org.jackhuang.hmcl.theme.*;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
|
||||
import org.jackhuang.hmcl.util.Holder;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
|
||||
@@ -1271,7 +1257,7 @@ public class PersonalizationPage extends StackPane {
|
||||
|
||||
Label lblLogFontDisplay = new Label("[23:33:33] [Client Thread/INFO] [WaterPower]: Loaded mod WaterPower.");
|
||||
lblLogFontDisplay.fontProperty().bind(Bindings.createObjectBinding(
|
||||
() -> Font.font(Lang.requireNonNullElse(settings().logFontFamilyProperty().get(), FXUtils.DEFAULT_MONOSPACE_FONT), settings().logFontSizeProperty().get()),
|
||||
() -> Font.font(Objects.requireNonNullElse(settings().logFontFamilyProperty().get(), FXUtils.DEFAULT_MONOSPACE_FONT), settings().logFontSizeProperty().get()),
|
||||
settings().logFontFamilyProperty(), settings().logFontSizeProperty()));
|
||||
|
||||
logFontPane.getChildren().add(lblLogFontDisplay);
|
||||
|
||||
@@ -20,18 +20,11 @@ package org.jackhuang.hmcl.ui.main;
|
||||
import com.jfoenix.controls.JFXPopup;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.scene.layout.Region;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.event.EventBus;
|
||||
import org.jackhuang.hmcl.event.RefreshedGameInstancesEvent;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.GameInstanceManifest;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.HMCLGameInstance;
|
||||
import org.jackhuang.hmcl.game.ModpackHelper;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.setting.GameDirectory;
|
||||
import org.jackhuang.hmcl.setting.GameDirectoryManager;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.terracotta.TerracottaMetadata;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
@@ -45,29 +38,21 @@ import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
|
||||
import org.jackhuang.hmcl.ui.nbt.NBTEditorPage;
|
||||
import org.jackhuang.hmcl.ui.nbt.NBTFileType;
|
||||
import org.jackhuang.hmcl.ui.instances.GameAdvancedListItem;
|
||||
import org.jackhuang.hmcl.ui.instances.GameListPopupMenu;
|
||||
import org.jackhuang.hmcl.ui.instances.Instances;
|
||||
import org.jackhuang.hmcl.ui.nbt.NBTEditorPage;
|
||||
import org.jackhuang.hmcl.ui.nbt.NBTFileType;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.*;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
@@ -75,13 +60,6 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
private MainPage mainPage = null;
|
||||
|
||||
public RootPage() {
|
||||
EventBus.EVENT_BUS.channel(RefreshedGameInstancesEvent.class)
|
||||
.register(event -> onRefreshedVersions((HMCLGameRepository) event.getSource()));
|
||||
|
||||
HMCLGameRepository repository = GameDirectoryManager.getSelectedRepository();
|
||||
if (repository.isLoaded())
|
||||
onRefreshedVersions(GameDirectoryManager.getSelectedRepository());
|
||||
|
||||
getStyleClass().remove("gray-background");
|
||||
getLeft().getStyleClass().add("gray-background");
|
||||
}
|
||||
@@ -122,20 +100,6 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
|
||||
FXUtils.onChangeAndOperate(GameDirectoryManager.selectedInstanceProperty(), mainPage::setCurrentGame);
|
||||
mainPage.latestVersionProperty().bind(UpdateChecker.latestVersionProperty());
|
||||
|
||||
GameDirectoryManager.registerVersionsListener(repository -> {
|
||||
GameDirectory gameDirectory = repository.getGameDirectory();
|
||||
List<GameInstanceManifest> children = repository.getInstanceManifests().parallelStream()
|
||||
.filter(version -> !version.isHidden())
|
||||
.sorted(Comparator
|
||||
.comparing((GameInstanceManifest manifest) -> Lang.requireNonNullElse(manifest.releaseTime(), Instant.EPOCH))
|
||||
.thenComparing(manifest -> VersionNumber.asVersion(repository.getGameVersion(manifest).orElse(manifest.id().toString()))))
|
||||
.collect(Collectors.toList());
|
||||
runInFX(() -> {
|
||||
if (gameDirectory == GameDirectoryManager.getSelectedGameDirectory())
|
||||
mainPage.initVersions(repository, children);
|
||||
});
|
||||
});
|
||||
this.mainPage = mainPage;
|
||||
}
|
||||
return mainPage;
|
||||
@@ -155,17 +119,18 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
// second item in left sidebar
|
||||
GameAdvancedListItem gameListItem = new GameAdvancedListItem();
|
||||
gameListItem.setOnAction(e -> {
|
||||
GameInstanceID instanceId = GameDirectoryManager.getSelectedRepository().getSelectedInstance();
|
||||
if (instanceId == null) {
|
||||
@Nullable HMCLGameInstance instance = GameDirectoryManager.getSelectedRepository().getSelectedInstance();
|
||||
if (instance == null) {
|
||||
Controllers.navigate(Controllers.getGameListPage());
|
||||
} else {
|
||||
Instances.modifyGameSettings(GameDirectoryManager.getSelectedRepository(), instanceId);
|
||||
Instances.modifyGameSettings(instance);
|
||||
}
|
||||
});
|
||||
FXUtils.onScroll(gameListItem, getSkinnable().getMainPage().getVersions(), list -> {
|
||||
GameInstanceID currentId = getSkinnable().getMainPage().getCurrentGame();
|
||||
return Lang.indexWhere(list, instance -> instance.id().equals(currentId));
|
||||
}, it -> getSkinnable().getMainPage().getRepository().setSelectedInstance(it.id()));
|
||||
FXUtils.onScroll(gameListItem, getSkinnable().getMainPage().getInstances(), list -> {
|
||||
@Nullable HMCLGameInstance currentGame = getSkinnable().getMainPage().getCurrentGame();
|
||||
@Nullable GameInstanceID currentId = currentGame != null ? currentGame.getId() : null;
|
||||
return Lang.indexWhere(list, instance -> instance.getId().equals(currentId));
|
||||
}, instance -> instance.getRepository().setSelectedInstance(instance));
|
||||
if (AnimationUtils.isAnimationEnabled()) {
|
||||
FXUtils.prepareOnMouseEnter(gameListItem, Controllers::prepareGameInstancePage);
|
||||
}
|
||||
@@ -182,10 +147,7 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
AdvancedListItem downloadItem = new AdvancedListItem();
|
||||
downloadItem.setLeftIcon(SVG.DOWNLOAD);
|
||||
downloadItem.setTitle(i18n("download"));
|
||||
downloadItem.setOnAction(e -> {
|
||||
Controllers.getDownloadPage().showGameDownloads();
|
||||
Controllers.navigate(Controllers.getDownloadPage());
|
||||
});
|
||||
downloadItem.setOnAction(e -> Controllers.navigate(Controllers.getDownloadPage()));
|
||||
if (AnimationUtils.isAnimationEnabled()) {
|
||||
FXUtils.prepareOnMouseEnter(downloadItem, Controllers::prepareDownloadPage);
|
||||
}
|
||||
@@ -252,44 +214,8 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
JFXPopup.PopupHPosition.LEFT,
|
||||
gameListItem.getWidth(),
|
||||
0,
|
||||
getSkinnable().getMainPage().getRepository(),
|
||||
getSkinnable().getMainPage().getVersions());
|
||||
getSkinnable().getMainPage().getInstances());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkedModpack = false;
|
||||
|
||||
private void onRefreshedVersions(HMCLGameRepository repository) {
|
||||
runInFX(() -> {
|
||||
if (!checkedModpack) {
|
||||
checkedModpack = true;
|
||||
|
||||
if (repository.getInstanceCount() == 0) {
|
||||
Path zipModpack = Metadata.CURRENT_DIRECTORY.resolve("modpack.zip");
|
||||
Path mrpackModpack = Metadata.CURRENT_DIRECTORY.resolve("modpack.mrpack");
|
||||
|
||||
Path modpackFile;
|
||||
if (Files.exists(zipModpack)) {
|
||||
modpackFile = zipModpack;
|
||||
} else if (Files.exists(mrpackModpack)) {
|
||||
modpackFile = mrpackModpack;
|
||||
} else {
|
||||
modpackFile = null;
|
||||
}
|
||||
|
||||
if (modpackFile != null) {
|
||||
Task.supplyAsync(() -> CompressingUtils.findSuitableEncoding(modpackFile))
|
||||
.thenApplyAsync(encoding -> ModpackHelper.readModpackManifest(modpackFile, encoding))
|
||||
.thenApplyAsync(modpack -> ModpackHelper
|
||||
.getInstallTask(repository, modpackFile, new GameInstanceID(modpack.getName()), modpack, null)
|
||||
.executor())
|
||||
.thenAcceptAsync(Schedulers.javafx(), executor -> {
|
||||
Controllers.taskDialog(executor, i18n("modpack.installing"), TaskCancellationAction.NO_CANCEL);
|
||||
executor.start();
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -632,7 +632,7 @@ public final class ThemePackManagementPage extends ListPageBase<ThemePackManager
|
||||
private final ThemePackManagementPage page;
|
||||
|
||||
/// Root graphic reused by this cell.
|
||||
private final Region graphic;
|
||||
private final RipplerContainer graphic;
|
||||
|
||||
/// The text content shown for the current theme pack.
|
||||
private final TwoLineListItem content = new TwoLineListItem();
|
||||
@@ -714,19 +714,21 @@ public final class ThemePackManagementPage extends ListPageBase<ThemePackManager
|
||||
@Override
|
||||
protected void updateItem(ThemePackManager.@Nullable InstalledThemePack themePack, boolean empty) {
|
||||
var currentItem = getItem();
|
||||
boolean oldEmpty = isEmpty();
|
||||
|
||||
super.updateItem(themePack, empty);
|
||||
|
||||
if (Objects.equals(getItem(), currentItem)) return;
|
||||
if (Objects.equals(getItem(), currentItem) && oldEmpty == empty) return;
|
||||
|
||||
this.graphic.releaseRippleImmediately();
|
||||
content.getTags().clear();
|
||||
iconImage.setImage(null);
|
||||
iconFallback.setVisible(false);
|
||||
|
||||
if (empty || themePack == null) {
|
||||
setGraphic(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setGraphic(graphic);
|
||||
|
||||
ThemePackManifest manifest = themePack.manifest();
|
||||
|
||||
@@ -221,7 +221,7 @@ public class TerracottaControllerPage extends StackPane {
|
||||
MessageDialogPane.MessageType.QUESTION
|
||||
).addAction(i18n("instance.launch"), () -> {
|
||||
var repository = GameDirectoryManager.getSelectedRepository();
|
||||
Instances.launch(repository, repository.getSelectedInstance(), launcherHelper -> {
|
||||
Instances.launch(repository.getSelectedInstance(), launcherHelper -> {
|
||||
launcherHelper.setKeep();
|
||||
launcherHelper.setDisableOfflineSkin();
|
||||
});
|
||||
|
||||
@@ -26,6 +26,7 @@ import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.HMCLGameInstance;
|
||||
import org.jackhuang.hmcl.setting.*;
|
||||
import org.jackhuang.hmcl.terracotta.TerracottaMetadata;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
@@ -41,6 +42,7 @@ import org.jackhuang.hmcl.ui.main.MainPage;
|
||||
import org.jackhuang.hmcl.ui.instances.GameListPopupMenu;
|
||||
import org.jackhuang.hmcl.ui.instances.Instances;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.SettingsManager.userState;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
@@ -54,7 +56,7 @@ public class TerracottaPage extends DecoratorAnimatedPage implements DecoratorPa
|
||||
private final TransitionPane transitionPane = new TransitionPane();
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private ChangeListener<GameInstanceID> instanceChangeListenerHolder;
|
||||
private @Nullable ChangeListener<@Nullable HMCLGameInstance> instanceChangeListenerHolder;
|
||||
|
||||
public TerracottaPage() {
|
||||
statusPage.setNodeSupplier(TerracottaControllerPage::new);
|
||||
@@ -79,27 +81,28 @@ public class TerracottaPage extends DecoratorAnimatedPage implements DecoratorPa
|
||||
.add(accountListItem)
|
||||
.addNavigationDrawerItem(i18n("instance.launch"), SVG.ROCKET_LAUNCH, () -> {
|
||||
var repository = GameDirectoryManager.getSelectedRepository();
|
||||
Instances.launch(repository, repository.getSelectedInstance(), launcherHelper -> {
|
||||
Instances.launch(repository.getSelectedInstance(), launcherHelper -> {
|
||||
launcherHelper.setKeep();
|
||||
launcherHelper.setDisableOfflineSkin();
|
||||
});
|
||||
}, item -> {
|
||||
instanceChangeListenerHolder = FXUtils.onWeakChangeAndOperate(GameDirectoryManager.selectedInstanceProperty(),
|
||||
instanceName -> item.setSubtitle(instanceName != null ? instanceName.toString() : i18n("instance.empty"))
|
||||
instance -> item.setSubtitle(instance != null ? instance.getId().toString() : i18n("instance.empty"))
|
||||
);
|
||||
|
||||
MainPage mainPage = Controllers.getRootPage().getMainPage();
|
||||
FXUtils.onScroll(item, mainPage.getVersions(), list -> {
|
||||
GameInstanceID currentId = mainPage.getCurrentGame();
|
||||
return Lang.indexWhere(list, instance -> instance.id().equals(currentId));
|
||||
}, it -> mainPage.getRepository().setSelectedInstance(it.id()));
|
||||
FXUtils.onScroll(item, mainPage.getInstances(), list -> {
|
||||
@Nullable HMCLGameInstance currentGame = mainPage.getCurrentGame();
|
||||
@Nullable GameInstanceID currentId = currentGame != null ? currentGame.getId() : null;
|
||||
return Lang.indexWhere(list, instance -> instance.getId().equals(currentId));
|
||||
}, instance -> instance.getRepository().setSelectedInstance(instance));
|
||||
|
||||
FXUtils.onSecondaryButtonClicked(item, () -> GameListPopupMenu.show(item,
|
||||
JFXPopup.PopupVPosition.BOTTOM,
|
||||
JFXPopup.PopupHPosition.LEFT,
|
||||
item.getWidth(),
|
||||
0,
|
||||
mainPage.getRepository(), mainPage.getVersions()));
|
||||
mainPage.getInstances()));
|
||||
})
|
||||
.addNavigationDrawerItem(i18n("terracotta.feedback.title"), SVG.FEEDBACK, () -> FXUtils.openLink(TerracottaMetadata.FEEDBACK_LINK));
|
||||
BorderPane.setMargin(toolbar, new Insets(0, 0, 12, 0));
|
||||
|
||||
@@ -159,9 +159,7 @@ public final class UpdateHandler {
|
||||
} else {
|
||||
Exception e = executor.getException();
|
||||
LOG.warning("Failed to update to " + version, e);
|
||||
if (e instanceof CancellationException) {
|
||||
Platform.runLater(() -> Controllers.showToast(i18n("message.cancelled")));
|
||||
} else {
|
||||
if (!(e instanceof CancellationException)) {
|
||||
Platform.runLater(() -> Controllers.dialog(e.toString(), i18n("update.failed"), MessageType.ERROR));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.util;
|
||||
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.addon.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.addon.mod.ModManager;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.java.JavaRuntime;
|
||||
import org.jackhuang.hmcl.setting.GameSettings;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.java.JavaRuntime;
|
||||
import org.jackhuang.hmcl.util.platform.OSVersion;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.Platform;
|
||||
@@ -73,13 +73,14 @@ public final class NativePatcher {
|
||||
);
|
||||
}
|
||||
|
||||
public static GameInstanceManifest patchNative(DefaultGameRepository repository,
|
||||
GameInstanceManifest manifest, String gameVersion,
|
||||
public static GameInstanceManifest patchNative(DefaultGameInstance instance,
|
||||
GameInstanceManifest manifest,
|
||||
JavaRuntime javaVersion,
|
||||
GameSettings.Effective settings,
|
||||
List<String> javaArguments) {
|
||||
GameVersionNumber gameVersion = instance.getVersion();
|
||||
if (settings.getInheritable(GameSettings::useCustomNativesProperty)) {
|
||||
if (gameVersion != null && GameVersionNumber.compare(gameVersion, "1.19") < 0)
|
||||
if (gameVersion.compareTo("1.19") < 0)
|
||||
return manifest;
|
||||
|
||||
ArrayList<Library> newLibraries = new ArrayList<>();
|
||||
@@ -96,17 +97,17 @@ public final class NativePatcher {
|
||||
return manifest.withLibraries(newLibraries);
|
||||
}
|
||||
|
||||
final boolean useNativeGLFW = settings.getInheritable(GameSettings::useNativeGLFWProperty);
|
||||
final boolean useNativeGLFWorSDL = settings.getInheritable(GameSettings::useNativeGLFWorSDLProperty);
|
||||
final boolean useNativeOpenAL = settings.getInheritable(GameSettings::useNativeOpenALProperty);
|
||||
|
||||
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD() && (useNativeGLFW || useNativeOpenAL)
|
||||
&& gameVersion != null && GameVersionNumber.compare(gameVersion, "1.19") >= 0) {
|
||||
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()
|
||||
&& (useNativeGLFWorSDL || useNativeOpenAL) && gameVersion.compareTo("1.19") >= 0) {
|
||||
|
||||
manifest = manifest.withLibraries(manifest.getLibraries().stream()
|
||||
.filter(library -> {
|
||||
if (library.classifier() != null && library.classifier().startsWith("natives")
|
||||
&& "org.lwjgl".equals(library.groupId())) {
|
||||
if ((useNativeGLFW && "lwjgl-glfw".equals(library.artifactId()))
|
||||
if ((useNativeGLFWorSDL && ("lwjgl-glfw".equals(library.artifactId()) || library.artifactId().contains("sdl")))
|
||||
|| (useNativeOpenAL && "lwjgl-openal".equals(library.artifactId()))) {
|
||||
LOG.info("Filter out " + library.name());
|
||||
return false;
|
||||
@@ -122,7 +123,6 @@ public final class NativePatcher {
|
||||
|
||||
OperatingSystem os = javaVersion.getPlatform().getOperatingSystem();
|
||||
Architecture arch = javaVersion.getArchitecture();
|
||||
GameVersionNumber gameVersionNumber = gameVersion != null ? GameVersionNumber.asGameVersion(gameVersion) : null;
|
||||
|
||||
if (settings.getInheritable(GameSettings::notPatchNativesProperty))
|
||||
return manifest;
|
||||
@@ -131,8 +131,7 @@ public final class NativePatcher {
|
||||
return manifest;
|
||||
|
||||
if (arch == Architecture.ARM64 && (os == OperatingSystem.MACOS || os == OperatingSystem.WINDOWS)
|
||||
&& gameVersionNumber != null
|
||||
&& gameVersionNumber.compareTo("1.19") >= 0)
|
||||
&& gameVersion.compareTo("1.19") >= 0)
|
||||
return manifest;
|
||||
|
||||
Map<String, Library> replacements = getNatives(javaVersion.getPlatform());
|
||||
@@ -172,7 +171,7 @@ public final class NativePatcher {
|
||||
}
|
||||
|
||||
if (lwjglVersionChanged) {
|
||||
ModManager modManager = repository.getModManager(manifest.id());
|
||||
ModManager modManager = instance.getModManager();
|
||||
try {
|
||||
for (LocalModFile mod : modManager.getLocalFiles()) {
|
||||
if ("sodium".equals(mod.getId())) {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.util.i18n;
|
||||
|
||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||
import org.jackhuang.hmcl.download.ComponentRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
|
||||
import org.jackhuang.hmcl.util.i18n.translator.Translator;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
@@ -77,7 +77,7 @@ public final class I18n {
|
||||
return getTranslator().formatSpeed(bytes);
|
||||
}
|
||||
|
||||
public static String getDisplayVersion(RemoteVersion version) {
|
||||
public static String getDisplayVersion(ComponentRemoteVersion version) {
|
||||
return getTranslator().getDisplayVersion(version);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.util.i18n.translator;
|
||||
|
||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||
import org.jackhuang.hmcl.download.ComponentRemoteVersion;
|
||||
import org.jackhuang.hmcl.util.i18n.SupportedLocale;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
|
||||
@@ -44,7 +44,7 @@ public class Translator {
|
||||
return displayLocale;
|
||||
}
|
||||
|
||||
public String getDisplayVersion(RemoteVersion remoteVersion) {
|
||||
public String getDisplayVersion(ComponentRemoteVersion remoteVersion) {
|
||||
return remoteVersion.getSelfVersion();
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.util.i18n.translator;
|
||||
|
||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||
import org.jackhuang.hmcl.download.ComponentRemoteVersion;
|
||||
import org.jackhuang.hmcl.util.i18n.SupportedLocale;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -73,7 +73,7 @@ public class Translator_en_Qabs extends Translator {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayVersion(RemoteVersion remoteVersion) {
|
||||
public String getDisplayVersion(ComponentRemoteVersion remoteVersion) {
|
||||
return translate(remoteVersion.getSelfVersion());
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.util.i18n.translator;
|
||||
|
||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||
import org.jackhuang.hmcl.download.ComponentRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
|
||||
import org.jackhuang.hmcl.util.i18n.SupportedLocale;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
@@ -221,7 +221,7 @@ public class Translator_lzh extends Translator {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayVersion(RemoteVersion remoteVersion) {
|
||||
public String getDisplayVersion(ComponentRemoteVersion remoteVersion) {
|
||||
if (remoteVersion instanceof GameRemoteVersion)
|
||||
return translateGameVersion(GameVersionNumber.asGameVersion(remoteVersion.getSelfVersion()));
|
||||
else
|
||||
|
||||
@@ -57,7 +57,7 @@ fi
|
||||
if [ -n "${HMCL_JAVA_OPTS+x}" ]; then
|
||||
_HMCL_VM_OPTIONS=${HMCL_JAVA_OPTS}
|
||||
else
|
||||
_HMCL_VM_OPTIONS="-XX:MinHeapFreeRatio=5 -XX:MaxHeapFreeRatio=15"
|
||||
_HMCL_VM_OPTIONS="-XX:MinHeapFreeRatio=5 -XX:MaxHeapFreeRatio=15 -XX:G1PeriodicGCInterval=900000 -XX:G1PeriodicGCSystemLoadThreshold=0.6 -XX:+UseStringDeduplication"
|
||||
fi
|
||||
|
||||
function show_warning_console() {
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
-monet-surface-tint: #4858AB;
|
||||
-monet-primary-seed: #5C6BC0;
|
||||
-monet-primary-transparent-50: #4352A580;
|
||||
-monet-primary-transparent-80: #4352A5CC;
|
||||
-monet-secondary-container-transparent-50: #D0D5FD80;
|
||||
-monet-surface-transparent-50: #FBF8FF80;
|
||||
-monet-surface-transparent-80: #FBF8FFCC;
|
||||
|
||||
@@ -70,15 +70,6 @@
|
||||
-fx-fill: -monet-on-primary-fixed;
|
||||
}
|
||||
|
||||
.hint.success {
|
||||
-fx-background-color: #d1e7dd;
|
||||
-fx-border-color: -monet-outline;
|
||||
}
|
||||
|
||||
.hint.success Text, .hint.success .svg {
|
||||
-fx-fill: #0a3622;
|
||||
}
|
||||
|
||||
.hint.error {
|
||||
-fx-background-color: -monet-error-container;
|
||||
-fx-border-color: -monet-outline;
|
||||
@@ -480,7 +471,7 @@
|
||||
-fx-padding: 0;
|
||||
}
|
||||
|
||||
.launch-pane > .jfx-button, .jfx-button * {
|
||||
.launch-pane > .launch-button .label {
|
||||
-fx-text-fill: -monet-on-primary-container;
|
||||
-fx-font-size: 14px;
|
||||
}
|
||||
@@ -1237,69 +1228,14 @@
|
||||
-fx-font-size: 14.0px;
|
||||
}
|
||||
|
||||
/* unused */
|
||||
.toggle-icon1 .icon {
|
||||
-fx-fill: #4285F4;
|
||||
-fx-padding: 20.0;
|
||||
}
|
||||
|
||||
.toggle-icon1 {
|
||||
-fx-pref-width: 80px;
|
||||
-fx-background-radius: 80px;
|
||||
-fx-pref-height: 80px;
|
||||
-fx-background-color: transparent;
|
||||
-jfx-toggle-color: rgba(66.0, 133.0, 244.0, 0.29885056614875793);
|
||||
-jfx-untoggle-color: transparent;
|
||||
}
|
||||
|
||||
.toggle-icon1 .jfx-rippler {
|
||||
-jfx-rippler-fill: rgba(66.0, 133.0, 244.0, 0.29885056614875793);
|
||||
-jfx-mask-type: CIRCLE;
|
||||
}
|
||||
|
||||
.toggle-icon2, .toggle-icon3, .jfx-decorator-button {
|
||||
.jfx-decorator-button {
|
||||
-fx-pref-width: 50px;
|
||||
-fx-background-radius: 50px;
|
||||
-fx-pref-height: 50px;
|
||||
-fx-background-color: transparent;
|
||||
}
|
||||
|
||||
/* unused */
|
||||
.toggle-icon2 .icon {
|
||||
-fx-fill: RED;
|
||||
}
|
||||
|
||||
.toggle-icon2 .jfx-rippler {
|
||||
-jfx-rippler-fill: RED;
|
||||
-jfx-mask-type: CIRCLE;
|
||||
}
|
||||
|
||||
/* unused */
|
||||
.toggle-icon3 {
|
||||
-fx-toggle-icon4-size: 35px;
|
||||
-fx-pref-width: -fx-toggle-icon4-size;
|
||||
-fx-max-width: -fx-toggle-icon4-size;
|
||||
-fx-min-width: -fx-toggle-icon4-size;
|
||||
-fx-pref-height: -fx-toggle-icon4-size;
|
||||
-fx-max-height: -fx-toggle-icon4-size;
|
||||
-fx-min-height: -fx-toggle-icon4-size;
|
||||
-fx-background-radius: 50px;
|
||||
-fx-background-color: transparent;
|
||||
-jfx-toggle-color: white;
|
||||
-jfx-untoggle-color: transparent;
|
||||
}
|
||||
|
||||
.toggle-icon3 .icon {
|
||||
-fx-fill: rgb(204.0, 204.0, 51.0);
|
||||
-fx-padding: 10.0;
|
||||
}
|
||||
|
||||
.toggle-icon3 .jfx-rippler {
|
||||
-jfx-rippler-fill: white;
|
||||
-jfx-mask-type: CIRCLE;
|
||||
}
|
||||
|
||||
/* used */
|
||||
.toggle-icon4 {
|
||||
-fx-toggle-icon4-size: 30px;
|
||||
-fx-pref-width: -fx-toggle-icon4-size;
|
||||
@@ -2003,7 +1939,7 @@
|
||||
}
|
||||
|
||||
.line-select-button .menu-container:selected .subtitle-label {
|
||||
-fx-text-fill: -monet-primary-container;
|
||||
-fx-text-fill: -monet-primary-transparent-80;
|
||||
}
|
||||
|
||||
.line-toggle-button .jfx-toggle-button {
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
about=About
|
||||
about.copyright=Copyright
|
||||
about.copyright.statement=Copyright © 2013-2026 huangyuhui and contributors.
|
||||
about.author=Author
|
||||
about.author.statement=bilibili @huanghongxun
|
||||
about.claim=EULA
|
||||
about.claim.statement=Click this link for full text.
|
||||
@@ -80,7 +79,6 @@ account.failed.ssl=An SSL error occurred while connecting to the server. Please
|
||||
account.failed.dns=An SSL error occurred while connecting to the server. DNS resolution may be incorrect. Please try changing your DNS server or using a proxy service.
|
||||
account.failed.wrong_account=You have logged in to the wrong account.
|
||||
account.storage.read_only=The account file was saved by a newer version of HMCL. The current version of HMCL cannot modify accounts.
|
||||
account.hmcl.hint=You need to click "Log in" and complete the process in the opened browser window.
|
||||
account.injector.add=New Auth Server
|
||||
account.injector.empty=None (You can click "+" to add one)
|
||||
account.injector.http=Warning: This server uses the unsafe HTTP protocol. Anyone intercepting your connection would be able to see your credentials in plaintext.
|
||||
@@ -97,19 +95,15 @@ account.login.retry=Retry
|
||||
account.login.refresh=Log in again
|
||||
account.login.refresh.microsoft.hint=You need to log in to your Microsoft account again because the account authorization is invalid.
|
||||
account.login.restricted=Sign in to your Microsoft account to enable this feature
|
||||
account.logout=Logout
|
||||
account.register=Register
|
||||
account.manage=Account List
|
||||
account.copy_uuid=Copy UUID of the Account
|
||||
account.methods=Login Type
|
||||
account.methods.authlib_injector=authlib-injector
|
||||
account.methods.microsoft=Microsoft
|
||||
account.methods.microsoft.birth=How to Change Your Account Birth Date
|
||||
account.methods.microsoft.code=Code (Copied to Clipboard)
|
||||
account.methods.microsoft.close_page=Microsoft account authorization is now completed.\n\
|
||||
\n\
|
||||
There are some extra work for us, but you can safely close this tab for now.
|
||||
account.methods.microsoft.deauthorize=Deauthorize
|
||||
account.methods.microsoft.error.add_family=Please click <a href="https://support.microsoft.com/account-billing/837badbc-999e-54d2-2617-d19206b9540a">here</a> to change your account birth date to be over 18 years old, or add your account to a family.
|
||||
account.methods.microsoft.error.country_unavailable=XBOX Live is not available in your current country/region.
|
||||
account.methods.microsoft.error.missing_xbox_account=Please confirm that you have purchased Minecraft: Java Edition. If you have, please click <a href="https://www.minecraft.net/msaprofile/mygames/editprofile">here</a> to create your game profile.
|
||||
@@ -119,8 +113,6 @@ account.methods.microsoft.error.banned=Your account may have been banned by XBOX
|
||||
You can click <a href="https://enforcement.xbox.com/enforcement/showenforcementhistory">here</a> to check the ban status of your account.
|
||||
account.methods.microsoft.error.unknown=Failed to log in, error code: %d.
|
||||
account.methods.microsoft.error.wrong_verify_method=Failed to log in. Please try logging into your account using PASSWORD instead of other login methods.
|
||||
account.methods.microsoft.logging_in=Logging in...
|
||||
account.methods.microsoft.makegameidsettings=Create Profile / Edit Profile Name
|
||||
account.methods.microsoft.hint=Click the "Log in" button to start adding your Microsoft account.
|
||||
account.methods.microsoft.methods.device=Log In with QR Code
|
||||
account.methods.microsoft.methods.device.hint=Scan QR code or visit <a href="%s">%s</a> to complete login, enter <b>%s</b> in the opened page.
|
||||
@@ -132,7 +124,6 @@ account.methods.microsoft.profile=Account Profile
|
||||
account.methods.microsoft.purchase=Buy Minecraft
|
||||
account.methods.microsoft.snapshot=You are using an unofficial build of HMCL. Please download the <a href="https://hmcl.huangyuhui.net/download">official build</a> to log in.
|
||||
account.methods.microsoft.snapshot.tooltip=You are using an unofficial build of HMCL. Please download the official build to refresh the account.
|
||||
account.methods.microsoft.snapshot.website=Official Website
|
||||
account.methods.offline=Offline
|
||||
account.methods.offline.name.special_characters=Use only letters, numbers, and underscores (max 16 chars)
|
||||
account.methods.offline.name.invalid=It is recommended to use only English letters, numbers and underscores for the username, and the length should not exceed 16 characters.\n\
|
||||
@@ -146,12 +137,10 @@ account.methods.offline.uuid.hint=UUID is a unique identifier for Minecraft play
|
||||
\n\
|
||||
This option is for advanced users only. We do not recommend changing this option unless you know what you are doing.
|
||||
account.methods.offline.uuid.malformed=Invalid format.
|
||||
account.methods.ban_query=Ban Query
|
||||
account.missing=No Accounts
|
||||
account.missing.add=Click here to add one.
|
||||
account.move_to_global=Convert to Global Account\nThe account information will be saved in a config file of the current user directory of the system.
|
||||
account.move_to_portable=Convert to Portable Account\nThe account information will be saved in a config file in the same directory as HMCL.
|
||||
account.not_logged_in=Not Logged in
|
||||
account.password=Password
|
||||
account.portable=Portable
|
||||
account.skin=Skin
|
||||
@@ -172,8 +161,6 @@ account.skin.upload.failed=Failed to upload skin.
|
||||
account.skin.invalid_skin=Invalid skin file.
|
||||
account.username=Username
|
||||
|
||||
addon.broken_dependency.title=Broken dependency
|
||||
addon.broken_dependency.desc=This dependency existed before, but it does not exist anymore. Try using another download source.
|
||||
addon.category=Category
|
||||
addon.changelog=Changelog
|
||||
addon.changelog.empty=Currently no changelog
|
||||
@@ -198,6 +185,7 @@ addon.dependency.tool=Required Dependencies (Must be downloaded separately. Miss
|
||||
addon.dependency.include=Built-in Dependencies (Already packaged in the addon file by the author. No need to download separately)
|
||||
addon.dependency.incompatible=Incompatible Addons (Installing these addons at the same time will prevent the game from launching)
|
||||
addon.dependency.broken=Broken Dependencies (This addon existed before, but it does not exist anymore. Try using another download source.)
|
||||
addon.dependencies.has_broken=This version has broken dependencies. Please try another download source, or installation could be unsuccessful.
|
||||
addon.download.recommend=Recommended Version for Minecraft %1s
|
||||
addon.download.title.release=Minecraft %s
|
||||
addon.download.title.snapshot=Minecraft %s (Snapshots)
|
||||
@@ -232,7 +220,6 @@ button.reveal_dir=Reveal
|
||||
button.refresh=Refresh
|
||||
button.remove=Remove
|
||||
button.remove.confirm=Are you sure you want to permanently remove it? This action cannot be undone!
|
||||
button.retry=Retry
|
||||
button.save=Save
|
||||
button.save_as=Save As
|
||||
button.select_all=Select All
|
||||
@@ -397,9 +384,6 @@ download=Download
|
||||
download.code.404=File "%s" not found on the remote server.
|
||||
download.content=Addons
|
||||
download.shader=Shaders
|
||||
download.curseforge.unavailable=This HMCL build does not support access to CurseForge. Please use the official build to access CurseForge.
|
||||
download.existing=The file cannot be saved because it already exists. You can click "Save As" to save the file elsewhere.
|
||||
download.external_link=Visit Download Website
|
||||
download.failed=Failed to download "%1$s", response code: %2$d.
|
||||
download.failed.empty=No versions are available. Please click here to go back.
|
||||
download.failed.no_code=Failed to download
|
||||
@@ -482,10 +466,6 @@ fatal.deprecated_java_version=HMCL will require Java 17 or later to run in the f
|
||||
It is recommended to install the latest version of Java to ensure that HMCL works properly.\n\
|
||||
\n\
|
||||
You can continue to keep the old version of Java. HMCL can recognize and manage multiple Java versions and will automatically select the appropriate Java for you based on the game version.
|
||||
fatal.deprecated_java_version.update=HMCL will require Java 17 or later to run in the future. Please install the latest version of Java to ensure that HMCL can complete the upgrade.\n\
|
||||
\n\
|
||||
You can continue to keep the old version of Java.\
|
||||
HMCL can recognize and manage multiple Java versions and will automatically select the appropriate Java for you based on the game version.
|
||||
fatal.deprecated_java_version.download_link=Download Java %d
|
||||
fatal.samba=If you opened Hello Minecraft! Launcher from a Samba network drive, some features might not be working. Please try updating your Java or moving the launcher to another directory.
|
||||
fatal.illegal_char=Your working directory contains an illegal character "=". You will not be able to use authlib-injector or change the skin of your offline account.
|
||||
@@ -749,12 +729,10 @@ game_directory.default=Current
|
||||
game_directory.home=Minecraft Launcher
|
||||
game_directory.instance_directory=Game Directory
|
||||
game_directory.instance_directory.choose=Choose game directory
|
||||
game_directory.manage=Instance Directory List
|
||||
game_directory.name=Name
|
||||
game_directory.new=New Directory
|
||||
game_directory.title=Game Directories
|
||||
game_directory.selected=Selected
|
||||
game_directory.use_relative_path=Use a relative path for the game directory if possible
|
||||
game_directory.root=Using the disk root directory as the game folder may cause the game to fail to launch and will create multiple files and folders in the disk root directory.\n\nAre you sure you want to continue?
|
||||
|
||||
help=Help
|
||||
help.doc=Hello Minecraft! Launcher Documentation
|
||||
@@ -767,6 +745,7 @@ input.url=The input must be a valid URL.
|
||||
|
||||
install=New Instance
|
||||
install.change_version=Change Version
|
||||
install.change_version.title=Change Version - %1s
|
||||
install.change_version.confirm=Are you sure you want to switch %1$s from version %2$s to %3$s?
|
||||
install.change_version.process=Change Version Process
|
||||
install.failed=Failed to install
|
||||
@@ -775,13 +754,13 @@ install.failed.downloading.detail=Failed to download file: %s
|
||||
install.failed.downloading.timeout=Download timeout when fetching: %s
|
||||
install.failed.install_online=Failed to identify the provided file. If you are installing a mod, navigate to the "Mods" page.
|
||||
install.failed.malformed=The downloaded files are corrupted. You can try resolving this problem by switching to another download source in "Settings → Download → Download Source".
|
||||
install.failed.cleanroom_not_compatible_with_forge=Cleanroom is not compatible with Forge.
|
||||
install.failed.optifine_conflict=Cannot install both OptiFine and Fabric on Minecraft 1.13 or later.
|
||||
install.failed.optifine_forge_1.17=For Minecraft 1.17.1, Forge is only compatible with OptiFine H1 pre2 or later. You can install them by checking "Snapshots" when choosing an OptiFine version in HMCL.
|
||||
install.failed.version_mismatch=This loader requires the game version %1$s, but the installed one is %2$s.
|
||||
install.installer.change_version=%s Incompatible
|
||||
install.installer.choose=Choose Your %s Version
|
||||
install.installer.cleanroom=Cleanroom
|
||||
install.installer.depend=Requires %s
|
||||
install.installer.do_not_install=Do not install
|
||||
install.installer.fabric=Fabric
|
||||
install.installer.fabric-api=Fabric API
|
||||
@@ -794,9 +773,6 @@ install.installer.game=Minecraft
|
||||
install.installer.incompatible=Incompatible with %s
|
||||
install.installer.install=Installing %s
|
||||
install.installer.install_offline=Install/Update from Local File
|
||||
install.installer.install_offline.tooltip=We support using the local (Neo)Forge, Cleanroom, and OptiFine installer.
|
||||
install.installer.install_online=Online Install
|
||||
install.installer.install_online.tooltip=We currently support Forge, NeoForge, Cleanroom, OptiFine, Fabric, Legacy Fabric, Quilt, and LiteLoader.
|
||||
install.installer.liteloader=LiteLoader
|
||||
install.installer.not_installed=Not installed
|
||||
install.installer.optifine=OptiFine
|
||||
@@ -810,11 +786,9 @@ install.modpack.installation=Modpack Installation
|
||||
install.name.invalid=The name contains special characters (such as emoji or CJK characters).\nIt is recommended to change the name to include only English letters, numbers, and underscores to avoid potential issues when launching the game.\nDo you want to proceed with the installation?
|
||||
install.new_game=Install Instance
|
||||
install.new_game.already_exists=This instance name already exists. Please use another name.
|
||||
install.new_game.current_game_version=Current Instance Version
|
||||
install.new_game.installation=Instance Installation
|
||||
install.new_game.malformed=Invalid name.
|
||||
install.new_game.malformed_json=Invalid Minecraft instance JSON file.
|
||||
install.select=Choose operation
|
||||
install.success=Successfully installed.
|
||||
|
||||
instance=Games
|
||||
@@ -908,16 +882,14 @@ launch.advice.multi=The following problems were detected:\n\n%s\n\nThese problem
|
||||
launch.advice.java.auto=The current Java version is not compatible with the instance.\n\nClick "Yes" to automatically choose the most compatible Java version. Or, you can navigate to "Global/Instance-specific Settings → Java" to choose one yourself.
|
||||
launch.advice.java.modded_java_7=Minecraft 1.7.2 and previous versions require Java 7 or earlier.
|
||||
launch.advice.cleanroom=Cleanroom %2$s can only be run on Java %1$s or later. Please use Java %1$s or later versions.
|
||||
launch.advice.corrected=We have resolved the Java problem. If you still want to use your choice of Java version, you can disable "Do not check JVM compatibility" in "Global/Instance-specific Settings → Advanced Settings".
|
||||
launch.advice.uncorrected=If you still want to use your choice of Java version, you can disable "Do not check JVM compatibility" in "Global/Instance-specific Settings → Advanced Settings".
|
||||
launch.advice.different_platform=The 64-bit Java version is recommended for your device, but you have installed a 32-bit one.
|
||||
launch.advice.forge2760_liteloader=Forge version 2760 is not compatible with LiteLoader. Please consider upgrading Forge to version 2773 or later.
|
||||
launch.advice.forge28_2_2_optifine=Forge version 28.2.2 or later is not compatible with OptiFine. Please consider downgrading Forge to version 28.2.1 or earlier.
|
||||
launch.advice.forge37_0_60=Forge versions prior to 37.0.60 are not compatible with Java 17. Please update Forge to 37.0.60 or later, or launch the game with Java 16.
|
||||
launch.advice.java8_1_13=Minecraft 1.13 and later can only be run on Java 8 or later. Please use Java 8 or later versions.
|
||||
launch.advice.java8_51_1_13=Minecraft 1.13 may crash on Java 8 versions prior to 1.8.0_51. Please install the latest Java 8 version.
|
||||
launch.advice.java9=You cannot launch Minecraft 1.12 or earlier with Java 9 or later. Please use Java 8 instead.
|
||||
launch.advice.lwjgl_3_4_1=The current game version has a known issue that may cause stuttering in the game.\nHMCL can automatically optimize the game performance (using a Java Agent).\nDo you allow HMCL to automatically optimize the game?\nIf you encounter issues, you can disable the "Allow HMCL to modify the game" option in "Settings → General".
|
||||
launch.advice.lwjgl_3_4_1=The current game version has a known issue that may cause stuttering in the game.\nHMCL can automatically optimize the game performance (using a Java Agent).\nDo you allow HMCL to automatically optimize the game?\nIf you encounter issues, you can disable the "Allow HMCL to modify the game" option in instance settings.
|
||||
launch.advice.lwjgl_3_4_1.title=Allow HMCL to automatically optimize the game?
|
||||
launch.advice.modded_java=Some mods may not be compatible with newer Java versions. It is recommended to use Java %1$s to launch Minecraft %2$s.
|
||||
launch.advice.modlauncher8=The Forge version you are using is not compatible with the current Java version. Please try updating Forge.
|
||||
@@ -972,8 +944,6 @@ launcher.background.load_policy.wait_for_background=Wait for Background
|
||||
launcher.background.loading=Background Loading
|
||||
launcher.background.network=From URL
|
||||
launcher.background.network.cache=Cache URL Images
|
||||
launcher.background.network.cache.disabled=Disabled
|
||||
launcher.background.network.cache.enabled=Enabled
|
||||
launcher.background.paint=Solid Color
|
||||
launcher.background.theme=Follow Theme
|
||||
launcher.background.theme_color=Follow Theme Color
|
||||
@@ -991,7 +961,6 @@ launcher.update_java=Please update your Java version.
|
||||
|
||||
libraries.download=Downloading Libraries
|
||||
|
||||
login.empty_username=You have not set your username yet!
|
||||
login.enter_password=Please enter your password.
|
||||
|
||||
logwindow.always_on_top=Always On Top
|
||||
@@ -1005,7 +974,6 @@ logwindow.export_game_crash_logs=Export Crash Logs
|
||||
logwindow.export_dump=Export Game Stack Dump
|
||||
logwindow.export_dump.no_dependency=Your Java does not contain the dependencies to create the stack dump. Please join our Discord or QQ group for help.
|
||||
|
||||
main_page=Home
|
||||
|
||||
message.cancelled=Operation Canceled
|
||||
message.confirm=Confirm
|
||||
@@ -1035,7 +1003,6 @@ modpack.desc=Describe your modpack, including an introduction and probably some
|
||||
modpack.description=Modpack Description
|
||||
modpack.download=Download Modpacks
|
||||
modpack.download.title=Download Modpack - %1s
|
||||
modpack.enter_name=Enter a name for this modpack.
|
||||
modpack.export=Export as Modpack
|
||||
modpack.export.as=Export Modpack As...
|
||||
modpack.file_api=Modpack URL Prefix
|
||||
@@ -1056,11 +1023,9 @@ modpack.files.scripts=MineTweaker Configuration File
|
||||
modpack.files.servers_dat=Server List File
|
||||
modpack.installing=Installing modpack
|
||||
modpack.installing.given=Installing %s modpack
|
||||
modpack.introduction=Curse, Modrinth, MultiMC, and MCBBS modpacks are currently supported.
|
||||
modpack.invalid=Invalid modpack, you can try downloading it again.
|
||||
modpack.mismatched_type=Modpack type mismatched, the current instance is a(n) %1$s type, but the provided one is %2$s type.
|
||||
modpack.name=Modpack Name
|
||||
modpack.not_a_valid_name=Invalid modpack name.
|
||||
modpack.origin=Source
|
||||
modpack.origin.url=Official Website
|
||||
modpack.origin.mcbbs=MCBBS
|
||||
@@ -1102,6 +1067,7 @@ modpack.wizard.step.initialization.warning=Before making a modpack, please make
|
||||
Keep in mind that you are not allowed to add mods and resource packs that explicitly state they could not to be distributed or put in a modpack.
|
||||
modpack.wizard.step.initialization.server=Click here for more information on how to make a server modpack that can be automatically updated.
|
||||
|
||||
# Translation primarily from modrinth.
|
||||
modrinth.category.adventure=Adventure
|
||||
modrinth.category.atmosphere=Atmosphere
|
||||
modrinth.category.audio=Audio
|
||||
@@ -1137,7 +1103,7 @@ modrinth.category.high=High
|
||||
modrinth.category.iris=Iris
|
||||
modrinth.category.items=Items
|
||||
modrinth.category.java-agent=Java Agent
|
||||
modrinth.category.kitchen-sink=Kitchen-Sink
|
||||
modrinth.category.kitchen-sink=Kitchen Sink
|
||||
modrinth.category.legacy-fabric=Legacy Fabric
|
||||
modrinth.category.library=Library
|
||||
modrinth.category.lightweight=Lightweight
|
||||
@@ -1153,7 +1119,7 @@ modrinth.category.misc=Misc
|
||||
modrinth.category.mobs=Mobs
|
||||
modrinth.category.modded=Modded
|
||||
modrinth.category.models=Models
|
||||
modrinth.category.modloader=Modloader
|
||||
modrinth.category.modloader=Risugami's ModLoader
|
||||
modrinth.category.multiplayer=Multiplayer
|
||||
modrinth.category.neoforge=NeoForge
|
||||
modrinth.category.nilloader=NilLoader
|
||||
@@ -1183,13 +1149,12 @@ modrinth.category.themed=Themed
|
||||
modrinth.category.transportation=Transportation
|
||||
modrinth.category.tweaks=Tweaks
|
||||
modrinth.category.utility=Utility
|
||||
modrinth.category.vanilla=Vanilla
|
||||
modrinth.category.vanilla=Vanilla Shader
|
||||
modrinth.category.vanilla-like=Vanilla-like
|
||||
modrinth.category.velocity=Velocity
|
||||
modrinth.category.waterfall=Waterfall
|
||||
modrinth.category.worldgen=Worldgen
|
||||
modrinth.category.8x-=8x or lower
|
||||
modrinth.category.16x=16x
|
||||
modrinth.category.32x=32x
|
||||
modrinth.category.48x=48x
|
||||
modrinth.category.64x=64x
|
||||
@@ -1214,7 +1179,6 @@ mods.mcmod=MCMod
|
||||
mods.mcmod.page=MCMod Page
|
||||
mods.mcmod.search=Search in MCMod
|
||||
mods.name=Name
|
||||
mods.not_modded=You must install a modloader (Forge, NeoForge, Fabric, Legacy Fabric, Quilt, or LiteLoader) first to manage your mods!
|
||||
mods.restore=Restore
|
||||
mods.url=Official Page
|
||||
mods.update_modpack_mod.warning=Updating mods in a modpack can lead to irreparable results, possibly corrupting the modpack so that it cannot launch. Are you sure you want to update?
|
||||
@@ -1268,7 +1232,6 @@ world.chunkbase.nether_fortress=Nether Fortress
|
||||
world.duplicate=Duplicate the World
|
||||
world.duplicate.prompt=Please enter the name of the duplicated world
|
||||
world.duplicate.failed.already_exists=Directory already exists
|
||||
world.duplicate.failed.empty_name=Name cannot be empty
|
||||
world.duplicate.failed.invalid_name=Name contains invalid characters
|
||||
world.duplicate.failed=Failed to duplicate the world
|
||||
world.duplicate.success.toast=Successfully duplicated the world
|
||||
@@ -1374,7 +1337,6 @@ schematics.create_directory=New Directory
|
||||
schematics.create_directory.prompt=Please enter the new directory name
|
||||
schematics.create_directory.failed=Failed to create directory
|
||||
schematics.create_directory.failed.already_exists=Directory already exists
|
||||
schematics.create_directory.failed.empty_name=Name cannot be empty
|
||||
schematics.create_directory.failed.invalid_name=Name contains invalid characters
|
||||
schematics.empty=No Schematics
|
||||
schematics.info.description=Description
|
||||
@@ -1413,20 +1375,6 @@ settings.advanced=Advanced Settings
|
||||
settings.advanced.modify=Edit Advanced Settings
|
||||
settings.advanced.title=Advanced Settings - %s
|
||||
settings.advanced.custom_commands=Custom Commands
|
||||
settings.advanced.custom_commands.hint=The following environment variables are provided:\n\
|
||||
\ · $INST_NAME: instance name.\n\
|
||||
\ · $INST_ID: instance name.\n\
|
||||
\ · $INST_DIR: absolute path of the instance working directory.\n\
|
||||
\ · $INST_MC_DIR: absolute path of the game directory.\n\
|
||||
\ · $INST_JAVA: java binary used for launch.\n\
|
||||
\ · $INST_FORGE: set if Forge is installed.\n\
|
||||
\ · $INST_NEOFORGE: set if NeoForge is installed.\n\
|
||||
\ · $INST_CLEANROOM: set if Cleanroom is installed.\n\
|
||||
\ · $INST_LITELOADER: set if LiteLoader is installed.\n\
|
||||
\ · $INST_OPTIFINE: set if OptiFine is installed.\n\
|
||||
\ · $INST_FABRIC: set if Fabric is installed.\n\
|
||||
\ · $INST_LEGACYFABRIC: set if Legacy Fabric is installed.\n\
|
||||
\ · $INST_QUILT: set if Quilt is installed.
|
||||
settings.advanced.dont_check_game_completeness=Do not check game integrity
|
||||
settings.advanced.dont_check_jvm_validity=Do not check JVM compatibility
|
||||
settings.advanced.dont_patch_natives=Do not attempt to automatically replace native libraries
|
||||
@@ -1462,14 +1410,9 @@ settings.advanced.launcher_visibility.keep=Keep the launcher visible
|
||||
settings.advanced.launcher_visible=Launcher Visibility
|
||||
settings.advanced.minecraft_arguments=Launch Arguments
|
||||
settings.advanced.minecraft_arguments.prompt=Default
|
||||
settings.advanced.natives=Native Libraries
|
||||
settings.advanced.natives_settings=Native Library Settings
|
||||
settings.advanced.natives_directory=Native Library Path
|
||||
settings.advanced.natives_directory.choose=Choose the location of the desired native library
|
||||
settings.advanced.natives_directory.custom=Custom
|
||||
settings.advanced.natives_directory.custom.enabled=Use Custom Native Libraries
|
||||
settings.advanced.natives_directory.default=Default
|
||||
settings.advanced.natives_directory.default.version_id=<Instance Name>
|
||||
settings.advanced.no_jvm_args=Do not add default JVM arguments
|
||||
settings.advanced.no_optimizing_jvm_args=Do not add default JVM optimization arguments
|
||||
settings.advanced.precall_command=Pre-launch Command
|
||||
@@ -1536,12 +1479,11 @@ settings.advanced.renderer.zink.desc=OpenGL renderer based on Vulkan
|
||||
settings.advanced.server_ip=Server Address
|
||||
settings.advanced.server_ip.prompt=Automatically join after launching the game
|
||||
settings.advanced.unsupported_system_options=Settings not applicable to the current system
|
||||
settings.advanced.use_native_glfw=Use System GLFW
|
||||
settings.advanced.use_native_glfw_or_sdl=Use System GLFW / SDL
|
||||
settings.advanced.use_native_openal=Use System OpenAL
|
||||
settings.advanced.use_native_sdl=Use System SDL
|
||||
settings.advanced.linux_freebsd_only=Linux/FreeBSD Only
|
||||
settings.advanced.windows_only=Windows Only
|
||||
settings.advanced.workaround=Workaround
|
||||
settings.advanced.workaround.warning=Workaround options are intended only for advanced users. Tweaking with these options may crash the game. Unless you know what you are doing, please do not edit these options.
|
||||
settings.advanced.wrapper_launcher=Wrapper Command
|
||||
settings.advanced.wrapper_launcher.prompt=Allows launching using an extra wrapper program like "optirun"
|
||||
|
||||
@@ -1588,11 +1530,7 @@ settings.game.window_type=Game Window Type
|
||||
settings.game.window_type.fullscreen=Fullscreen
|
||||
settings.game.window_type.maximized=Maximized
|
||||
settings.game.window_type.windowed=Windowed
|
||||
settings.game.working_directory=Working Directory
|
||||
settings.game.working_directory.choose=Choose the working directory
|
||||
settings.game.working_directory.hint=Enable the "Isolated" option in "Working Directory" to allow the current instance to store its settings, worlds, and mods in a separate directory.\n\
|
||||
\n\
|
||||
It is recommended to enable this option to avoid mod conflicts, but you will need to move your worlds manually.
|
||||
|
||||
settings.icon=Icon
|
||||
settings.game_directories.read_only=The game directory file was saved by a newer version of HMCL. The current version of HMCL cannot modify game directories.
|
||||
@@ -1606,7 +1544,6 @@ settings.launcher.brightness=Theme Mode
|
||||
settings.launcher.brightness.auto=Follow System Settings
|
||||
settings.launcher.brightness.dark=Dark Mode
|
||||
settings.launcher.brightness.light=Light Mode
|
||||
settings.launcher.common_path.tooltip=HMCL will put all game assets and dependencies here. If there are existing libraries in the game directory, then HMCL will prefer to use them first.
|
||||
settings.launcher.debug=Debug
|
||||
settings.launcher.disable_april_fools=Do not enable April Fools features
|
||||
settings.launcher.disable_auto_game_options=Do not switch game language
|
||||
@@ -1614,11 +1551,9 @@ settings.launcher.download=Download
|
||||
settings.launcher.download.threads=Download Threads
|
||||
settings.launcher.download.threads.auto=Automatically Determine
|
||||
settings.launcher.download.threads.custom=Custom
|
||||
settings.launcher.download.threads.hint=Too many threads may cause your system to freeze, and your download speed may be affected by your ISP and download servers. It is not always the case that more threads increase your download speed.
|
||||
settings.launcher.download_source=Download Source
|
||||
settings.launcher.download_source.auto=Automatically Choose Download Sources
|
||||
settings.launcher.default_addon_source=Default Addon Source
|
||||
settings.launcher.enable_game_list=Show instance list in homepage
|
||||
settings.launcher.misc=Misc
|
||||
settings.launcher.font=Launcher Font
|
||||
settings.launcher.font.anti_aliasing=Anti-aliasing
|
||||
@@ -1709,9 +1644,6 @@ settings.type.global.preset.auto_name=Preset %d
|
||||
|
||||
shaderpack.download.title=Download Shader - %1s
|
||||
|
||||
sponsor=Donors
|
||||
sponsor.bmclapi=Downloads for the Chinese Mainland are provided by BMCLAPI. Click here for more information.
|
||||
sponsor.hmcl=Hello Minecraft! Launcher is a FOSS Minecraft launcher that allows users to manage multiple Minecraft instances easily. Click here for more information.
|
||||
|
||||
system.architecture=Architecture
|
||||
system.operating_system=Operating System
|
||||
@@ -1863,7 +1795,6 @@ update.bubble.subtitle=Click here to update
|
||||
update.note.stable=Suitable for users who prioritize software stability.\nNew features are added to the stable version only after thorough testing.
|
||||
update.note.dev=Suitable for users who want to experience new features first.\nThe development version includes the latest features and bug fixes,\nbut may also have more issues due to insufficient testing.
|
||||
update.latest=This is the latest version
|
||||
update.no_browser=Cannot open in system browser. But we copied the link to your clipboard, and you can open it manually.
|
||||
update.tooltip=Update
|
||||
update.preview=Preview HMCL releases early
|
||||
update.preview.subtitle=Enable this option to receive new versions of HMCL early for testing before their official release.
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
about=حول
|
||||
about.copyright=حقوق النشر
|
||||
about.copyright.statement=حقوق النشر © 2013-2026 huangyuhui والمساهمون.
|
||||
about.author=المطوّر
|
||||
about.author.statement=bilibili @huanghongxun
|
||||
about.claim=اتفاقية الترخيص
|
||||
about.claim.statement=انقر على هذا الرابط للاطلاع على النص الكامل.
|
||||
@@ -77,7 +76,6 @@ account.failed.server_response_malformed=استجابة الخادم غير صا
|
||||
account.failed.ssl=حدث خطأ SSL أثناء الاتصال بالخادم. يُرجى محاولة تحديث Java.
|
||||
account.failed.dns=حدث خطأ SSL أثناء الاتصال بالخادم. قد يكون تحليل DNS غير صحيح. يُرجى تغيير خادم DNS الخاص بك أو استخدام خدمة وكيل.
|
||||
account.failed.wrong_account=لقد سجّلت الدخول إلى حساب خاطئ.
|
||||
account.hmcl.hint=تحتاج إلى النقر على "تسجيل الدخول" وإتمام العملية في نافذة المتصفح المفتوحة.
|
||||
account.injector.add=خادم مصادقة جديد
|
||||
account.injector.empty=لا يوجد (يمكنك النقر على "+" لإضافة واحد)
|
||||
account.injector.http=تحذير: يستخدم هذا الخادم بروتوكول HTTP غير الآمن. أي شخص يعترض اتصالك سيتمكن من رؤية بياناتك بنص واضح.
|
||||
@@ -93,19 +91,15 @@ account.login.retry=إعادة المحاولة
|
||||
account.login.refresh=تسجيل الدخول مجدداً
|
||||
account.login.refresh.microsoft.hint=تحتاج إلى تسجيل الدخول إلى حساب Microsoft مجدداً لأن تفويض الحساب انتهت صلاحيته.
|
||||
account.login.restricted=سجّل الدخول إلى حساب Microsoft لتفعيل هذه الميزة
|
||||
account.logout=تسجيل الخروج
|
||||
account.register=إنشاء حساب
|
||||
account.manage=قائمة الحسابات
|
||||
account.copy_uuid=نسخ UUID الحساب
|
||||
account.methods=نوع تسجيل الدخول
|
||||
account.methods.authlib_injector=authlib-injector
|
||||
account.methods.microsoft=Microsoft
|
||||
account.methods.microsoft.birth=كيفية تغيير تاريخ ميلاد حسابك
|
||||
account.methods.microsoft.code=الرمز (تم النسخ إلى الحافظة)
|
||||
account.methods.microsoft.close_page=اكتمل تفويض حساب Microsoft.\n\
|
||||
\n\
|
||||
لا يزال هناك بعض الخطوات من جانبنا، لكن يمكنك إغلاق هذا التبويب بأمان الآن.
|
||||
account.methods.microsoft.deauthorize=إلغاء التفويض
|
||||
account.methods.microsoft.error.add_family=يُرجى النقر <a href="https://support.microsoft.com/account-billing/837badbc-999e-54d2-2617-d19206b9540a">هنا</a> لتغيير تاريخ ميلاد حسابك ليكون فوق 18 عاماً، أو إضافة حسابك إلى عائلة.
|
||||
account.methods.microsoft.error.country_unavailable=XBOX Live غير متاح في بلدك/منطقتك الحالية.
|
||||
account.methods.microsoft.error.missing_xbox_account=لا يوجد حساب XBOX مرتبط بحساب Microsoft الخاص بك بعد. يُرجى النقر <a href="https://www.minecraft.net/msaprofile/mygames/editprofile">هنا</a> لربط واحد.
|
||||
@@ -115,8 +109,6 @@ account.methods.microsoft.error.banned=قد يكون حسابك محظوراً
|
||||
يمكنك النقر <a href="https://enforcement.xbox.com/enforcement/showenforcementhistory">هنا</a> للتحقق من حالة الحظر لحسابك.
|
||||
account.methods.microsoft.error.unknown=فشل تسجيل الدخول، رمز الخطأ: %d.
|
||||
account.methods.microsoft.error.wrong_verify_method=فشل تسجيل الدخول. يُرجى محاولة تسجيل الدخول إلى حسابك باستخدام كلمة المرور بدلاً من طرق الدخول الأخرى.
|
||||
account.methods.microsoft.logging_in=جارٍ تسجيل الدخول...
|
||||
account.methods.microsoft.makegameidsettings=إنشاء ملف اللعبة / تعديل اسم الملف
|
||||
account.methods.microsoft.hint=انقر على زر "تسجيل الدخول" لبدء إضافة حساب Microsoft الخاص بك.
|
||||
account.methods.microsoft.methods.device=تسجيل الدخول برمز QR
|
||||
account.methods.microsoft.methods.device.hint=امسح رمز QR أو زر <a href="%s">%s</a> لإتمام تسجيل الدخول، وأدخل <b>%s</b> في الصفحة المفتوحة.
|
||||
@@ -127,7 +119,6 @@ account.methods.microsoft.profile=ملف الحساب
|
||||
account.methods.microsoft.purchase=شراء Minecraft
|
||||
account.methods.microsoft.snapshot=أنت تستخدم إصداراً غير رسمي من HMCL. يُرجى تنزيل <a href="https://hmcl.huangyuhui.net/download">الإصدار الرسمي</a> لتسجيل الدخول.
|
||||
account.methods.microsoft.snapshot.tooltip=أنت تستخدم إصداراً غير رسمي من HMCL. يُرجى تنزيل الإصدار الرسمي لتحديث الحساب.
|
||||
account.methods.microsoft.snapshot.website=الموقع الرسمي
|
||||
account.methods.offline=غير متصل
|
||||
account.methods.offline.name.special_characters=استخدم الحروف والأرقام والشرطات السفلية فقط (16 حرفاً كحد أقصى)
|
||||
account.methods.offline.name.invalid=يُنصح باستخدام الحروف الإنجليزية والأرقام والشرطات السفلية فقط في اسم المستخدم، على ألا يتجاوز الطول 16 حرفاً.\n\
|
||||
@@ -141,12 +132,10 @@ account.methods.offline.uuid.hint=UUID هو معرّف فريد للاعبي Min
|
||||
\n\
|
||||
هذا الخيار مخصص للمستخدمين المتقدمين فقط. لا نوصي بتغيير هذا الخيار إلا إذا كنت تعرف ما تفعله.
|
||||
account.methods.offline.uuid.malformed=تنسيق غير صالح.
|
||||
account.methods.ban_query=الاستعلام عن الحظر
|
||||
account.missing=لا توجد حسابات
|
||||
account.missing.add=انقر هنا لإضافة واحد.
|
||||
account.move_to_global=تحويل إلى حساب عام\nسيتم حفظ معلومات الحساب في ملف إعدادات في مجلد المستخدم الحالي للنظام.
|
||||
account.move_to_portable=تحويل إلى حساب محمول\nسيتم حفظ معلومات الحساب في ملف إعدادات في نفس المجلد الذي يوجد فيه HMCL.
|
||||
account.not_logged_in=غير مسجّل الدخول
|
||||
account.password=كلمة المرور
|
||||
account.portable=محمول
|
||||
account.skin=المظهر
|
||||
@@ -167,8 +156,6 @@ account.skin.upload.failed=فشل رفع المظهر.
|
||||
account.skin.invalid_skin=ملف المظهر غير صالح.
|
||||
account.username=اسم المستخدم
|
||||
|
||||
addon.broken_dependency.title=تبعية معطوبة
|
||||
addon.broken_dependency.desc=كانت هذه التبعية موجودة مسبقاً، لكنها لم تعد كذلك. جرّب استخدام مصدر تنزيل آخر.
|
||||
addon.category=الفئة
|
||||
addon.channel.alpha=ألفا
|
||||
addon.channel.beta=بيتا
|
||||
@@ -213,7 +200,6 @@ button.reveal_dir=فتح المجلد
|
||||
button.refresh=تحديث
|
||||
button.remove=إزالة
|
||||
button.remove.confirm=هل أنت متأكد من الإزالة النهائية؟ لا يمكن التراجع عن هذا الإجراء!
|
||||
button.retry=إعادة المحاولة
|
||||
button.save=حفظ
|
||||
button.save_as=حفظ باسم
|
||||
button.select_all=تحديد الكل
|
||||
@@ -367,9 +353,6 @@ download=تنزيل
|
||||
download.code.404=الملف "%s" غير موجود على الخادم البعيد.
|
||||
download.content=الإضافات
|
||||
download.shader=الظلال
|
||||
download.curseforge.unavailable=هذا الإصدار من HMCL لا يدعم الوصول إلى CurseForge. يُرجى استخدام الإصدار الرسمي للوصول إلى CurseForge.
|
||||
download.existing=لا يمكن حفظ الملف لأنه موجود مسبقاً. يمكنك النقر على "حفظ باسم" لحفظ الملف في مكان آخر.
|
||||
download.external_link=زيارة موقع التنزيل
|
||||
download.failed=فشل تنزيل "%1$s"، رمز الاستجابة: %2$d.
|
||||
download.failed.empty=لا توجد إصدارات متاحة. انقر هنا للرجوع.
|
||||
download.failed.no_code=فشل التنزيل
|
||||
@@ -445,10 +428,6 @@ fatal.deprecated_java_version=سيتطلب HMCL Java 17 أو أحدث للتشغ
|
||||
يُنصح بتثبيت أحدث إصدار من Java لضمان عمل HMCL بشكل صحيح.\n\
|
||||
\n\
|
||||
يمكنك الاحتفاظ بإصدار Java القديم. يستطيع HMCL التعرف على إصدارات Java المتعددة وإدارتها وسيختار تلقائياً Java المناسب بناءً على إصدار اللعبة.
|
||||
fatal.deprecated_java_version.update=سيتطلب HMCL Java 17 أو أحدث للتشغيل في المستقبل. يُرجى تثبيت أحدث إصدار من Java لضمان قدرة HMCL على إتمام الترقية.\n\
|
||||
\n\
|
||||
يمكنك الاحتفاظ بإصدار Java القديم.\
|
||||
يستطيع HMCL التعرف على إصدارات Java المتعددة وإدارتها وسيختار تلقائياً Java المناسب بناءً على إصدار اللعبة.
|
||||
fatal.deprecated_java_version.download_link=تنزيل Java %d
|
||||
fatal.samba=إذا فتحت Hello Minecraft! Launcher من محرك أقراص شبكة Samba، قد لا تعمل بعض الميزات. يُرجى محاولة تحديث Java أو نقل المشغّل إلى مجلد آخر.
|
||||
fatal.illegal_char=مسار المستخدم الخاص بك يحتوي على حرف غير مسموح به "=". لن تتمكن من استخدام authlib-injector أو تغيير مظهر حسابك غير المتصل.
|
||||
@@ -709,11 +688,8 @@ game_directory.default=الحالي
|
||||
game_directory.home=مشغّل Minecraft
|
||||
game_directory.instance_directory=مجلد اللعبة
|
||||
game_directory.instance_directory.choose=اختر مجلد اللعبة
|
||||
game_directory.manage=قائمة مجلدات النسخ
|
||||
game_directory.name=الاسم
|
||||
game_directory.new=مجلد جديد
|
||||
game_directory.title=مجلدات اللعبة
|
||||
game_directory.selected=محدد
|
||||
game_directory.use_relative_path=استخدام مسار نسبي لمجلد اللعبة إن أمكن
|
||||
|
||||
help=مساعدة
|
||||
@@ -741,7 +717,6 @@ install.failed.version_mismatch=يتطلب هذا المحمّل إصدار ال
|
||||
install.installer.change_version=%s غير متوافق
|
||||
install.installer.choose=اختر إصدار %s
|
||||
install.installer.cleanroom=Cleanroom
|
||||
install.installer.depend=يتطلب %s
|
||||
install.installer.do_not_install=لا تثبّت
|
||||
install.installer.fabric=Fabric
|
||||
install.installer.fabric-api=Fabric API
|
||||
@@ -754,9 +729,6 @@ install.installer.game=Minecraft
|
||||
install.installer.incompatible=غير متوافق مع %s
|
||||
install.installer.install=جارٍ تثبيت %s
|
||||
install.installer.install_offline=تثبيت/تحديث من ملف محلي
|
||||
install.installer.install_offline.tooltip=ندعم استخدام مثبّت (Neo)Forge وCleanroom وOptiFine المحلي.
|
||||
install.installer.install_online=تثبيت عبر الإنترنت
|
||||
install.installer.install_online.tooltip=ندعم حالياً Forge وNeoForge وCleanroom وOptiFine وFabric وLegacy Fabric وQuilt وLiteLoader.
|
||||
install.installer.liteloader=LiteLoader
|
||||
install.installer.not_installed=غير مثبّت
|
||||
install.installer.optifine=OptiFine
|
||||
@@ -770,10 +742,8 @@ install.modpack.installation=تثبيت حزمة المودات
|
||||
install.name.invalid=الاسم يحتوي على أحرف خاصة (مثل الرموز التعبيرية أو أحرف CJK).\nيُنصح بتغيير الاسم ليحتوي على الحروف الإنجليزية والأرقام والشرطات السفلية فقط لتجنب مشاكل محتملة عند تشغيل اللعبة.\nهل تريد المتابعة؟
|
||||
install.new_game=تثبيت نسخة
|
||||
install.new_game.already_exists=اسم هذه النسخة موجود مسبقاً. يُرجى اختيار اسم آخر.
|
||||
install.new_game.current_game_version=إصدار النسخة الحالية
|
||||
install.new_game.installation=تثبيت النسخة
|
||||
install.new_game.malformed=اسم غير صالح.
|
||||
install.select=اختر العملية
|
||||
install.success=تم التثبيت بنجاح.
|
||||
|
||||
instance=الألعاب
|
||||
@@ -867,13 +837,11 @@ launch.advice.multi=تم اكتشاف المشاكل التالية:\n\n%s\n\nق
|
||||
launch.advice.java.auto=إصدار Java الحالي غير متوافق مع هذه النسخة.\n\nانقر "نعم" لاختيار إصدار Java الأكثر توافقاً تلقائياً. أو، انتقل إلى "الإعدادات العامة/الخاصة بالنسخة ← Java" لاختياره بنفسك.
|
||||
launch.advice.java.modded_java_7=يتطلب Minecraft 1.7.2 والإصدارات الأقدم Java 7 أو أقدم.
|
||||
launch.advice.cleanroom=لا يعمل Cleanroom %2$s إلا على Java %1$s أو أحدث. يُرجى استخدام Java %1$s أو إصدار أحدث.
|
||||
launch.advice.corrected=لقد حللنا مشكلة Java. إذا كنت تريد استخدام إصدار Java الذي اخترته، يمكنك تعطيل "عدم التحقق من توافق JVM" في "الإعدادات العامة/الخاصة بالنسخة ← الإعدادات المتقدمة".
|
||||
launch.advice.uncorrected=إذا كنت تريد استخدام إصدار Java الذي اخترته، يمكنك تعطيل "عدم التحقق من توافق JVM" في "الإعدادات العامة/الخاصة بالنسخة ← الإعدادات المتقدمة".
|
||||
launch.advice.different_platform=يُنصح باستخدام إصدار Java 64-bit لجهازك، لكنك ثبّتت إصداراً 32-bit.
|
||||
launch.advice.forge2760_liteloader=إصدار Forge 2760 غير متوافق مع LiteLoader. يُرجى الترقية إلى Forge 2773 أو أحدث.
|
||||
launch.advice.forge28_2_2_optifine=إصدار Forge 28.2.2 أو أحدث غير متوافق مع OptiFine. يُرجى تخفيض Forge إلى 28.2.1 أو أقدم.
|
||||
launch.advice.forge37_0_60=إصدارات Forge قبل 37.0.60 غير متوافقة مع Java 17. يُرجى تحديث Forge إلى 37.0.60 أو أحدث، أو تشغيل اللعبة بـ Java 16.
|
||||
launch.advice.java8_1_13=يعمل Minecraft 1.13 وأحدث فقط على Java 8 أو أحدث. يُرجى استخدام Java 8 أو إصدار أحدث.
|
||||
launch.advice.java8_51_1_13=قد يتعطل Minecraft 1.13 على إصدارات Java 8 قبل 1.8.0_51. يُرجى تثبيت أحدث إصدار من Java 8.
|
||||
launch.advice.java9=لا يمكن تشغيل Minecraft 1.12 أو أقدم على Java 9 أو أحدث. يُرجى استخدام Java 8.
|
||||
launch.advice.modded_java=قد لا تكون بعض المودات متوافقة مع إصدارات Java الأحدث. يُنصح باستخدام Java %1$s لتشغيل Minecraft %2$s.
|
||||
@@ -933,7 +901,6 @@ launcher.update_java=يُرجى تحديث إصدار Java الخاص بك.
|
||||
|
||||
libraries.download=جارٍ تنزيل المكتبات
|
||||
|
||||
login.empty_username=لم تحدد اسم المستخدم بعد!
|
||||
login.enter_password=يُرجى إدخال كلمة المرور.
|
||||
|
||||
logwindow.show_lines=إظهار أرقام الأسطر
|
||||
@@ -945,7 +912,6 @@ logwindow.export_game_crash_logs=تصدير سجلات التعطل
|
||||
logwindow.export_dump=تصدير تفريغ مكدس اللعبة
|
||||
logwindow.export_dump.no_dependency=لا يحتوي Java الخاص بك على المكتبات اللازمة لإنشاء تفريغ المكدس. يُرجى الانضمام إلى Discord أو مجموعة QQ للحصول على مساعدة.
|
||||
|
||||
main_page=الرئيسية
|
||||
|
||||
message.cancelled=تم إلغاء العملية
|
||||
message.confirm=تأكيد
|
||||
@@ -975,7 +941,6 @@ modpack.desc=صف حزمة مودتك، بما في ذلك مقدمة وربما
|
||||
modpack.description=وصف حزمة المودات
|
||||
modpack.download=تنزيل حزم المودات
|
||||
modpack.download.title=تنزيل حزمة المودات - %1s
|
||||
modpack.enter_name=أدخل اسماً لهذه الحزمة.
|
||||
modpack.export=تصدير كحزمة مودات
|
||||
modpack.export.as=تصدير حزمة المودات باسم...
|
||||
modpack.file_api=بادئة رابط حزمة المودات
|
||||
@@ -994,11 +959,9 @@ modpack.files.scripts=ملف إعداد MineTweaker
|
||||
modpack.files.servers_dat=ملف قائمة الخوادم
|
||||
modpack.installing=جارٍ تثبيت حزمة المودات
|
||||
modpack.installing.given=جارٍ تثبيت حزمة المودات %s
|
||||
modpack.introduction=يدعم حالياً حزم Curse وModrinth وMultiMC وMCBBS.
|
||||
modpack.invalid=حزمة مودات غير صالحة، يمكنك محاولة تنزيلها مجدداً.
|
||||
modpack.mismatched_type=نوع حزمة المودات غير متطابق، النسخة الحالية من نوع %1$s، لكن الحزمة المقدمة من نوع %2$s.
|
||||
modpack.name=اسم حزمة المودات
|
||||
modpack.not_a_valid_name=اسم حزمة مودات غير صالح.
|
||||
modpack.origin=المصدر
|
||||
modpack.origin.url=الموقع الرسمي
|
||||
modpack.origin.mcbbs=MCBBS
|
||||
@@ -1127,7 +1090,6 @@ modrinth.category.velocity=Velocity
|
||||
modrinth.category.waterfall=Waterfall
|
||||
modrinth.category.worldgen=توليد العالم
|
||||
modrinth.category.8x-=8x-
|
||||
modrinth.category.16x=16x
|
||||
modrinth.category.32x=32x
|
||||
modrinth.category.48x=48x
|
||||
modrinth.category.64x=64x
|
||||
@@ -1151,7 +1113,6 @@ mods.mcmod=MCMod
|
||||
mods.mcmod.page=صفحة MCMod
|
||||
mods.mcmod.search=البحث في MCMod
|
||||
mods.name=الاسم
|
||||
mods.not_modded=يجب تثبيت محمّل مودات (Forge أو NeoForge أو Fabric أو Legacy Fabric أو Quilt أو LiteLoader) أولاً لإدارة موداتك!
|
||||
mods.restore=استعادة
|
||||
mods.url=الصفحة الرسمية
|
||||
mods.update_modpack_mod.warning=تحديث المودات في حزمة مودات قد يؤدي إلى نتائج لا يمكن إصلاحها، وقد يتسبب في تلف الحزمة بحيث لا تعمل. هل أنت متأكد من التحديث؟
|
||||
@@ -1203,7 +1164,6 @@ world.chunkbase.nether_fortress=قلعة الجحيم
|
||||
world.duplicate=نسخ العالم
|
||||
world.duplicate.prompt=يُرجى إدخال اسم العالم المنسوخ
|
||||
world.duplicate.failed.already_exists=المجلد موجود مسبقاً
|
||||
world.duplicate.failed.empty_name=لا يمكن أن يكون الاسم فارغاً
|
||||
world.duplicate.failed.invalid_name=الاسم يحتوي على أحرف غير صالحة
|
||||
world.duplicate.failed=فشل نسخ العالم
|
||||
world.duplicate.success.toast=تم نسخ العالم بنجاح
|
||||
@@ -1299,7 +1259,6 @@ schematics.create_directory=مجلد جديد
|
||||
schematics.create_directory.prompt=يُرجى إدخال اسم المجلد الجديد
|
||||
schematics.create_directory.failed=فشل إنشاء المجلد
|
||||
schematics.create_directory.failed.already_exists=المجلد موجود مسبقاً
|
||||
schematics.create_directory.failed.empty_name=لا يمكن أن يكون الاسم فارغاً
|
||||
schematics.create_directory.failed.invalid_name=الاسم يحتوي على أحرف غير صالحة
|
||||
schematics.info.description=الوصف
|
||||
schematics.info.enclosing_size=الحجم المحيط
|
||||
@@ -1335,20 +1294,6 @@ settings.advanced=الإعدادات المتقدمة
|
||||
settings.advanced.modify=تعديل الإعدادات المتقدمة
|
||||
settings.advanced.title=الإعدادات المتقدمة - %s
|
||||
settings.advanced.custom_commands=أوامر مخصصة
|
||||
settings.advanced.custom_commands.hint=المتغيرات البيئية التالية متاحة:\n\
|
||||
\ · $INST_NAME: اسم النسخة.\n\
|
||||
\ · $INST_ID: اسم النسخة.\n\
|
||||
\ · $INST_DIR: المسار المطلق لمجلد عمل النسخة.\n\
|
||||
\ · $INST_MC_DIR: المسار المطلق لمجلد اللعبة.\n\
|
||||
\ · $INST_JAVA: ملف Java المستخدم للتشغيل.\n\
|
||||
\ · $INST_FORGE: يُضبط إذا كان Forge مثبّتاً.\n\
|
||||
\ · $INST_NEOFORGE: يُضبط إذا كان NeoForge مثبّتاً.\n\
|
||||
\ · $INST_CLEANROOM: يُضبط إذا كان Cleanroom مثبّتاً.\n\
|
||||
\ · $INST_LITELOADER: يُضبط إذا كان LiteLoader مثبّتاً.\n\
|
||||
\ · $INST_OPTIFINE: يُضبط إذا كان OptiFine مثبّتاً.\n\
|
||||
\ · $INST_FABRIC: يُضبط إذا كان Fabric مثبّتاً.\n\
|
||||
\ · $INST_LEGACYFABRIC: يُضبط إذا كان Legacy Fabric مثبّتاً.\n\
|
||||
\ · $INST_QUILT: يُضبط إذا كان Quilt مثبّتاً.
|
||||
settings.advanced.dont_check_game_completeness=عدم التحقق من سلامة اللعبة
|
||||
settings.advanced.dont_check_jvm_validity=عدم التحقق من توافق JVM
|
||||
settings.advanced.dont_patch_natives=عدم محاولة استبدال المكتبات الأصلية تلقائياً
|
||||
@@ -1369,10 +1314,6 @@ settings.advanced.launcher_visible=ظهور المشغّل
|
||||
settings.advanced.minecraft_arguments=وسيطات التشغيل
|
||||
settings.advanced.minecraft_arguments.prompt=افتراضي
|
||||
settings.advanced.natives_directory=مسار المكتبات الأصلية
|
||||
settings.advanced.natives_directory.choose=اختر موقع المكتبة الأصلية المطلوبة
|
||||
settings.advanced.natives_directory.custom=مخصص
|
||||
settings.advanced.natives_directory.default=افتراضي
|
||||
settings.advanced.natives_directory.default.version_id=<اسم النسخة>
|
||||
settings.advanced.no_jvm_args=عدم إضافة وسيطات JVM الافتراضية
|
||||
settings.advanced.no_optimizing_jvm_args=عدم إضافة وسيطات تحسين JVM الافتراضية
|
||||
settings.advanced.precall_command=أمر ما قبل التشغيل
|
||||
@@ -1398,11 +1339,9 @@ settings.advanced.renderer.zink=Mesa Zink
|
||||
settings.advanced.server_ip=عنوان الخادم
|
||||
settings.advanced.server_ip.prompt=الانضمام تلقائياً بعد تشغيل اللعبة
|
||||
settings.advanced.unsupported_system_options=إعدادات غير مطبّقة على النظام الحالي
|
||||
settings.advanced.use_native_glfw=استخدام GLFW النظام
|
||||
settings.advanced.use_native_glfw_or_sdl=استخدام GLFW / SDL النظام
|
||||
settings.advanced.use_native_openal=استخدام OpenAL النظام
|
||||
settings.advanced.linux_freebsd_only=Linux/FreeBSD فقط
|
||||
settings.advanced.workaround=حلول بديلة
|
||||
settings.advanced.workaround.warning=خيارات الحلول البديلة مخصصة للمستخدمين المتقدمين فقط. قد يتسبب تعديل هذه الخيارات في تعطل اللعبة. يُرجى عدم تعديل هذه الخيارات إلا إذا كنت تعرف ما تفعله.
|
||||
settings.advanced.wrapper_launcher=أمر التغليف
|
||||
settings.advanced.wrapper_launcher.prompt=يتيح التشغيل باستخدام برنامج تغليف إضافي مثل "optirun" على Linux
|
||||
|
||||
@@ -1422,11 +1361,7 @@ settings.game.java_directory.invalid=مسار Java غير صحيح
|
||||
settings.game.java_directory.version=تحديد إصدار Java
|
||||
settings.game.java_directory.template=%1$s (%2$s)
|
||||
settings.game.management=إدارة
|
||||
settings.game.working_directory=مجلد العمل
|
||||
settings.game.working_directory.choose=اختر مجلد العمل
|
||||
settings.game.working_directory.hint=فعّل خيار "معزول" في "مجلد العمل" للسماح للنسخة الحالية بتخزين إعداداتها وعوالمها ومودتها في مجلد منفصل.\n\
|
||||
\n\
|
||||
يُنصح بتفعيل هذا الخيار لتجنب تعارض المودات، لكنك ستحتاج إلى نقل عوالمك يدوياً.
|
||||
|
||||
settings.icon=الأيقونة
|
||||
|
||||
@@ -1436,16 +1371,13 @@ settings.launcher.brightness=وضع السمة
|
||||
settings.launcher.brightness.auto=متابعة إعدادات النظام
|
||||
settings.launcher.brightness.dark=الوضع الداكن
|
||||
settings.launcher.brightness.light=الوضع الفاتح
|
||||
settings.launcher.common_path.tooltip=سيضع HMCL جميع أصول اللعبة وتبعياتها هنا. إذا كانت هناك مكتبات موجودة في مجلد اللعبة، فإن HMCL سيفضّل استخدامها أولاً.
|
||||
settings.launcher.debug=تصحيح
|
||||
settings.launcher.disable_auto_game_options=عدم تغيير لغة اللعبة تلقائياً
|
||||
settings.launcher.download=التنزيل
|
||||
settings.launcher.download.threads=الخيوط
|
||||
settings.launcher.download.threads.auto=تحديد تلقائي
|
||||
settings.launcher.download.threads.hint=قد يتسبب عدد كبير من الخيوط في تجميد النظام، وقد تتأثر سرعة التنزيل بمزوّد الخدمة وخوادم التنزيل. ليس دائماً أن المزيد من الخيوط يزيد سرعة التنزيل.
|
||||
settings.launcher.download_source=مصدر التنزيل
|
||||
settings.launcher.download_source.auto=اختيار مصادر التنزيل تلقائياً
|
||||
settings.launcher.enable_game_list=عرض قائمة النسخ في الصفحة الرئيسية
|
||||
settings.launcher.font=الخط
|
||||
settings.launcher.font.anti_aliasing=مضاد التعرج
|
||||
settings.launcher.font.anti_aliasing.auto=تلقائي
|
||||
@@ -1492,9 +1424,6 @@ settings.type.global.manage=الإعدادات العامة
|
||||
|
||||
shaderpack.download.title=تنزيل ظلال - %1s
|
||||
|
||||
sponsor=المتبرعون
|
||||
sponsor.bmclapi=تنزيلات البر الرئيسي الصيني مقدَّمة من BMCLAPI. انقر هنا للمزيد من المعلومات.
|
||||
sponsor.hmcl=Hello Minecraft! Launcher هو مشغّل Minecraft مفتوح المصدر يتيح للمستخدمين إدارة نسخ متعددة من Minecraft بسهولة. انقر هنا للمزيد من المعلومات.
|
||||
|
||||
system.architecture=المعمارية
|
||||
system.operating_system=نظام التشغيل
|
||||
@@ -1607,7 +1536,6 @@ update.newest_version=أحدث إصدار: %s
|
||||
update.bubble.title=تحديث متاح: %s
|
||||
update.bubble.subtitle=انقر هنا للتحديث
|
||||
update.latest=هذا هو أحدث إصدار
|
||||
update.no_browser=لا يمكن الفتح في متصفح النظام. لكننا نسخنا الرابط إلى حافظتك، ويمكنك فتحه يدوياً.
|
||||
update.tooltip=تحديث
|
||||
update.preview=الاطلاع المبكر على إصدارات HMCL
|
||||
update.preview.subtitle=فعّل هذا الخيار لاستقبال إصدارات HMCL الجديدة مبكراً للاختبار قبل إصدارها الرسمي.
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -21,7 +21,6 @@
|
||||
|
||||
about=Acerca de
|
||||
about.copyright=Copyright
|
||||
about.author=Autor
|
||||
about.author.statement=bilibili @huanghongxun
|
||||
about.claim=EULA
|
||||
about.claim.statement=Haga clic en este enlace para ver el texto completo.
|
||||
@@ -75,7 +74,6 @@ account.failed.server_response_malformed=Respuesta del servidor no válida, el s
|
||||
account.failed.ssl=Se ha producido un error SSL al conectar con el servidor. Por favor, intente actualizar su Java.
|
||||
account.failed.dns=Se produjo un error SSL al conectar con el servidor. Es posible que la resolución DNS sea incorrecta. Intente cambiar el servidor DNS o usar un servicio proxy.
|
||||
account.failed.wrong_account=Ha iniciado sesión en la cuenta equivocada.
|
||||
account.hmcl.hint=Debe hacer clic en «Iniciar sesión» y completar el proceso en la ventana abierta del navegador.
|
||||
account.injector.add=Nuevo servidor Auth
|
||||
account.injector.empty=Ninguno (Puedes hacer clic en el botón más de la derecha para añadir uno)
|
||||
account.injector.http=Atención: Este servidor utiliza el protocolo HTTP inseguro. Cualquiera entre su conexión podrá ver sus credenciales en texto claro.
|
||||
@@ -91,23 +89,16 @@ account.login.retry=Reintentar
|
||||
account.login.refresh=Iniciar sesión de nuevo
|
||||
account.login.refresh.microsoft.hint=Debe volver a iniciar sesión en su cuenta Microsoft porque la autorización de la cuenta no es válida.
|
||||
account.login.restricted=Iniciar sesión en su cuenta Microsoft para activar esta función
|
||||
account.logout=Salir
|
||||
account.register=Registrarse
|
||||
account.manage=Lista de cuentas
|
||||
account.copy_uuid=Copiar UUID de la cuenta
|
||||
account.methods=Tipo de inicio de sesión
|
||||
account.methods.authlib_injector=authlib-injector
|
||||
account.methods.microsoft=Microsoft
|
||||
account.methods.microsoft.birth=Cómo cambiar la fecha de nacimiento de su cuenta
|
||||
account.methods.microsoft.close_page=La autorización de la cuenta de Microsoft ha finalizado.\n\
|
||||
\n\
|
||||
Hay algunos trabajos extra para nosotros, pero puedes cerrar esta pestaña con seguridad por ahora.
|
||||
account.methods.microsoft.makegameidsettings=Crear perfil / Editar nombre del perfil
|
||||
account.methods.microsoft.deauthorize=Desautorizar
|
||||
account.methods.microsoft.logging_in=Iniciando sesión...
|
||||
account.methods.microsoft.profile=Perfil de la cuenta
|
||||
account.methods.microsoft.purchase=Comprar Minecraft
|
||||
account.methods.microsoft.snapshot.website=Sitio web oficial
|
||||
account.methods.offline=Sin conexión
|
||||
account.methods.offline.name.special_characters=Utilice solo letras, números y guiones bajos (máximo 16 caracteres)
|
||||
account.methods.offline.name.invalid=Se recomienda utilizar sólo letras en inglés, números y guiones bajos para el nombre de usuario, y la longitud no debe superar los 16 caracteres.\n\
|
||||
@@ -121,12 +112,10 @@ account.methods.offline.uuid.hint=UUID es un identificador único para los jugad
|
||||
\n\
|
||||
Esta opción es sólo para usuarios avanzados. No recomendamos editar esta opción a menos que sepas lo que estás haciendo.
|
||||
account.methods.offline.uuid.malformed=Formato no válido.
|
||||
account.methods.ban_query=Consulta de bloqueo de cuentas
|
||||
account.missing=No hay cuentas
|
||||
account.missing.add=Haga clic aquí para añadir una.
|
||||
account.move_to_global=Convertir en cuenta global\nLa información de la cuenta se guardará en un archivo de configuración del directorio de usuario actual del sistema.
|
||||
account.move_to_portable=Convertir en cuenta portátil\nLa información de la cuenta se guardará en un archivo de configuración en el mismo directorio que HMCL.
|
||||
account.not_logged_in=No ha iniciado sesión
|
||||
account.password=Contraseña
|
||||
account.portable=Cuenta portátil
|
||||
account.skin=Aspecto
|
||||
@@ -147,8 +136,6 @@ account.skin.upload.failed=No se pudo subir el aspecto.
|
||||
account.skin.invalid_skin=Archivo de aspecto inválido.
|
||||
account.username=Nombre de usuario
|
||||
|
||||
addon.broken_dependency.title=Dependencia rota
|
||||
addon.broken_dependency.desc=Esta dependencia existía antes, pero ahora no existe. Intente utilizar otra fuente de descarga.
|
||||
addon.category=Categoría
|
||||
addon.channel.alpha=Alpha
|
||||
addon.channel.beta=Beta
|
||||
@@ -193,7 +180,6 @@ button.reveal_dir=Carpeta
|
||||
button.refresh=Refrescar
|
||||
button.remove=Eliminar
|
||||
button.remove.confirm=¿Estás seguro de que deseas eliminarlo de forma permanente? ¡Esta acción no se puede deshacer!
|
||||
button.retry=Reintentar
|
||||
button.save=Guardar
|
||||
button.save_as=Guardar como
|
||||
button.select_all=Seleccionar todo
|
||||
@@ -347,9 +333,6 @@ download=Descargar
|
||||
download.code.404=Archivo no encontrado en el servidor remoto: %s
|
||||
download.content=Complementos
|
||||
download.shader=Sombreadores
|
||||
download.curseforge.unavailable=Esta versión de HMCL no permite acceder a CurseForge. Utilice la versión oficial para acceder a CurseForge.
|
||||
download.existing=El archivo no se puede guardar porque ya existe. Puedes hacer clic en «Guardar como» para guardar el archivo en otro lugar.
|
||||
download.external_link=Abrir sitio web
|
||||
download.failed=Falló la descarga de %1$s, código de respuesta: %2$d.
|
||||
download.failed.empty=No hay versiones disponibles, por favor haga clic aquí para volver.
|
||||
download.failed.no_code=No se ha podido descargar
|
||||
@@ -419,11 +402,6 @@ fatal.deprecated_java_version=HMCL requerirá Java 17 o posterior para funcionar
|
||||
Se recomienda instalar la última versión de Java para garantizar el correcto funcionamiento de HMCL.\n\
|
||||
\n\
|
||||
Puedes conservar la versión antigua de Java. HMCL puede reconocer y gestionar varias versiones de Java y elegirá automáticamente la adecuada para usted en función de la versión del juego.
|
||||
fatal.deprecated_java_version.update=HMCL requerirá Java 17 o posterior para funcionar en el futuro. Por favor, instalar la última versión de Java para asegurarse de que el lanzador puede completar la actualización.\n\
|
||||
\n\
|
||||
Puedes conservar la versión antigua de Java.\
|
||||
HMCL puede reconocer y gestionar varias versiones de Java \
|
||||
y elegirá automáticamente la adecuada para usted en función de la versión del juego.
|
||||
fatal.deprecated_java_version.download_link=Descargar Java %d
|
||||
fatal.samba=Si ha abierto Hello Minecraft! Launcher desde una unidad de red Samba, es posible que algunas funciones no funcionen. Intenta actualizar tu Java o mover el launcher a otro directorio.
|
||||
fatal.illegal_char=Su ruta de usuario contiene un carácter ilegal «=», por lo que algunas características podrían no funcionar correctamente.\n\
|
||||
@@ -675,11 +653,8 @@ game_directory.default=Directorio actual
|
||||
game_directory.home=Minecraft Launcher
|
||||
game_directory.instance_directory=Directorio ded juego
|
||||
game_directory.instance_directory.choose=Elegir directorio de juegos
|
||||
game_directory.manage=Lista de directorios de instancia
|
||||
game_directory.name=Nombre
|
||||
game_directory.new=Nuevo directorio
|
||||
game_directory.title=Directorios del juego
|
||||
game_directory.selected=Seleccionado
|
||||
game_directory.use_relative_path=Utilizar ruta relativa para la ruta del juego si es posible
|
||||
|
||||
help=Ayuda
|
||||
@@ -707,7 +682,6 @@ install.failed.version_mismatch=Este cargador requiere la versión del juego %s,
|
||||
install.installer.change_version=%s Incompatible
|
||||
install.installer.choose=Elija su versión %s
|
||||
install.installer.cleanroom=Cleanroom
|
||||
install.installer.depend=Requiere %s
|
||||
install.installer.do_not_install=No instalar
|
||||
install.installer.fabric=Fabric
|
||||
install.installer.fabric-api=Fabric API
|
||||
@@ -718,9 +692,6 @@ install.installer.game=Minecraft
|
||||
install.installer.incompatible=Incompatible con %s
|
||||
install.installer.install=Instalando %s
|
||||
install.installer.install_offline=Instalar/actualizar desde archivo local
|
||||
install.installer.install_offline.tooltip=Apoyamos el uso del instalador local de (Neo)Forge/Cleanroom/OptiFine.
|
||||
install.installer.install_online=Instalación en línea
|
||||
install.installer.install_online.tooltip=Actualmente soportamos Forge, NeoForge, Cleanroom, Fabric, Legacy Fabric, Quilt, LiteLoader y OptiFine.
|
||||
install.installer.liteloader=LiteLoader
|
||||
install.installer.not_installed=No está instalado
|
||||
install.installer.optifine=OptiFine
|
||||
@@ -734,10 +705,8 @@ install.modpack.installation=Instalación de modpack
|
||||
install.name.invalid=El nombre contiene caracteres especiales (como emoji o caracteres CJK).\n\nSe recomienda cambiar el nombre para que sólo incluya letras, números y guiones bajos en inglés para evitar posibles problemas al iniciar el juego.\n\n¿Desea continuar con la instalación?
|
||||
install.new_game=Añadir instancia
|
||||
install.new_game.already_exists=Esta instancia ya existe. Por favor, utilice otro nombre.
|
||||
install.new_game.current_game_version=Versión actual de la instancia
|
||||
install.new_game.installation=Instalación de instancia
|
||||
install.new_game.malformed=Nombre no válido.
|
||||
install.select=Elegir una operación
|
||||
install.success=Instalado con éxito.
|
||||
|
||||
instance=Juegos
|
||||
@@ -821,13 +790,11 @@ launch.advice.multi=Se han detectado los siguientes problemas:\n\n%s\n\nEstos pr
|
||||
launch.advice.java.auto=La versión actual de Java no es compatible con la instancia.\n\nHaga clic en «Sí» para elegir automáticamente la versión de Java más compatible. O bien, puede navegar hasta «Config. Global/Específica de instancia → Java» para elegir uno usted mismo.
|
||||
launch.advice.java.modded_java_7=Minecraft 1.7.2 y versiones anteriores requieren Java 7 o anterior.
|
||||
launch.advice.cleanroom=Cleanroom %2$s solo se puede ejecutar en Java %1$s o versiones posteriores. Por favor, use Java %1$s o una versión posterior.
|
||||
launch.advice.corrected=Hemos resuelto el problema de Java. Si aún desea utilizar la versión de Java que elija, puede desactivar «No comprobar la compatibilidad con JVM» en «Config. Global/Específica de instancia → Configuración avanzada».
|
||||
launch.advice.uncorrected=Si aún desea utilizar la versión de Java que elija, puede desactivar «No comprobar la compatibilidad con JVM» en «Config. Global/Específica de instancia → Configuración avanzada».
|
||||
launch.advice.different_platform=Se recomienda la versión de 64 bits de Java para tu dispositivo, pero has instalado una de 32 bits.
|
||||
launch.advice.forge2760_liteloader=La versión 2760 o posterior de Forge no es compatible con LiteLoader, por favor considere actualizar Forge a la versión 2773 o posterior.
|
||||
launch.advice.forge28_2_2_optifine=La versión 28.2.2 o posterior de Forge no es compatible con OptiFine. Considere la posibilidad de actualizar Forge a la versión 28.2.1 o anterior.
|
||||
launch.advice.forge37_0_60=Las versiones de Forge anteriores a la 37.0.60 no son compatibles con Java 17. Por favor, actualiza Forge a la versión 37.0.60 o posterior, o inicia el juego con Java 16.
|
||||
launch.advice.java8_1_13=Minecraft 1.13 y posteriores sólo se pueden ejecutar en Java 8 o posterior. Por favor, utilice Java 8 o versiones posteriores.
|
||||
launch.advice.java8_51_1_13=Minecraft 1.13 puede fallar en versiones de Java 8 anteriores a la 1.8.0_51. Por favor, instala la última versión de Java 8.
|
||||
launch.advice.java9=No puedes ejecutar Minecraft 1.12 o anterior con Java 9 o más reciente, por favor, utiliza Java 8 en su lugar.
|
||||
launch.advice.modded_java=Algunos mods pueden no ser compatibles con las nuevas versiones de Java. Se recomienda utilizar Java %s para iniciar Minecraft %s.
|
||||
@@ -887,7 +854,6 @@ launcher.update_java=Por favor, actualice su versión de Java.
|
||||
|
||||
libraries.download=Descargando bibliotecas
|
||||
|
||||
login.empty_username=¡Todavía no has puesto tu nombre de usuario!
|
||||
login.enter_password=Por favor, introduzca su contraseña.
|
||||
|
||||
logwindow.show_lines=Mostrar número de líneas
|
||||
@@ -899,7 +865,6 @@ logwindow.export_game_crash_logs=Exportar registros de colgado
|
||||
logwindow.export_dump=Exportar volcado de pila de juegos
|
||||
logwindow.export_dump.no_dependency=Su Java no contiene las dependencias para crear el volcado de pila. Dirígete a nuestro Discord o grupo QQ para obtener ayuda.
|
||||
|
||||
main_page=Inicio
|
||||
|
||||
message.cancelled=La operación se ha cancelado
|
||||
message.confirm=Confirmar
|
||||
@@ -928,7 +893,6 @@ modpack.desc=Describa su modpack, incluyendo una introducción y probablemente a
|
||||
modpack.description=Descripción
|
||||
modpack.download=Descargar Modpacks
|
||||
modpack.download.title=Descargar Modpack - %1s
|
||||
modpack.enter_name=Introduzca un nombre para este modpack.
|
||||
modpack.export=Exportar como modpack
|
||||
modpack.export.as=Exportar Modpack como...
|
||||
modpack.file_api=Prefijo de la URL del modpack
|
||||
@@ -947,11 +911,9 @@ modpack.files.scripts=Archivo de configuración de MineTweaker
|
||||
modpack.files.servers_dat=Archivo de lista de servidores
|
||||
modpack.installing=Instalando modpack
|
||||
modpack.installing.given=Instalando %s modpack
|
||||
modpack.introduction=Actualmente se soportan los modpacks CurseForge, Modrinth, MultiMC y MCBBS.
|
||||
modpack.invalid=Modpack inválido, puede intentar volver a descargarlo.
|
||||
modpack.mismatched_type=Tipo de modpack erróneo, la instancia actual es del tipo %s, pero la proporcionada es del tipo %s.
|
||||
modpack.name=Nombre del modpack
|
||||
modpack.not_a_valid_name=Nombre de paquete no válido
|
||||
modpack.origin=Fuente
|
||||
modpack.origin.url=Sitio web oficial
|
||||
modpack.origin.mcbbs=MCBBS
|
||||
@@ -1080,6 +1042,11 @@ modrinth.category.velocity=Velocity
|
||||
modrinth.category.waterfall=Waterfall
|
||||
modrinth.category.worldgen=Generación de mundos
|
||||
modrinth.category.8x-=8x o más bajo
|
||||
modrinth.category.32x=32x
|
||||
modrinth.category.48x=48x
|
||||
modrinth.category.64x=64x
|
||||
modrinth.category.128x=128x
|
||||
modrinth.category.256x=256x
|
||||
modrinth.category.512x+=512x o mayor
|
||||
|
||||
mods=Mods
|
||||
@@ -1096,7 +1063,6 @@ mods.mcmod=MCMod
|
||||
mods.mcmod.page=Página de MCMod
|
||||
mods.mcmod.search=Búsqueda en MCMod
|
||||
mods.name=Nombre
|
||||
mods.not_modded=¡Debes instalar primero un cargador de mods (Forge, NeoForge, Fabric, Legacy Fabric, Quilt o LiteLoader) para gestionar tus mods!
|
||||
mods.restore=Restaurar
|
||||
mods.url=Página oficial
|
||||
mods.update_modpack_mod.warning=Actualizar mods en un modpack puede generar resultados irreparables, posiblemente corrompiendo el modpack para que no pueda iniciarse. ¿Seguro que quieres actualizar?
|
||||
@@ -1131,7 +1097,6 @@ world.chunkbase.seed_map=Vista previa de la generación mundial
|
||||
world.chunkbase.stronghold=Fortaleza
|
||||
world.chunkbase.nether_fortress=Fortaleza del Nether
|
||||
world.duplicate.failed.already_exists=El directorio ya existe
|
||||
world.duplicate.failed.empty_name=El nombre no puede estar vacío
|
||||
world.duplicate.failed.invalid_name=El nombre contiene caracteres no válidos
|
||||
world.datapack=Paquetes de datos
|
||||
world.datetime=Jugado por última vez en %s
|
||||
@@ -1200,7 +1165,6 @@ schematics.back_to=Atrás a «%s»
|
||||
schematics.create_directory.prompt=Introduzca el nuevo nombre del directorio
|
||||
schematics.create_directory.failed=No se ha podido crear el directorio
|
||||
schematics.create_directory.failed.already_exists=El directorio ya existe
|
||||
schematics.create_directory.failed.empty_name=El nombre no puede estar vacío
|
||||
schematics.create_directory.failed.invalid_name=El nombre contiene caracteres no válidos
|
||||
schematics.info.description=Descripción
|
||||
schematics.info.enclosing_size=Tamaño de Encierro
|
||||
@@ -1236,19 +1200,6 @@ settings.advanced=Configuración avanzada
|
||||
settings.advanced.modify=Editar configuración avanzada
|
||||
settings.advanced.title=Configuración avanzada - %s
|
||||
settings.advanced.custom_commands=Comandos personalizados
|
||||
settings.advanced.custom_commands.hint=Se proporcionan las siguientes variables de entorno:\n\
|
||||
\ · $INST_NAME: nombre de la instancia.\n\
|
||||
\ · $INST_ID: nombre de la instancia.\n\
|
||||
\ · $INST_DIR: ruta absoluta del directorio de trabajo de la instancia.\n\
|
||||
\ · $INST_MC_DIR: ruta absoluta del directorio del juego.\n\
|
||||
\ · $INST_JAVA: binario de java utilizado para la ejecución\n\
|
||||
\ · $INST_FORGE: disponible si Forge está instalado.\n\
|
||||
\ · $INST_NEOFORGE: disponible si NeoForge está instalado.\n\
|
||||
\ · $INST_CLEANROOM: disponible si Cleanroom está instalado.\n\
|
||||
\ · $INST_LITELOADER: disponible si LiteLoader está instalado.\n\
|
||||
\ · $INST_OPTIFINE: disponible si OptiFine está instalado.\n\
|
||||
\ · $INST_FABRIC: disponible si Fabric está instalado.\n\
|
||||
\ · $INST_QUILT: disponible si Quilt está instalado.
|
||||
settings.advanced.dont_check_game_completeness=No chequear integridad del juego
|
||||
settings.advanced.dont_check_jvm_validity=No comprobar la compatibilidad con JVM
|
||||
settings.advanced.dont_patch_natives=No intente sustituir automáticamente las bibliotecas nativas
|
||||
@@ -1269,10 +1220,6 @@ settings.advanced.launcher_visible=Visibilidad del launcher
|
||||
settings.advanced.minecraft_arguments=Argumentos de lanzamiento
|
||||
settings.advanced.minecraft_arguments.prompt=Por defecto
|
||||
settings.advanced.natives_directory=Ruta de la biblioteca nativa
|
||||
settings.advanced.natives_directory.choose=Elija la ubicación de la biblioteca nativa deseada
|
||||
settings.advanced.natives_directory.custom=Personalizado
|
||||
settings.advanced.natives_directory.default=Por defecto
|
||||
settings.advanced.natives_directory.default.version_id=<Nombre de la instancia>
|
||||
settings.advanced.no_jvm_args=No añadir argumentos JVM por defecto
|
||||
settings.advanced.no_optimizing_jvm_args=No añadir argumentos de optimización de JVM
|
||||
settings.advanced.precall_command=Comando pre-lanzamiento:
|
||||
@@ -1293,11 +1240,9 @@ settings.advanced.renderer.zink=Mesa Zink
|
||||
settings.advanced.server_ip=Dirección del servidor
|
||||
settings.advanced.server_ip.prompt=Entrar automáticamente después de ejecutar el juego
|
||||
settings.advanced.unsupported_system_options=Configuración no aplicable al sistema actual
|
||||
settings.advanced.use_native_glfw=Utilizar GLFW nativo
|
||||
settings.advanced.use_native_glfw_or_sdl=Utilizar GLFW / SDL nativo
|
||||
settings.advanced.use_native_openal=Utilizar OpenAL nativo
|
||||
settings.advanced.linux_freebsd_only=Sólo Linux/FreeBSD
|
||||
settings.advanced.workaround=Métodos alternativos
|
||||
settings.advanced.workaround.warning=Éstas opciones están pensadas sólo para usuarios expertos. Jugar con estas opciones puede romper el juego. A menos que sepas lo que estás haciendo, por favor no modifiques estas opciones.
|
||||
settings.advanced.wrapper_launcher=Comando Wrapper
|
||||
settings.advanced.wrapper_launcher.prompt=Permite ejecutar el juego usando un programa extra como 'optirun' en Linux.
|
||||
|
||||
@@ -1317,23 +1262,19 @@ settings.game.java_directory.invalid=Ruta de Java incorrecta.
|
||||
settings.game.java_directory.template=%s (%s)
|
||||
settings.game.java_directory.version=Especifique la versión de Java
|
||||
settings.game.management=Gestionar
|
||||
settings.game.working_directory=Directorio de trabajo
|
||||
settings.game.working_directory.choose=Elija el directorio de trabajo
|
||||
|
||||
settings.icon=Icono
|
||||
|
||||
settings.launcher=Configuración del launcher
|
||||
settings.launcher.appearance=Apariencia
|
||||
settings.launcher.common_path.tooltip=Esta aplicación pondrá todos los activos y dependencias del juego aquí. Si hay bibliotecas existentes en el directorio del juego, el launcher preferirá usarlas primero.
|
||||
settings.launcher.debug=Depuración
|
||||
settings.launcher.disable_auto_game_options=No cambiar el idioma del juego
|
||||
settings.launcher.download=Descargas
|
||||
settings.launcher.download.threads=Hilos
|
||||
settings.launcher.download.threads.auto=Determinar automáticamente
|
||||
settings.launcher.download.threads.hint=Demasiados hilos pueden hacer que tu sistema se congele, y tu velocidad de descarga puede verse afectada por tu ISP y servidores de descarga. No siempre se da el caso de que más hilos aumenten la velocidad de descarga.
|
||||
settings.launcher.download_source=Fuente de descarga
|
||||
settings.launcher.download_source.auto=Elegir automáticamente el espejo de descarga
|
||||
settings.launcher.enable_game_list=Mostrar lista de versiones en la página de inicio
|
||||
settings.launcher.font=Fuente
|
||||
settings.launcher.font.anti_aliasing=Suavizado de bordes
|
||||
settings.launcher.font.anti_aliasing.auto=Automático
|
||||
@@ -1377,9 +1318,6 @@ settings.take_effect_after_restart=Se aplica después de reiniciar
|
||||
settings.type=Tipo de configuración de la instancia
|
||||
settings.type.global.manage=Config. Global
|
||||
|
||||
sponsor=Donantes
|
||||
sponsor.bmclapi=Las descargas de China continental son proporcionadas por BMCLAPI. Haga clic aquí para obtener más información.
|
||||
sponsor.hmcl=Hello Minecraft! Launcher es un launcher FOSS de Minecraft que permite a los usuarios gestionar múltiples instancias de Minecraft fácilmente. Haga clic aquí para obtener más información.
|
||||
|
||||
system.architecture=Arquitectura
|
||||
system.operating_system=Sistema operativo
|
||||
@@ -1407,7 +1345,6 @@ update.newest_version=Última versión: %s
|
||||
update.bubble.title=Actualización disponible: %s
|
||||
update.bubble.subtitle=Haga clic aquí para actualizar
|
||||
update.latest=Esta es la última versión.
|
||||
update.no_browser=No se puede abrir en el navegador del sistema. Pero, hemos copiado el enlace a su portapapeles y puede abrirlo manualmente.
|
||||
update.tooltip=Actualización
|
||||
update.preview=Vista previa de actualizaciones anticipadas
|
||||
update.preview.subtitle=Activa esta opción para recibir nuevas versiones del lanzador antes de su lanzamiento oficial para probarlas
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
|
||||
about=について
|
||||
about.copyright=著作権
|
||||
about.author=著者
|
||||
about.author.statement=bilibili @huanghongxun
|
||||
about.claim=EULA
|
||||
about.claim.statement=全文はこのリンクをクリック
|
||||
@@ -68,7 +67,6 @@ account.failed.migration=アカウントをMicrosoftアカウントに移行す
|
||||
account.failed.no_character=このアカウントに文字がありません。
|
||||
account.failed.server_response_malformed=無効なサーバー応答。認証サーバーでエラーが発生している可能性があります。
|
||||
account.failed.wrong_account=不一致のアカウントでログインしました。
|
||||
account.hmcl.hint=「ログイン」をクリックし、開いているページでログインを完了する必要があります。
|
||||
account.injector.add=認証サーバーを追加します
|
||||
account.injector.empty=Empty(追加するには右側のプラスボタンをクリックしてください)
|
||||
account.injector.http=警告:このサーバーはHTTPを使用するため、パスワードはクリアテキストで送信されます。
|
||||
@@ -80,17 +78,11 @@ account.login=ログイン
|
||||
account.login.hint=パスワードは保存されません。
|
||||
account.login.refresh=再ログイン
|
||||
account.login.refresh.microsoft.hint=アカウント認証が無効なため、Microsoft アカウントを再度追加する必要があります
|
||||
account.logout=ログアウト
|
||||
account.register=登録
|
||||
account.manage=アカウントリスト
|
||||
account.methods=ログインタイプ
|
||||
account.methods.authlib_injector=authlib-インジェクター
|
||||
account.methods.microsoft=Microsoft Account
|
||||
account.methods.microsoft.birth=誕生日の設定を編集する方法...
|
||||
account.methods.microsoft.deauthorize=アカウントのバインドを解除
|
||||
account.methods.microsoft.close_page=Microsoftアカウントの認証が終了しました。後で終了するログイン手順がいくつか残っています。このページを今すぐ閉じることができます。
|
||||
account.methods.microsoft.logging_in=ログイン...
|
||||
account.methods.microsoft.makegameidsettings=プロファイルを作成/プロフィール名を編集する
|
||||
account.methods.microsoft.profile=アカウントプロファイル..
|
||||
account.methods.microsoft.purchase=Minecraftを購入する
|
||||
account.methods.offline=オフライン
|
||||
@@ -99,7 +91,6 @@ account.methods.offline.uuid.hint=UUIDは、Minecraftのゲームキャラクタ
|
||||
account.methods.offline.uuid.malformed=Malformed
|
||||
account.missing=アカウントなし
|
||||
account.missing.add=追加するにはここをクリック
|
||||
account.not_logged_in=ログインしていません
|
||||
account.password=パスワード
|
||||
account.skin=スキン
|
||||
account.skin.file=スキンイメージファイル
|
||||
@@ -153,7 +144,6 @@ button.ok=OK
|
||||
button.refresh=更新
|
||||
button.remove=削除
|
||||
button.remove.confirm=本当に完全に削除しますか?この操作は元に戻せません!
|
||||
button.retry=リトライ
|
||||
button.save=保存
|
||||
button.save_as=名前を付けて保存
|
||||
button.select_all=すべて選択
|
||||
@@ -300,8 +290,6 @@ download=ダウンロード
|
||||
download.code.404=リモートサーバーにファイルが見つかりません:%s
|
||||
download.content=ゲームコンテンツ
|
||||
download.shader=シェーダー
|
||||
download.existing=ファイルは既に存在するため、保存できません。「名前を付けて保存」を選択して、ファイルを別の場所に保存できます。
|
||||
download.external_link=ダウンロードサイトを開く
|
||||
download.failed=%1$s のダウンロードに失敗しました、応答コード:%2$d
|
||||
download.failed.empty=候補者なし。戻るにはここをクリックしてください。
|
||||
download.failed.refresh=バージョンリストをダウンロードできません。ここをクリックして再試行してください。
|
||||
@@ -419,11 +407,8 @@ game_directory.default=現在のディレクトリ
|
||||
game_directory.home=標準ディレクトリ
|
||||
game_directory.instance_directory=ゲームディレクトリ
|
||||
game_directory.instance_directory.choose=ゲームディレクトリを選択してください
|
||||
game_directory.manage=ゲームディレクトリリスト
|
||||
game_directory.name=名前
|
||||
game_directory.new=新規作成
|
||||
game_directory.title=ゲームディレクトリ
|
||||
game_directory.selected=選択済み
|
||||
game_directory.use_relative_path=可能であれば、ゲームディレクトリの相対パスを使用します
|
||||
|
||||
help=ヘルプ
|
||||
@@ -449,7 +434,6 @@ install.failed.optifine_forge_1.17=Minecraft 1.17.1の場合、OptiFine H1 Pre2
|
||||
install.failed.version_mismatch=ライブラリにはゲームバージョン %s が必要ですが、実際のバージョンは %s です。
|
||||
install.installer.change_version=%s 、このバージョンは現在のゲームバージョンと互換性がありません。別のものを選択するには、ここをクリックしてください。
|
||||
install.installer.choose=%s バージョンを選択してください
|
||||
install.installer.depend=%s に依存
|
||||
install.installer.fabric=Fabric
|
||||
install.installer.fabric-api=Fabric API
|
||||
install.installer.fabric-quilt-api.warning=%1$sはmodであり、新しいゲームのmodディレクトリにインストールされます。新しいゲームの実行ディレクトリを変更しないでください。変更すると、%1$sのインストールが失われます。その設定を変更したい場合は、%1$sを再インストールする必要があります。
|
||||
@@ -458,9 +442,6 @@ install.installer.game=Minecraft
|
||||
install.installer.incompatible=%s と互換性がありません
|
||||
install.installer.install=インストール %s
|
||||
install.installer.install_offline=ファイルからのインストール/更新
|
||||
install.installer.install_offline.tooltip=Forge / OptiFineインストールファイルのインポートをサポートします
|
||||
install.installer.install_online=オンラインでインストール
|
||||
install.installer.install_online.tooltip=Fabric、Forge、OptiFine、LiteLoaderのインストールをサポートします。
|
||||
install.installer.liteloader=LiteLoader
|
||||
install.installer.not_installed=インストールされていません
|
||||
install.installer.optifine=OptiFine
|
||||
@@ -470,9 +451,7 @@ install.installer.version=%s
|
||||
install.modpack=modpackを導入
|
||||
install.new_game=新規作成
|
||||
install.new_game.already_exists=このバージョンはすでに存在します。
|
||||
install.new_game.current_game_version=現在のゲームバージョン
|
||||
install.new_game.malformed=無効な名前
|
||||
install.select=操作を選択します
|
||||
install.success=正常にインストールされました
|
||||
|
||||
instance=ゲーム
|
||||
@@ -542,12 +521,10 @@ lang.default=システム言語を使用する
|
||||
|
||||
launch.advice.java.auto=現在選択されているJavaVMは、ゲームの要件を満たしていません。適切なJavaVMを選択することを許可しますか?または、ゲーム設定で適切なJavaVMを選択することもできます。
|
||||
launch.advice.java.modded_java_7=Minecraft 1.7.2以前では、Java7以前のバージョンが必要です。
|
||||
launch.advice.corrected=JVMの選択はすでに修正されています。選択したJavaバージョンを維持したい場合は、ゲーム設定でJavaVMチェックを無効にすることができます。
|
||||
launch.advice.uncorrected=ゲームを正常に開始できることが確実な場合は、ゲーム設定でJavaVMチェックを無効にすることができます。
|
||||
launch.advice.different_platform=OSは64ビットですが、Javaは32ビットです。64ビットJavaをお勧めします。
|
||||
launch.advice.forge2760_liteloader=Forge 2760以降はLiteLoaderと互換性がないため、Forgeを2773以降にアップグレードすることを検討してください。
|
||||
launch.advice.forge28_2_2_optifine=Forge28.2.2以降のバージョンはOptiFineと互換性がありません。Forgeを28.2.1以前のバージョンにダウングレードすることを検討してください。
|
||||
launch.advice.java8_1_13=Minecraft 1.13以降は、Java8以降でのみ実行できます。
|
||||
launch.advice.java8_51_1_13=Minecraft 1.13は、1.8.0_51より前のJava8でクラッシュする可能性があります。最新バージョンのJava8をインストールしてください。
|
||||
launch.advice.java9=Java9以降のバージョンのJavaでMinecraft1.12以前を起動することはできません。ゲームを高速化するには、Java8をお勧めします。
|
||||
launch.advice.newer_java=多くのMinecraft1.12以降、およびほとんどのModには、Java8が必要です。
|
||||
@@ -596,7 +573,6 @@ launcher.crash=Hello Minecraft!ランチャーがクラッシュしました
|
||||
launcher.crash.hmcl_out_dated=Hello Minecraft!ランチャーがクラッシュしました!ランチャーが古くなっています。ランチャーを更新してください!
|
||||
launcher.update_java=Javaを更新してください。
|
||||
|
||||
login.empty_username=ユーザー名を設定していません!
|
||||
login.enter_password=パスワードを入力してください。
|
||||
|
||||
logwindow.show_lines=行を表示
|
||||
@@ -606,7 +582,6 @@ logwindow.help=HMCL コミュニティにアクセスして、他のユーザー
|
||||
logwindow.autoscroll=Autoscroll
|
||||
logwindow.export_game_crash_logs=ゲームのクラッシュ情報をエクスポートする
|
||||
|
||||
main_page=ホーム
|
||||
|
||||
message.cancelled=操作がキャンセルされました
|
||||
message.confirm=確認
|
||||
@@ -631,7 +606,6 @@ modpack.choose.remote.tooltip=リモートmodpackファイルへの直接ダウ
|
||||
modpack.desc=注意事項と変更ログを含めてmodpackを説明してください。マークダウンとオンライン写真がサポートされています。
|
||||
modpack.description=説明
|
||||
modpack.download=Modpackをダウンロード
|
||||
modpack.enter_name=modpackの名前を入力。
|
||||
modpack.export=Modpackをエクスポート
|
||||
modpack.export.as=Export Modpack As...
|
||||
modpack.file_api=Modpackダウンロードリンクプレフィックス
|
||||
@@ -649,11 +623,9 @@ modpack.files.saves=保存されたマップ
|
||||
modpack.files.scripts=MineTweaker構成
|
||||
modpack.files.servers_dat=サーバーリスト
|
||||
modpack.installing=modpackのインストール
|
||||
modpack.introduction= Curse, Modrinth, MultiMC, MCBBS Modpackがサポートされています。
|
||||
modpack.invalid=無効なmodpackファイル。
|
||||
modpack.mismatched_type=不適切なmodpackタイプ、現在のゲームは %s modpackですが、更新ファイルは %s modpackです。
|
||||
modpack.name=Modpack名
|
||||
modpack.not_a_valid_name=無効なmodpack名
|
||||
modpack.origin=Origin
|
||||
modpack.origin.url=公式ウェブサイト
|
||||
modpack.origin.mcbbs=MCBBS
|
||||
@@ -709,6 +681,11 @@ modrinth.category.worldgen=Worldgen
|
||||
modrinth.category.datapack=Datapack
|
||||
modrinth.category.folia=Folia
|
||||
modrinth.category.8x-=8x以下
|
||||
modrinth.category.32x=32x
|
||||
modrinth.category.48x=48x
|
||||
modrinth.category.64x=64x
|
||||
modrinth.category.128x=128x
|
||||
modrinth.category.256x=256x
|
||||
modrinth.category.512x+=512x以上
|
||||
|
||||
mods=Mods
|
||||
@@ -724,7 +701,6 @@ mods.mcmod=MCMOD
|
||||
mods.mcmod.page=MCMOD
|
||||
mods.mcmod.search=MCMODで検索
|
||||
mods.name=Name
|
||||
mods.not_modded=最初にmodloaderをインストールする必要があります(Fabric、Forge、またはLiteLoader)
|
||||
mods.restore=Restore
|
||||
mods.url=公式ページ
|
||||
mods.update_modpack_mod.warning=modpack 内の mod を更新すると、修復不可能な結果につながる可能性があり、modpack が破損して起動できなくなる可能性があります。更新してもよろしいですか?
|
||||
@@ -777,18 +753,6 @@ settings=設定
|
||||
settings.advanced=詳細設定
|
||||
settings.advanced.title=詳細設定 - %s
|
||||
settings.advanced.custom_commands=カスタムコマンド
|
||||
settings.advanced.custom_commands.hint=カスタムコマンドは、次の環境変数を使用して実行されます。\n \
|
||||
\-$ INST_NAME:バージョンの名前\n \
|
||||
\-$ INST_ID:バージョンの名前\n \
|
||||
\-$ INST_DIR:バージョンの絶対パス\n \
|
||||
\-$ INST_MC_DIR:Minecraftの絶対パス\n \
|
||||
\-$ INST_JAVA:起動に使用されるJavaバイナリ\n \
|
||||
\-$ INST_FORGE:Forgeがインストールされている場合に設定\n \
|
||||
\-$ INST_NEOFORGE:NeoForgeがインストールされている場合に設定\n \
|
||||
\-$ INST_LITELOADER:LiteLoaderがインストールされている場合に設定\n \
|
||||
\-$ INST_OPTIFINE:OptiFineがインストールされている場合に設定\n \
|
||||
\-$ INST_FABRIC:ファブリックがインストールされている場合に設定\n \
|
||||
\-$ INST_QUILT:ファブリックがインストールされている場合に設定
|
||||
settings.advanced.dont_check_game_completeness=ゲームファイルをスキャンしない
|
||||
settings.advanced.dont_check_jvm_validity=JVMがゲームを起動できるかどうかを確認しないでください
|
||||
settings.advanced.game_dir.default=標準(.minecraft /)
|
||||
@@ -807,9 +771,6 @@ settings.advanced.launcher_visible=ランチャーの設定
|
||||
settings.advanced.minecraft_arguments=Minecraftの引数
|
||||
settings.advanced.minecraft_arguments.prompt=デフォルト
|
||||
settings.advanced.natives_directory=ネイティブライブラリパス(例: LWJGL)
|
||||
settings.advanced.natives_directory.choose=ネイティブライブラリパスを選択してください
|
||||
settings.advanced.natives_directory.custom=Custom(ゲームが実行時に必要とするネイティブライブラリを提供する必要があります)
|
||||
settings.advanced.natives_directory.default=Standard(ネイティブライブラリは当社が提供します)
|
||||
settings.advanced.no_jvm_args=デフォルトのJVM引数なし
|
||||
settings.advanced.precall_command=起動前コマンド
|
||||
settings.advanced.precall_command.prompt=ゲーム開始前に実行されます
|
||||
@@ -829,11 +790,9 @@ settings.advanced.renderer.zink=Mesa Zink
|
||||
settings.advanced.server_ip=サーバーアドレス
|
||||
settings.advanced.server_ip.prompt=ゲームの起動時にサーバーに参加する
|
||||
settings.advanced.unsupported_system_options=サポートされていないシステムオプション
|
||||
settings.advanced.use_native_glfw=システムGLFWを使用する
|
||||
settings.advanced.use_native_glfw_or_sdl=システムGLFW / SDLを使用する
|
||||
settings.advanced.use_native_openal=システムOpenALを使用する
|
||||
settings.advanced.linux_freebsd_only=Linux/FreeBSDのみ
|
||||
settings.advanced.workaround=デバッグ用オプション
|
||||
settings.advanced.workaround.warning=デバッグオプションはプロフェッショナルのみ使用可能です。デバッグオプションにより、ゲームが起動しない場合があります。これらのオプションは、ご自分が何をしているのかが分からない限り、変更しないでください。
|
||||
settings.advanced.wrapper_launcher=パッキングオーダー
|
||||
settings.advanced.wrapper_launcher.prompt=optirunが記入されている場合、起動コマンドが "java ... "から変更されます。を「optirun java ...」に変更しました。
|
||||
|
||||
@@ -852,22 +811,18 @@ settings.game.java_directory.choose=Javaディレクトリを選択します。
|
||||
settings.game.java_directory.invalid=Java のパスが正しくありません。
|
||||
settings.game.java_directory.template=%s(%s)
|
||||
settings.game.management=管理
|
||||
settings.game.working_directory=作業ディレクトリ
|
||||
settings.game.working_directory.choose=作業ディレクトリを選択してください
|
||||
|
||||
settings.icon=ゲームアイコン
|
||||
|
||||
settings.launcher=設定
|
||||
settings.launcher.appearance=外観
|
||||
settings.launcher.common_path.tooltip=このアプリは、すべてのダウンロードをここにキャッシュします。
|
||||
settings.launcher.debug=デバッグ
|
||||
settings.launcher.download=ダウンロード
|
||||
settings.launcher.download.threads=スレッド
|
||||
settings.launcher.download.threads.auto=自動
|
||||
settings.launcher.download.threads.hint=同時実行性が大きすぎると、システムがフリーズする可能性があります。ダウンロード速度は、ICPと宛先サーバーの影響を受ける可能性があります。
|
||||
settings.launcher.download_source=ソースのダウンロード
|
||||
settings.launcher.download_source.auto=ダウンロードソースの自動選択
|
||||
settings.launcher.enable_game_list=メインページにバージョンリストを表示する
|
||||
settings.launcher.font=フォント
|
||||
settings.launcher.general=全般的
|
||||
settings.launcher.language=言語
|
||||
@@ -905,9 +860,6 @@ settings.take_effect_after_restart=再起動後に有効になります
|
||||
settings.type=バージョン設定タイプ
|
||||
settings.type.global.manage=グローバルゲーム設定
|
||||
|
||||
sponsor=寄付
|
||||
sponsor.bmclapi=中国本土向けのダウンロードサービスはBMCLAPIによって提供されています。詳細については、ここをクリックしてください。
|
||||
sponsor.hmcl=Hello Minecraft! Launcherは、無料のオープンソースのMinecraftランチャーであり、ユーザーは複数の個別のMinecraftインストールを簡単に管理できます。詳細については、ここをクリックしてください。
|
||||
|
||||
system.architecture=Arch
|
||||
system.operating_system=OS
|
||||
@@ -933,7 +885,6 @@ update.newest_version=最新バージョン:%s
|
||||
update.bubble.title=利用可能な更新:%s
|
||||
update.bubble.subtitle=更新するにはここをクリックしてください
|
||||
update.latest=これは最新バージョンです。
|
||||
update.no_browser=ブラウザを開くことができません。リンクがクリップボードにコピーされました。ブラウザのアドレスバーに貼り付けて更新します。
|
||||
update.tooltip=更新
|
||||
|
||||
wiki.version.game=https://ja.minecraft.wiki/w/Java_Edition_%s
|
||||
|
||||
Loaded 100 of 270 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user