Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bdacd65473 | ||
|
|
f176fbd1fd | ||
|
|
9d43a6fc0f | ||
|
|
23811dad7f | ||
|
|
e792bfc554 | ||
|
|
d6b4af2d96 | ||
|
|
2a915460ae | ||
|
|
e9b577f66f | ||
|
|
e12c7e53f6 | ||
|
|
61caf8a925 |
No files matched your search
@@ -1,21 +0,0 @@
|
||||
name: Check Codes Comment
|
||||
|
||||
# Disabled on this Gitea fork: the upstream flow depends on reviewdog
|
||||
# (missing from git.nest.potatolab.uk) and a checkstyle-artifact produced by
|
||||
# check-codes.yml. Re-enable only after those actions are mirrored or the
|
||||
# comment path is rewritten without reviewdog.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
runs-on: ubuntu-latest
|
||||
if: false
|
||||
steps:
|
||||
- name: Disabled
|
||||
run: echo "check-codes-comment is disabled on this fork"
|
||||
@@ -1,35 +0,0 @@
|
||||
name: Check Codes
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- '**.java'
|
||||
- '**.properties'
|
||||
- '.github/workflows/check-codes.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- '**.java'
|
||||
- '**.properties'
|
||||
- '.github/workflows/check-codes.yml'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Setup JDK 17
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '17'
|
||||
java-package: 'jdk+fx'
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v5
|
||||
with:
|
||||
cache-cleanup: never
|
||||
# Keep only Gradle checks. reviewdog/action-setup is not mirrored on
|
||||
# git.nest.potatolab.uk (404), so PR-comment artifact steps are omitted
|
||||
# for Gitea Actions / act runners.
|
||||
- name: Check Codes
|
||||
run: ./gradlew checkstyle checkTranslations --no-daemon --parallel --stacktrace
|
||||
@@ -1,11 +1,22 @@
|
||||
name: Java CI
|
||||
|
||||
# Pushes to main and pull requests only. A branch with an open pull request used to build twice
|
||||
# (once for the push, once for the pull request), which kept the runners queued.
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- '*'
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
|
||||
# A newer run for the same pull request or branch replaces the older one.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
@@ -23,12 +34,26 @@ jobs:
|
||||
uses: gradle/actions/setup-gradle@v5
|
||||
with:
|
||||
cache-cleanup: never
|
||||
# Only main writes the dependency cache; pull requests read it.
|
||||
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
|
||||
# The repository's wrapper downloads Gradle itself, so cache that download too.
|
||||
- name: Cache Gradle distribution
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/wrapper
|
||||
.gradle/wrapper-neo
|
||||
key: gradle-dist-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/wrapper/GradleWrapperNeo.java') }}
|
||||
- name: Install Xvfb
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y xvfb
|
||||
- name: Build with Gradle
|
||||
run: xvfb-run --auto-servernum ./gradlew build --no-daemon --parallel
|
||||
# Installing is the slowest setup step, so skip it when the runner already has xvfb, and
|
||||
# only refresh the package index when a plain install fails.
|
||||
command -v xvfb-run >/dev/null || sudo apt-get install -y --no-install-recommends xvfb \
|
||||
|| { sudo apt-get update && sudo apt-get install -y --no-install-recommends xvfb; }
|
||||
# One Gradle run builds, tests and runs the code and translation checks, so the
|
||||
# project is configured and compiled once instead of in two workflows.
|
||||
- name: Build and check with Gradle
|
||||
run: xvfb-run --auto-servernum ./gradlew build checkstyle checkTranslations --no-daemon --parallel
|
||||
env:
|
||||
JAVA_TOOL_OPTIONS: -Djava.awt.headless=true
|
||||
MICROSOFT_AUTH_ID: ${{ secrets.MICROSOFT_AUTH_ID }}
|
||||
|
||||
@@ -77,3 +77,6 @@ language-subtag-registry
|
||||
!/.gemini/config.yaml
|
||||
.codex
|
||||
/.gradle-*/
|
||||
|
||||
# Managed Java downloaded by the launcher when it is run from the repository root
|
||||
/java/
|
||||
@@ -141,6 +141,30 @@ public final class Metadata {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Default client Server Modpack fileApi published by TDLMPack (Gitea generic package `latest`).
|
||||
public static final String DEFAULT_CLIENT_FILE_API =
|
||||
"https://git.nest.potatolab.uk:8443/api/packages/TDLM/generic/tdlm-client/latest";
|
||||
|
||||
/// Stable client update root (`server-manifest.json` and `overrides/` live under this URL).
|
||||
///
|
||||
/// Override with system property `hmcl.client.fileApi` or jar attribute `hmcl.client.fileApi`.
|
||||
public static String getClientFileApi() {
|
||||
String fromProperty = System.getProperty("hmcl.client.fileApi", "").trim();
|
||||
if (StringUtils.isNotBlank(fromProperty)) {
|
||||
return StringUtils.removeSuffix(fromProperty, "/");
|
||||
}
|
||||
String fromJar = JarUtils.getAttribute("hmcl.client.fileApi", null);
|
||||
if (StringUtils.isNotBlank(fromJar)) {
|
||||
return StringUtils.removeSuffix(fromJar.trim(), "/");
|
||||
}
|
||||
return DEFAULT_CLIENT_FILE_API;
|
||||
}
|
||||
|
||||
/// Absolute URL of the client `server-manifest.json` used for first install and differential updates.
|
||||
public static String getClientManifestUrl() {
|
||||
return getClientFileApi() + "/server-manifest.json";
|
||||
}
|
||||
|
||||
/// Server address used when the installed modpack manifest does not provide one.
|
||||
///
|
||||
/// Can be set at build or run time with the `hmcl.server.address` system property; empty by default.
|
||||
@@ -150,9 +174,10 @@ public final class Metadata {
|
||||
|
||||
/// The server name shown in the title bar.
|
||||
///
|
||||
/// Can be set with the `hmcl.server.name` system property; empty by default.
|
||||
/// Can be set with the `hmcl.server.name` system property; defaults to `TDLM`.
|
||||
public static String getServerName() {
|
||||
return System.getProperty("hmcl.server.name", "").trim();
|
||||
String name = System.getProperty("hmcl.server.name", "TDLM").trim();
|
||||
return name.isEmpty() ? "TDLM" : name;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -27,11 +27,16 @@ 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.glavo.url.WebURL;
|
||||
import org.jackhuang.hmcl.modpack.Modpack;
|
||||
import org.jackhuang.hmcl.modpack.server.ServerModpackManifest;
|
||||
import org.jackhuang.hmcl.setting.*;
|
||||
import org.jackhuang.hmcl.task.GetTask;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.ui.account.AccountListPage;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
@@ -354,35 +359,59 @@ public final class Controllers {
|
||||
);
|
||||
}
|
||||
|
||||
/// Installs the package found by [Metadata#findBundledModpackFile()] asynchronously, if present.
|
||||
///
|
||||
/// Shows progress without a cancel action and attempts to delete the package after success.
|
||||
/// Failures are logged; a retained package may be retried at the next startup.
|
||||
/// Installs a launcher-bundled modpack when present; otherwise, when the repository has no
|
||||
/// instances yet, pulls the org client [Metadata#getClientManifestUrl] and opens the install wizard.
|
||||
private static void tryInstallBundledModpack(HMCLGameRepository repository) {
|
||||
@Nullable Path modpackFile = Metadata.findBundledModpackFile();
|
||||
if (modpackFile == null) {
|
||||
if (modpackFile != null) {
|
||||
LOG.info("Found bundled modpack at " + modpackFile + "; starting automatic install");
|
||||
|
||||
Controllers.taskDialog(
|
||||
Task.composeAsync(Schedulers.io(), () -> {
|
||||
Modpack modpack = ModpackHelper.readModpackManifest(modpackFile);
|
||||
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
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.info("Found bundled modpack at " + modpackFile + "; starting automatic install");
|
||||
if (repository.getDisplayInstances().findAny().isPresent()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String manifestUrl = Metadata.getClientManifestUrl();
|
||||
LOG.info("No instances yet; fetching client Server Modpack from " + manifestUrl);
|
||||
Controllers.taskDialog(
|
||||
Task.composeAsync(Schedulers.io(), () -> {
|
||||
Modpack modpack = ModpackHelper.readModpackManifest(modpackFile);
|
||||
return ModpackHelper.getInstallTask(
|
||||
repository, modpackFile, new GameInstanceID(modpack.getName()), modpack, null);
|
||||
})
|
||||
.whenComplete(Schedulers.javafx(), (ignored, exception) -> {
|
||||
new GetTask(WebURL.parse(manifestUrl))
|
||||
.whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception != null) {
|
||||
LOG.warning("Failed to install bundled modpack", exception);
|
||||
LOG.warning("Failed to fetch client Server Modpack manifest", exception);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Files.deleteIfExists(modpackFile);
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to delete bundled modpack: " + modpackFile, e);
|
||||
ServerModpackManifest manifest =
|
||||
JsonUtils.fromMaybeMalformedJson(result, ServerModpackManifest.class);
|
||||
if (manifest == null) {
|
||||
LOG.warning("Malformed client Server Modpack manifest from " + manifestUrl);
|
||||
return;
|
||||
}
|
||||
}), i18n("modpack.installing"), TaskCancellationAction.NO_CANCEL
|
||||
Controllers.getDecorator().startWizard(
|
||||
new ModpackInstallWizardProvider(repository, manifest),
|
||||
i18n("install.modpack"));
|
||||
}),
|
||||
i18n("message.downloading"),
|
||||
TaskCancellationAction.NO_CANCEL
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ import org.jackhuang.hmcl.util.javafx.MappedObservableList;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.UnmodifiableView;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.createSelectedItemPropertyFor;
|
||||
@@ -78,7 +77,7 @@ public final class AccountListPage extends DecoratorAnimatedPage implements Deco
|
||||
getSidebar().clear();
|
||||
|
||||
getSidebar()
|
||||
.startCategory(i18n("account.create").toUpperCase(Locale.ROOT))
|
||||
.startCategory(i18n("account.create"))
|
||||
.add(offlineItem)
|
||||
.add(microsoftItem);
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
@@ -19,6 +20,8 @@ import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class AccountListPopupMenu extends StackPane {
|
||||
@@ -69,20 +72,6 @@ public final class AccountListPopupMenu extends StackPane {
|
||||
box.add(item);
|
||||
}
|
||||
|
||||
// Footer actions: add another account, or open the full account page.
|
||||
AdvancedListItem addItem = footerItem(SVG.ADD_CIRCLE, i18n("account.add"));
|
||||
addItem.setOnAction(e -> {
|
||||
hidePopup();
|
||||
Controllers.dialog(new CreateAccountPane());
|
||||
});
|
||||
AdvancedListItem manageItem = footerItem(SVG.SETTINGS, i18n("account.open_manage"));
|
||||
manageItem.setOnAction(e -> {
|
||||
hidePopup();
|
||||
Controllers.navigate(Controllers.getAccountListPage());
|
||||
});
|
||||
box.add(addItem);
|
||||
box.add(manageItem);
|
||||
|
||||
// https://github.com/HMCL-dev/HMCL/issues/5343
|
||||
Platform.runLater(() -> box.setVvalue(0.0));
|
||||
};
|
||||
@@ -92,9 +81,31 @@ public final class AccountListPopupMenu extends StackPane {
|
||||
Label placeholder = new Label(i18n("account.empty"));
|
||||
placeholder.setStyle("-fx-padding: 10px; -fx-text-fill: -monet-on-surface-variant; -fx-font-style: italic;");
|
||||
|
||||
FXUtils.onChangeAndOperate(isEmpty, empty -> {
|
||||
getChildren().setAll(empty ? placeholder : box);
|
||||
// Footer actions stay visible even when there is no account yet: add one, or open the full account page.
|
||||
AdvancedListItem addItem = footerItem(SVG.ADD_CIRCLE, i18n("account.add"));
|
||||
addItem.setOnAction(e -> {
|
||||
hidePopup();
|
||||
// The first account uses the short name dialog; later ones can pick any login method.
|
||||
if (Accounts.getAccounts().isEmpty()) {
|
||||
Controllers.dialog(new FirstRunAccountDialog());
|
||||
} else {
|
||||
Controllers.dialog(new CreateAccountPane());
|
||||
}
|
||||
});
|
||||
AdvancedListItem manageItem = footerItem(SVG.SETTINGS, i18n("account.open_manage"));
|
||||
manageItem.setOnAction(e -> {
|
||||
hidePopup();
|
||||
Controllers.navigate(Controllers.getAccountListPage());
|
||||
});
|
||||
VBox footer = new VBox(addItem, manageItem);
|
||||
footer.setPrefWidth(220);
|
||||
|
||||
VBox content = new VBox();
|
||||
FXUtils.onChangeAndOperate(isEmpty, empty -> {
|
||||
footer.getChildren().setAll(empty ? List.of(addItem) : List.of(addItem, manageItem));
|
||||
content.getChildren().setAll(empty ? List.of(placeholder, footer) : List.of(box, footer));
|
||||
});
|
||||
getChildren().setAll(content);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -135,6 +135,7 @@ public class ComponentList extends Control implements NoPaddingComponent {
|
||||
|
||||
{
|
||||
Label advanced = new Label(title);
|
||||
advanced.getStyleClass().add("component-list-title");
|
||||
node.getChildren().setAll(advanced);
|
||||
}
|
||||
|
||||
|
||||
+20
-3
@@ -28,25 +28,40 @@ public final class ModpackInstallWizardProvider implements WizardProvider {
|
||||
private final HMCLGameRepository repository;
|
||||
private final Path file;
|
||||
private final String updateVersion;
|
||||
private final ServerModpackManifest remoteManifest;
|
||||
private String iconUrl;
|
||||
private boolean hasSource;
|
||||
|
||||
public ModpackInstallWizardProvider(HMCLGameRepository repository) {
|
||||
this(repository, null, null);
|
||||
this(repository, null, null, null);
|
||||
}
|
||||
|
||||
public ModpackInstallWizardProvider(HMCLGameRepository repository, Path modpackFile) {
|
||||
this(repository, modpackFile, null);
|
||||
this(repository, modpackFile, null, null);
|
||||
}
|
||||
|
||||
public ModpackInstallWizardProvider(HMCLGameRepository repository, GameInstanceID updateInstanceId) {
|
||||
this(repository, null, updateInstanceId);
|
||||
this(repository, null, updateInstanceId, null);
|
||||
}
|
||||
|
||||
/// Starts the wizard with a remote Server Modpack manifest (TDLM fileApi install).
|
||||
public ModpackInstallWizardProvider(HMCLGameRepository repository, ServerModpackManifest remoteManifest) {
|
||||
this(repository, null, null, remoteManifest);
|
||||
}
|
||||
|
||||
public ModpackInstallWizardProvider(HMCLGameRepository repository, Path modpackFile, GameInstanceID updateInstanceId) {
|
||||
this(repository, modpackFile, updateInstanceId, null);
|
||||
}
|
||||
|
||||
private ModpackInstallWizardProvider(
|
||||
HMCLGameRepository repository,
|
||||
Path modpackFile,
|
||||
GameInstanceID updateInstanceId,
|
||||
ServerModpackManifest remoteManifest) {
|
||||
this.repository = repository;
|
||||
this.file = modpackFile;
|
||||
this.updateVersion = updateInstanceId != null ? updateInstanceId.toString() : null;
|
||||
this.remoteManifest = remoteManifest;
|
||||
}
|
||||
|
||||
public void setIconUrl(String iconUrl) {
|
||||
@@ -57,6 +72,8 @@ public final class ModpackInstallWizardProvider implements WizardProvider {
|
||||
public void start(SettingsMap settings) {
|
||||
if (file != null)
|
||||
settings.put(LocalModpackPage.MODPACK_FILE, file);
|
||||
if (remoteManifest != null)
|
||||
settings.put(RemoteModpackPage.MODPACK_SERVER_MANIFEST, remoteManifest);
|
||||
if (updateVersion != null)
|
||||
settings.put(LocalModpackPage.MODPACK_NAME, updateVersion);
|
||||
if (StringUtils.isNotBlank(iconUrl))
|
||||
|
||||
@@ -13,6 +13,9 @@ import org.jackhuang.hmcl.download.game.GameDownloadTask;
|
||||
import org.jackhuang.hmcl.download.game.GameLibrariesTask;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.setting.*;
|
||||
import org.glavo.url.WebURL;
|
||||
import org.jackhuang.hmcl.modpack.server.ServerModpackManifest;
|
||||
import org.jackhuang.hmcl.task.GetTask;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
@@ -45,9 +48,10 @@ public final class Instances {
|
||||
private Instances() {
|
||||
}
|
||||
|
||||
/// Installs or updates the server client from the launcher-bundled modpack when present.
|
||||
/// Installs or updates the TDLM client.
|
||||
///
|
||||
/// Falls back to the local/remote modpack import wizard when no bundled package is found.
|
||||
/// Prefers a launcher-bundled zip, then the org [Metadata#getClientManifestUrl] Server Modpack,
|
||||
/// then the generic local/remote import wizard.
|
||||
public static void installServerModpack() {
|
||||
HMCLGameRepository repository = GameDirectoryManager.getSelectedRepository();
|
||||
if (!repository.isLoaded()) {
|
||||
@@ -62,7 +66,33 @@ public final class Instances {
|
||||
return;
|
||||
}
|
||||
|
||||
importModpack();
|
||||
Controllers.taskDialog(
|
||||
new GetTask(WebURL.parse(Metadata.getClientManifestUrl()))
|
||||
.whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception != null) {
|
||||
Controllers.dialog(
|
||||
exception.getMessage(),
|
||||
i18n("message.error"),
|
||||
MessageDialogPane.MessageType.ERROR);
|
||||
importModpack();
|
||||
return;
|
||||
}
|
||||
ServerModpackManifest manifest = JsonUtils.fromMaybeMalformedJson(
|
||||
result, ServerModpackManifest.class);
|
||||
if (manifest == null) {
|
||||
Controllers.dialog(
|
||||
i18n("modpack.type.server.malformed"),
|
||||
i18n("message.error"),
|
||||
MessageDialogPane.MessageType.ERROR);
|
||||
importModpack();
|
||||
return;
|
||||
}
|
||||
Controllers.getDecorator().startWizard(
|
||||
new ModpackInstallWizardProvider(repository, manifest),
|
||||
i18n("install.modpack"));
|
||||
}),
|
||||
i18n("message.downloading"),
|
||||
TaskCancellationAction.NORMAL);
|
||||
}
|
||||
|
||||
public static void importModpack() {
|
||||
|
||||
@@ -2349,3 +2349,78 @@
|
||||
-fx-pref-height: 18px;
|
||||
-fx-max-height: 18px;
|
||||
}
|
||||
|
||||
/* ---- Unified list and card styling ---- */
|
||||
|
||||
/* Sidebar section labels: small muted text without an underline. */
|
||||
.class-title {
|
||||
-fx-font-family: "Noto Sans SC";
|
||||
-fx-padding: 13px 16px 4px 16px;
|
||||
}
|
||||
|
||||
.class-title Text {
|
||||
-fx-fill: -monet-on-surface-variant;
|
||||
}
|
||||
|
||||
.class-title Rectangle {
|
||||
-fx-fill: transparent;
|
||||
}
|
||||
|
||||
/* Cards: a hairline border instead of a drop shadow. */
|
||||
.card, .card-no-padding, .card-non-transparent {
|
||||
-fx-background-color: -monet-surface-bright;
|
||||
-fx-border-color: -monet-outline-variant;
|
||||
-fx-border-radius: 13px;
|
||||
-fx-effect: null;
|
||||
}
|
||||
|
||||
/* Icon buttons use a softer ink than body text. */
|
||||
.toggle-icon4 .svg, .toggle-icon-tiny .svg {
|
||||
-fx-fill: -monet-on-surface-variant;
|
||||
}
|
||||
|
||||
/* ---- Settings lists ---- */
|
||||
.component-list-title {
|
||||
-fx-font-family: "Noto Sans SC";
|
||||
-fx-font-weight: bold;
|
||||
-fx-font-size: 14px;
|
||||
-fx-text-fill: -monet-on-surface;
|
||||
}
|
||||
|
||||
.options-list {
|
||||
-fx-background-radius: 13px;
|
||||
-fx-border-color: -monet-outline-variant;
|
||||
-fx-border-radius: 13px;
|
||||
-fx-effect: null;
|
||||
}
|
||||
|
||||
.options-list-item {
|
||||
-fx-background-color: -monet-surface-bright;
|
||||
}
|
||||
|
||||
.options-list-item:first {
|
||||
-fx-background-radius: 13px 13px 0 0;
|
||||
}
|
||||
|
||||
.options-list-item:last {
|
||||
-fx-background-radius: 0 0 13px 13px;
|
||||
}
|
||||
|
||||
.options-list-item:first:last {
|
||||
-fx-background-radius: 13px;
|
||||
}
|
||||
|
||||
/* Sidebar rows: a lighter selected and hover tint. */
|
||||
.advanced-list-item:selected .rippler-container,
|
||||
.advanced-list-item .rippler-container:hover {
|
||||
-fx-background-color: -monet-surface-container-high;
|
||||
}
|
||||
|
||||
/* Outlined buttons: one crisp hairline. */
|
||||
.jfx-button-border {
|
||||
-fx-border-color: -monet-outline-variant;
|
||||
-fx-border-radius: 8px;
|
||||
-fx-border-width: 1px;
|
||||
-fx-background-radius: 8px;
|
||||
-fx-padding: 8px 16px;
|
||||
}
|
||||
@@ -327,6 +327,10 @@ public class LaunchOptions implements Serializable {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Path getGameDir() {
|
||||
return options.gameDir;
|
||||
}
|
||||
|
||||
public Builder setJava(JavaRuntime java) {
|
||||
options.java = java;
|
||||
return this;
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.modpack.server;
|
||||
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
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.*;
|
||||
import org.jackhuang.hmcl.modpack.multimc.MultiMCComponents;
|
||||
import org.jackhuang.hmcl.modpack.multimc.MultiMCInstancePatch;
|
||||
import org.jackhuang.hmcl.modpack.multimc.MultiMCManifest;
|
||||
import org.jackhuang.hmcl.task.GetTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.*;
|
||||
|
||||
/// Applies the default-on lwjgl3ify MultiMC companion under {@link Lwjgl3ifySupport#ROOT}.
|
||||
///
|
||||
/// Replaces the plain Forge install produced by {@link org.jackhuang.hmcl.download.GameBuilder} with the
|
||||
/// resolved MultiMC artifact (libraries, JVM args, bootstrap main class) so Java 25 launches work
|
||||
/// without a player toggle.
|
||||
@NotNullByDefault
|
||||
public final class Lwjgl3ifyApplyTask extends Task<Void> {
|
||||
private final DefaultDependencyManager dependencyManager;
|
||||
private final DefaultGameRepository repository;
|
||||
private final GameInstanceID instanceId;
|
||||
private final @Nullable DefaultGameInstance updateTarget;
|
||||
private final List<Task<?>> dependents = new ArrayList<>();
|
||||
private final List<Task<?>> dependencies = new ArrayList<>();
|
||||
private @Nullable MultiMCInstancePatch.ResolvedInstance artifact;
|
||||
|
||||
public Lwjgl3ifyApplyTask(
|
||||
DefaultDependencyManager dependencyManager,
|
||||
GameInstanceID instanceId,
|
||||
@Nullable DefaultGameInstance updateTarget) {
|
||||
this.dependencyManager = dependencyManager;
|
||||
this.repository = dependencyManager.getGameRepository();
|
||||
this.instanceId = instanceId;
|
||||
this.updateTarget = updateTarget;
|
||||
setStage("hmcl.modpack");
|
||||
setSignificance(TaskSignificance.MODERATE);
|
||||
|
||||
Path instanceRoot = repository.getLayout().getInstanceRoot(instanceId);
|
||||
if (!Lwjgl3ifySupport.isPresent(instanceRoot)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
dependents.add(new AssemblePatchesTask(instanceRoot));
|
||||
} catch (IOException e) {
|
||||
dependents.add(Task.failed(e));
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs after Server Modpack file sync when the companion is present; otherwise a completed no-op.
|
||||
public static Task<?> afterCompletion(DefaultDependencyManager dependencyManager, DefaultGameInstance instance) {
|
||||
if (!Lwjgl3ifySupport.isPresent(instance.getInstanceRoot())) {
|
||||
return Task.completed(null);
|
||||
}
|
||||
return new Lwjgl3ifyApplyTask(dependencyManager, instance.getId(), instance);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Task<?>> getDependents() {
|
||||
return dependents;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Task<?>> getDependencies() {
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
Path instanceRoot = repository.getLayout().getInstanceRoot(instanceId);
|
||||
if (!Lwjgl3ifySupport.isPresent(instanceRoot)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@Nullable AssemblePatchesTask assemble = null;
|
||||
for (Task<?> dependent : dependents) {
|
||||
if (dependent instanceof AssemblePatchesTask patchesTask) {
|
||||
assemble = patchesTask;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Objects.requireNonNull(assemble, "lwjgl3ify patch assemble task");
|
||||
artifact = MultiMCInstancePatch.resolveArtifact(assemble.getResult(), instanceId);
|
||||
|
||||
Path embeddedLibraries = Lwjgl3ifySupport.root(instanceRoot).resolve("libraries");
|
||||
if (Files.isDirectory(embeddedLibraries)) {
|
||||
FileUtils.copyDirectory(embeddedLibraries, instanceRoot.resolve("libraries"));
|
||||
}
|
||||
|
||||
for (Library library : artifact.getManifest().getLibraries()) {
|
||||
if ("local".equals(library.hint())) {
|
||||
Path from = repository.getLayout().getLibraryFile(artifact.getManifest().id(), library);
|
||||
Path target = repository.getLayout().getLibraryFile(artifact.getManifest().id(), library.withoutCommunityFields());
|
||||
if (Files.exists(from)) {
|
||||
Files.createDirectories(target.getParent());
|
||||
Files.copy(from, target, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try (InputStream input = Objects.requireNonNull(
|
||||
Lwjgl3ifyApplyTask.class.getResourceAsStream("/assets/game/HMCLMultiMCBootstrap-1.0.jar"),
|
||||
"Bundled HMCLMultiMCBootstrap is missing.")) {
|
||||
Path libraryPath = repository.getLayout().getLibraryFile(artifact.getManifest().id(), MultiMCInstancePatch.BOOTSTRAP_LIBRARY);
|
||||
Files.createDirectories(libraryPath.getParent());
|
||||
Files.copy(input, libraryPath, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
|
||||
GameInstanceManifest instanceManifest = artifact.getManifest();
|
||||
repository.save(instanceManifest);
|
||||
|
||||
dependencies.add(new GameAssetDownloadTask(dependencyManager, instanceManifest, GameAssetDownloadTask.DOWNLOAD_INDEX_FORCIBLY, true));
|
||||
dependencies.add(new GameLibrariesTask(
|
||||
dependencyManager,
|
||||
instanceManifest.withLibraries(Lang.merge(instanceManifest.getLibraries(), artifact.getMavenOnlyFiles())),
|
||||
true));
|
||||
|
||||
Path instanceJar = getPrimaryJarFile();
|
||||
dependencies.add(new GameDownloadTask(dependencyManager, instanceManifest)
|
||||
.thenAcceptAsync(cachedJar -> FileUtils.copyFile(cachedJar, instanceJar))
|
||||
.withStage("hmcl.modpack.download"));
|
||||
}
|
||||
|
||||
private Path getPrimaryJarFile() {
|
||||
if (updateTarget == null) {
|
||||
return repository.getLayout().getInstanceJarFile(instanceId);
|
||||
}
|
||||
Path manifestFile = updateTarget.getManifestFile();
|
||||
return manifestFile.resolveSibling(FileUtils.getNameWithoutExtension(manifestFile) + ".jar");
|
||||
}
|
||||
|
||||
private static final class AssemblePatchesTask extends Task<List<MultiMCInstancePatch>> {
|
||||
private final Path companionRoot;
|
||||
private final String mcVersion;
|
||||
private final List<Task<MultiMCInstancePatch>> patches = new ArrayList<>();
|
||||
|
||||
AssemblePatchesTask(Path instanceRoot) throws IOException {
|
||||
this.companionRoot = Lwjgl3ifySupport.root(instanceRoot);
|
||||
MultiMCManifest mmcPack = Lwjgl3ifySupport.readMmcPack(instanceRoot);
|
||||
List<MultiMCManifest.MultiMCManifestComponent> components = Objects.requireNonNull(
|
||||
mmcPack.getComponents(), "components");
|
||||
|
||||
String resolvedMc = null;
|
||||
for (MultiMCManifest.MultiMCManifestComponent component : components) {
|
||||
if (MultiMCComponents.getComponent(component.getUid()) == GameComponentType.GAME) {
|
||||
resolvedMc = component.getVersion();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (resolvedMc == null) {
|
||||
throw new IOException("lwjgl3ify mmc-pack.json has no Minecraft component");
|
||||
}
|
||||
this.mcVersion = resolvedMc;
|
||||
|
||||
for (MultiMCManifest.MultiMCManifestComponent component : components) {
|
||||
String componentID = Objects.requireNonNull(component.getUid(), "Component ID");
|
||||
Path patchPath = companionRoot.resolve("patches").resolve(componentID + ".json");
|
||||
if (Files.isRegularFile(patchPath)) {
|
||||
MultiMCInstancePatch patch = MultiMCInstancePatch.read(componentID, Files.readString(patchPath));
|
||||
patches.add(Task.supplyAsync(() -> patch));
|
||||
} else {
|
||||
patches.add(new GetTask(MultiMCComponents.getMetaURL(componentID, component.getVersion(), mcVersion))
|
||||
.thenApplyAsync(s -> MultiMCInstancePatch.read(componentID, s)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<? extends Task<?>> getDependents() {
|
||||
return patches;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
Map<String, MultiMCInstancePatch> existed = new LinkedHashMap<>();
|
||||
for (Task<MultiMCInstancePatch> patch : patches) {
|
||||
MultiMCInstancePatch result = patch.getResult();
|
||||
existed.put(result.getID(), result);
|
||||
}
|
||||
|
||||
checking:
|
||||
while (true) {
|
||||
for (MultiMCInstancePatch patch : existed.values()) {
|
||||
for (MultiMCManifest.MultiMCManifestCachedRequires require : patch.getRequires()) {
|
||||
String componentID = require.getID();
|
||||
if (!existed.containsKey(componentID)) {
|
||||
Task<MultiMCInstancePatch> task = new GetTask(MultiMCComponents.getMetaURL(
|
||||
componentID,
|
||||
Lang.requireNonNullElse(require.getEqualsVersion(), require.getSuggests()),
|
||||
mcVersion
|
||||
)).thenApplyAsync(s -> MultiMCInstancePatch.read(componentID, s));
|
||||
task.run();
|
||||
MultiMCInstancePatch result = Objects.requireNonNull(task.getResult());
|
||||
existed.put(result.getID(), result);
|
||||
continue checking;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
setResult(new ArrayList<>(existed.values()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.modpack.server;
|
||||
|
||||
import org.jackhuang.hmcl.game.LaunchOptions;
|
||||
import org.jackhuang.hmcl.modpack.multimc.MultiMCInstancePatch;
|
||||
import org.jackhuang.hmcl.modpack.multimc.MultiMCManifest;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/// Default-on lwjgl3ify companion shipped under {@code tdlm/lwjgl3ify/} in the TDLM Server Modpack.
|
||||
@NotNullByDefault
|
||||
public final class Lwjgl3ifySupport {
|
||||
/// Instance-root-relative directory written by the TDLM pack builder.
|
||||
public static final String ROOT = "tdlm/lwjgl3ify";
|
||||
|
||||
private Lwjgl3ifySupport() {
|
||||
}
|
||||
|
||||
/// Returns whether the instance carries an enabled lwjgl3ify MultiMC companion.
|
||||
public static boolean isPresent(Path instanceRoot) {
|
||||
Path root = root(instanceRoot);
|
||||
return Files.isRegularFile(root.resolve("enabled")) && Files.isRegularFile(root.resolve("mmc-pack.json"));
|
||||
}
|
||||
|
||||
/// Returns the companion root under the instance.
|
||||
public static Path root(Path instanceRoot) {
|
||||
return instanceRoot.resolve(ROOT);
|
||||
}
|
||||
|
||||
/// Injects {@code +jvmArgs} from every local patch when the companion is present.
|
||||
///
|
||||
/// Version-json application via {@link Lwjgl3ifyApplyTask} is the primary path; this covers launches
|
||||
/// where the resolved manifest has not yet absorbed those args.
|
||||
public static void injectLaunchOptions(Path instanceRoot, LaunchOptions.Builder builder) {
|
||||
if (!isPresent(instanceRoot)) {
|
||||
return;
|
||||
}
|
||||
Path patches = root(instanceRoot).resolve("patches");
|
||||
if (!Files.isDirectory(patches)) {
|
||||
return;
|
||||
}
|
||||
List<String> args = new ArrayList<>();
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(patches, "*.json")) {
|
||||
for (Path patchFile : stream) {
|
||||
try {
|
||||
MultiMCInstancePatch patch = MultiMCInstancePatch.read(
|
||||
patchFile.getFileName().toString(), Files.readString(patchFile));
|
||||
args.addAll(patch.getJvmArgs());
|
||||
} catch (IOException | RuntimeException e) {
|
||||
LOG.warning("Failed to read lwjgl3ify patch " + patchFile, e);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to list lwjgl3ify patches under " + patches, e);
|
||||
return;
|
||||
}
|
||||
for (String arg : args) {
|
||||
if (!builder.getJavaArguments().contains(arg)) {
|
||||
builder.getJavaArguments().add(arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses {@code mmc-pack.json} from the companion root.
|
||||
public static MultiMCManifest readMmcPack(Path instanceRoot) throws IOException {
|
||||
Path mmcPack = root(instanceRoot).resolve("mmc-pack.json");
|
||||
return JsonUtils.fromNonNullJson(Files.readString(mmcPack), MultiMCManifest.class);
|
||||
}
|
||||
}
|
||||
+6
@@ -32,6 +32,7 @@ public class ServerModpackLocalInstallTask extends Task<Void> {
|
||||
/// Existing instance selecting update mode, or `null` for a new installation.
|
||||
private final @Nullable DefaultGameInstance updateTarget;
|
||||
|
||||
private final DefaultDependencyManager dependencyManager;
|
||||
private final DefaultGameRepository repository;
|
||||
private final List<Task<?>> dependencies = new ArrayList<>();
|
||||
private final List<Task<?>> dependents = new ArrayList<>(4);
|
||||
@@ -97,6 +98,7 @@ public class ServerModpackLocalInstallTask extends Task<Void> {
|
||||
this.manifest = manifest;
|
||||
this.instanceId = instanceId;
|
||||
this.updateTarget = updateTarget;
|
||||
this.dependencyManager = dependencyManager;
|
||||
this.repository = dependencyManager.getGameRepository();
|
||||
Path run = repository.getLayout().getInstanceRoot(instanceId);
|
||||
|
||||
@@ -148,5 +150,9 @@ public class ServerModpackLocalInstallTask extends Task<Void> {
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
Path instanceRoot = repository.getLayout().getInstanceRoot(instanceId);
|
||||
if (Lwjgl3ifySupport.isPresent(instanceRoot)) {
|
||||
dependencies.add(new Lwjgl3ifyApplyTask(dependencyManager, instanceId, updateTarget));
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
-1
@@ -6,6 +6,7 @@ import com.google.gson.JsonParseException;
|
||||
import kala.compress.archivers.zip.ZipArchiveReader;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.game.DefaultGameInstance;
|
||||
import org.jackhuang.hmcl.game.LaunchOptions;
|
||||
import org.jackhuang.hmcl.modpack.MismatchedModpackTypeException;
|
||||
import org.jackhuang.hmcl.modpack.Modpack;
|
||||
import org.jackhuang.hmcl.modpack.ModpackProvider;
|
||||
@@ -29,7 +30,8 @@ public final class ServerModpackProvider implements ModpackProvider {
|
||||
|
||||
@Override
|
||||
public Task<?> createCompletionTask(DefaultDependencyManager dependencyManager, DefaultGameInstance instance) {
|
||||
return new ServerModpackCompletionTask(dependencyManager, instance);
|
||||
return new ServerModpackCompletionTask(dependencyManager, instance)
|
||||
.thenComposeAsync(ignored -> Lwjgl3ifyApplyTask.afterCompletion(dependencyManager, instance));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -51,4 +53,12 @@ public final class ServerModpackProvider implements ModpackProvider {
|
||||
ServerModpackManifest manifest = JsonUtils.fromNonNullJson(json, ServerModpackManifest.class);
|
||||
return manifest.toModpack();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void injectLaunchOptions(String modpackConfigurationJson, LaunchOptions.Builder builder) {
|
||||
Path gameDir = builder.getGameDir();
|
||||
if (gameDir != null) {
|
||||
Lwjgl3ifySupport.injectLaunchOptions(gameDir, builder);
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -126,10 +126,12 @@ public class ServerModpackRemoteInstallTask extends Task<Void> {
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
DefaultGameInstance instance = repository.getInstance(instanceId);
|
||||
dependencies.add(new ServerModpackCompletionTask(
|
||||
dependency,
|
||||
repository.getInstance(instanceId),
|
||||
new ModpackConfiguration<>(manifest, MODPACK_TYPE, manifest.getName(), manifest.getVersion(), Collections.emptyList())));
|
||||
instance,
|
||||
new ModpackConfiguration<>(manifest, MODPACK_TYPE, manifest.getName(), manifest.getVersion(), Collections.emptyList()))
|
||||
.thenComposeAsync(ignored -> Lwjgl3ifyApplyTask.afterCompletion(dependency, instance)));
|
||||
}
|
||||
|
||||
public static final String MODPACK_TYPE = "Server";
|
||||
|
||||
Reference in new issue
Block a user