Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb7fb914cb | ||
|
|
adbdc85735 | ||
|
|
8dd4ef09aa | ||
|
|
5ca7bcaad0 | ||
|
|
040dceb9ed | ||
|
|
c905b49102 | ||
|
|
23ded32db1 | ||
|
|
9e66c9dbc7 | ||
|
|
07fffc4b01 | ||
|
|
141df8c7b8 | ||
|
|
f5d739711d | ||
|
|
a19679fd00 | ||
|
|
87b5efa64b |
No files matched your search
@@ -138,6 +138,17 @@ jobs:
|
||||
|
||||
def put(name: str, data: bytes):
|
||||
url = upload_root + "/" + urllib.parse.quote(name, safe="")
|
||||
# Gitea refuses to overwrite a generic package file (HTTP 409), so the previous copy of this one
|
||||
# file is deleted right before it is uploaded again (keeps the gap without a file as short as possible).
|
||||
delete = urllib.request.Request(url, method="DELETE")
|
||||
delete.add_header("Authorization", "token " + token)
|
||||
delete.add_header("User-Agent", "tdlm-hmcl-publish")
|
||||
try:
|
||||
with urllib.request.urlopen(delete, timeout=300) as response:
|
||||
response.read()
|
||||
except urllib.error.HTTPError as exc:
|
||||
if exc.code != 404:
|
||||
raise SystemExit("HTTP %s deleting the old %s: %s" % (exc.code, name, exc.read()[:300]))
|
||||
request = urllib.request.Request(url, data=data, method="PUT")
|
||||
request.add_header("Authorization", "token " + token)
|
||||
request.add_header("Content-Type", "application/octet-stream")
|
||||
@@ -204,7 +215,7 @@ jobs:
|
||||
"- %s/HMCL.jar\n\n"
|
||||
"TDLMPack does not Maven-resolve this launcher; point the pack README / install docs at these URLs "
|
||||
"instead of hand-copying exe/jar.\n"
|
||||
"Client pack fileApi default: https://packages.dandanton.com/tdlm-client/latest\n"
|
||||
"Client pack fileApi default: http://159.75.5.225:19527/tdlm-client\n"
|
||||
"(Pack must publish/mirror the same layout; see docs/TDLM_CONSUME.md).\n"
|
||||
) % (package_root, package_root)
|
||||
|
||||
|
||||
@@ -141,9 +141,9 @@ public final class Metadata {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Default client Server Modpack fileApi (CDN mirror of Pack's `tdlm-client/latest` layout).
|
||||
/// Default client Server Modpack fileApi: self-hosted static mirror on the TDLM VPS (nginx, port 19527).
|
||||
public static final String DEFAULT_CLIENT_FILE_API =
|
||||
"https://packages.dandanton.com/tdlm-client/latest";
|
||||
"http://159.75.5.225:19527/tdlm-client";
|
||||
|
||||
/// Stable client update root (`server-manifest.json` and `overrides/` live under this URL).
|
||||
///
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
package org.jackhuang.hmcl.download;
|
||||
|
||||
import org.glavo.url.WebURL;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.download.cleanroom.CleanroomRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.fabric.FabricRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.forge.ForgeRemoteVersion;
|
||||
@@ -132,6 +133,13 @@ public final class HMCLDownloadProvider extends DownloadProvider {
|
||||
public HMCLDownloadProvider(Supplier<@Unmodifiable List<String>> mirrorRoots) {
|
||||
this.mirrorRoots = Objects.requireNonNull(mirrorRoots);
|
||||
this.rules = List.of(
|
||||
// The self-hosted client mirror carries the pack's full dependency set. BMCLAPI's
|
||||
// redirect CDNs (sakura.sld.tw, cernet) and the Mojang/Forge origins are TLS-blocked
|
||||
// on many mainland networks, so the VPS must be tried before any of them.
|
||||
new SimpleRule("https://piston-meta.mojang.com", Metadata.getClientFileApi() + "/piston-meta", "https://bmclapi2.bangbang93.com"),
|
||||
new SimpleRule("https://libraries.minecraft.net", Metadata.getClientFileApi() + "/libraries", "https://bmclapi2.bangbang93.com/libraries"),
|
||||
new SimpleRule("https://maven.minecraftforge.net", Metadata.getClientFileApi() + "/forge-maven", "https://bmclapi2.bangbang93.com/maven"),
|
||||
new SimpleRule("https://launcher.mojang.com", Metadata.getClientFileApi() + "/launcher-mojang", "https://bmclapi2.bangbang93.com"),
|
||||
new MultiRootRule("https://bmclapi2.bangbang93.com", Function.identity()),
|
||||
new MultiRootRule("https://bmclapi.bangbang93.com", Function.identity()),
|
||||
new MultiRootRule("https://launchermeta.mojang.com", Function.identity()),
|
||||
@@ -183,6 +191,11 @@ public final class HMCLDownloadProvider extends DownloadProvider {
|
||||
new SimpleRule("https://hmcl.glavo.site/metadata/fmllibs", "https://alist.8mi.tech/d/mirror/HMCL-Metadata/Auto/fmllibs"),
|
||||
new SimpleRule("https://zkitefly.github.io/unlisted-versions-of-minecraft", "https://alist.8mi.tech/d/mirror/unlisted-versions-of-minecraft/Auto"),
|
||||
|
||||
// files.prismlauncher.org is unreachable from mainland China (TLS handshake reset) and BMCLAPI
|
||||
// does not mirror its repackaged log4j-fixed artifacts. Serve them from the self-hosted client
|
||||
// mirror (alongside the fileApi root); Prism stays as the fallback for reachable networks.
|
||||
new SimpleRule("https://files.prismlauncher.org/maven", Metadata.getClientFileApi() + "/maven"),
|
||||
|
||||
// https://github.com/mcmod-info-mirror/mcim-rust-api
|
||||
new SimpleRule("https://api.modrinth.com", null, "https://mod.mcimirror.top/modrinth"),
|
||||
new SimpleRule("https://cdn.modrinth.com", null, "https://mod.mcimirror.top"),
|
||||
@@ -295,11 +308,18 @@ public final class HMCLDownloadProvider extends DownloadProvider {
|
||||
@Override
|
||||
public DownloadCandidates getAssetObjectCandidates(AssetObject assetObject) {
|
||||
String location = assetObject.getLocation();
|
||||
return getCandidates(
|
||||
fileSource,
|
||||
WebURL.parse("https://resources.download.minecraft.net/" + location),
|
||||
mirrorUrls("/assets/" + location)
|
||||
);
|
||||
WebURL officialUrl = WebURL.parse("https://resources.download.minecraft.net/" + location);
|
||||
List<WebURL> mirrors = mirrorUrls("/assets/" + location);
|
||||
if (preferMirror(fileSource)) {
|
||||
// Keep the VPS strictly first: spreadPrimary would otherwise rotate BMCLAPI ahead of
|
||||
// it, and BMCLAPI's asset CDN is unreachable on blocked networks (per-file timeouts).
|
||||
var candidates = new ArrayList<DownloadCandidate>(mirrors.size() + 2);
|
||||
candidates.add(DownloadCandidate.of(WebURL.parse(Metadata.getClientFileApi() + "/assets/" + location)));
|
||||
candidates.addAll(spreadPrimary(mirrors.stream().map(DownloadCandidate::of).toList(), officialUrl.href()));
|
||||
candidates.add(DownloadCandidate.of(officialUrl));
|
||||
return DownloadCandidates.of(List.copyOf(candidates));
|
||||
}
|
||||
return getCandidates(fileSource, officialUrl, mirrors);
|
||||
}
|
||||
|
||||
@NotNullByDefault
|
||||
|
||||
@@ -27,16 +27,11 @@ 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.*;
|
||||
@@ -359,8 +354,8 @@ public final class Controllers {
|
||||
);
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// Installs a launcher-bundled modpack when present. First-run remote install is deferred to the
|
||||
/// home Start button ([Instances#ensureClientAndJoin]) so players never enter the install wizard.
|
||||
private static void tryInstallBundledModpack(HMCLGameRepository repository) {
|
||||
@Nullable Path modpackFile = Metadata.findBundledModpackFile();
|
||||
if (modpackFile != null) {
|
||||
@@ -384,35 +379,7 @@ public final class Controllers {
|
||||
}
|
||||
}), i18n("modpack.installing"), TaskCancellationAction.NO_CANCEL
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (repository.getDisplayInstances().findAny().isPresent()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String manifestUrl = Metadata.getClientManifestUrl();
|
||||
LOG.info("No instances yet; fetching client Server Modpack from " + manifestUrl);
|
||||
Controllers.taskDialog(
|
||||
new GetTask(WebURL.parse(manifestUrl))
|
||||
.whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception != null) {
|
||||
LOG.warning("Failed to fetch client Server Modpack manifest", exception);
|
||||
return;
|
||||
}
|
||||
ServerModpackManifest manifest =
|
||||
JsonUtils.fromMaybeMalformedJson(result, ServerModpackManifest.class);
|
||||
if (manifest == null) {
|
||||
LOG.warning("Malformed client Server Modpack manifest from " + manifestUrl);
|
||||
return;
|
||||
}
|
||||
Controllers.getDecorator().startWizard(
|
||||
new ModpackInstallWizardProvider(repository, manifest),
|
||||
i18n("install.modpack"));
|
||||
}),
|
||||
i18n("message.downloading"),
|
||||
TaskCancellationAction.NO_CANCEL
|
||||
);
|
||||
}
|
||||
|
||||
/// Shows the content in the main-window dialog stack without waiting for it to close.
|
||||
|
||||
@@ -14,6 +14,7 @@ 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.ModpackConfiguration;
|
||||
import org.jackhuang.hmcl.modpack.server.ServerModpackManifest;
|
||||
import org.jackhuang.hmcl.task.GetTask;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
@@ -25,21 +26,26 @@ import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.PromptDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.Validator;
|
||||
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
|
||||
import org.jackhuang.hmcl.ui.download.LocalModpackPage;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.modpack.Modpack;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
@@ -48,63 +54,198 @@ public final class Instances {
|
||||
private Instances() {
|
||||
}
|
||||
|
||||
/// Installs or updates the TDLM client.
|
||||
/// Returns whether the installed server modpack is behind the remote `server-manifest.json`.
|
||||
///
|
||||
/// Prefers a launcher-bundled zip, then the org [Metadata#getClientManifestUrl] Server Modpack,
|
||||
/// then the generic local/remote import wizard.
|
||||
public static void installServerModpack() {
|
||||
/// Compares the local embedded manifest version (and file path/hash set) with
|
||||
/// `fileApi/server-manifest.json`. Network or parse failures return `false` so the home button
|
||||
/// stays on "join".
|
||||
public static boolean isClientUpdateAvailable(@Nullable HMCLGameInstance instance) {
|
||||
if (instance == null) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
Path configurationFile = instance.getModpackConfigurationFile();
|
||||
if (Files.notExists(configurationFile)) {
|
||||
return false;
|
||||
}
|
||||
ModpackConfiguration<ServerModpackManifest> configuration = JsonUtils.fromJsonFile(
|
||||
configurationFile, ModpackConfiguration.typeOf(ServerModpackManifest.class));
|
||||
ServerModpackManifest local = configuration.getManifest();
|
||||
if (local == null || StringUtils.isBlank(local.getFileApi())) {
|
||||
return false;
|
||||
}
|
||||
String manifestUrl = StringUtils.removeSuffix(local.getFileApi().trim(), "/") + "/server-manifest.json";
|
||||
ServerModpackManifest remote = JsonUtils.fromNonNullJson(
|
||||
NetworkUtils.doGet(manifestUrl), ServerModpackManifest.class);
|
||||
if (!Objects.equals(local.getVersion(), remote.getVersion())) {
|
||||
return true;
|
||||
}
|
||||
return !fileHashes(local).equals(fileHashes(remote));
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to check TDLM client update", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, String> fileHashes(ServerModpackManifest manifest) {
|
||||
return manifest.getFiles().stream().collect(Collectors.toMap(
|
||||
ModpackConfiguration.FileInformation::getPath,
|
||||
ModpackConfiguration.FileInformation::getHash,
|
||||
(left, right) -> right));
|
||||
}
|
||||
|
||||
/// Installs or updates the TDLM client from fileApi / bundled zip without opening the install UI,
|
||||
/// then joins the configured server when ready.
|
||||
///
|
||||
/// Prefer this for the home Start / Join button. Existing server-modpack instances get differential
|
||||
/// updates during launch via [org.jackhuang.hmcl.modpack.server.ServerModpackCompletionTask].
|
||||
public static void ensureClientAndJoin() {
|
||||
HMCLGameRepository repository = GameDirectoryManager.getSelectedRepository();
|
||||
if (!repository.isLoaded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Already installed: join immediately. Launch runs ServerModpackCompletionTask for updates.
|
||||
if (repository.getSelectedInstance() != null && Metadata.findBundledModpackFile() == null) {
|
||||
joinServer(repository.getSelectedInstance());
|
||||
return;
|
||||
}
|
||||
|
||||
Path bundled = Metadata.findBundledModpackFile();
|
||||
if (bundled != null) {
|
||||
Controllers.getDecorator().startWizard(
|
||||
new ModpackInstallWizardProvider(repository, bundled),
|
||||
i18n("install.modpack"));
|
||||
installBundledThenJoin(repository, bundled);
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
.thenComposeAsync(Schedulers.javafx(), result -> {
|
||||
ServerModpackManifest manifest = JsonUtils.fromMaybeMalformedJson(
|
||||
result, ServerModpackManifest.class);
|
||||
if (manifest == null) {
|
||||
throw new IOException(i18n("modpack.type.server.malformed"));
|
||||
}
|
||||
return ensureRemoteClientInstalled(repository, manifest);
|
||||
})
|
||||
.whenComplete(Schedulers.javafx(), (ignored, exception) -> {
|
||||
if (exception != null) {
|
||||
LOG.warning("Failed to install or update TDLM client", exception);
|
||||
Controllers.dialog(
|
||||
i18n("modpack.type.server.malformed"),
|
||||
i18n("message.error"),
|
||||
StringUtils.getStackTrace(exception),
|
||||
i18n("install.failed"),
|
||||
MessageDialogPane.MessageType.ERROR);
|
||||
importModpack();
|
||||
return;
|
||||
}
|
||||
Controllers.getDecorator().startWizard(
|
||||
new ModpackInstallWizardProvider(repository, manifest),
|
||||
i18n("install.modpack"));
|
||||
joinServer(GameDirectoryManager.getSelectedRepository().getSelectedInstance());
|
||||
}),
|
||||
i18n("message.downloading"),
|
||||
i18n("modpack.installing"),
|
||||
TaskCancellationAction.NORMAL);
|
||||
}
|
||||
|
||||
public static void importModpack() {
|
||||
HMCLGameRepository repository = GameDirectoryManager.getSelectedRepository();
|
||||
if (repository.isLoaded()) {
|
||||
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(repository), i18n("install.modpack"));
|
||||
}
|
||||
/// Silent first-install / update from the org client manifest — no wizard UI.
|
||||
public static void installServerModpack() {
|
||||
ensureClientAndJoin();
|
||||
}
|
||||
|
||||
/// @deprecated use [#installServerModpack()]
|
||||
/// Install wizard is disabled for the dedicated server-client launcher.
|
||||
public static void importModpack() {
|
||||
LOG.info("Ignoring importModpack: install UI is disabled; use Start to install/update");
|
||||
}
|
||||
|
||||
/// @deprecated use [#ensureClientAndJoin()]
|
||||
public static void addNewGame() {
|
||||
installServerModpack();
|
||||
ensureClientAndJoin();
|
||||
}
|
||||
|
||||
/// Installs a local modpack zip without the wizard, then selects the new instance.
|
||||
public static void installLocalModpackSilently(HMCLGameRepository repository, Path file) {
|
||||
if (!repository.isLoaded()) {
|
||||
return;
|
||||
}
|
||||
Controllers.taskDialog(
|
||||
Task.composeAsync(Schedulers.io(), () -> {
|
||||
Modpack modpack = ModpackHelper.readModpackManifest(file);
|
||||
String name = LocalModpackPage.processModpackName(modpack.getName());
|
||||
if (StringUtils.isBlank(name)) {
|
||||
name = "TDLM";
|
||||
}
|
||||
GameInstanceID instanceId = new GameInstanceID(name);
|
||||
if (repository.hasInstance(instanceId)) {
|
||||
instanceId = new GameInstanceID(name + "-" + System.currentTimeMillis());
|
||||
}
|
||||
GameInstanceID id = instanceId;
|
||||
return ModpackHelper.getInstallTask(repository, file, id, modpack, null)
|
||||
.thenRunAsync(Schedulers.javafx(), () ->
|
||||
repository.setSelectedInstance(repository.getInstance(id)));
|
||||
})
|
||||
.whenComplete(Schedulers.javafx(), (ignored, exception) -> {
|
||||
if (exception != null) {
|
||||
LOG.warning("Failed to install local modpack", exception);
|
||||
Controllers.dialog(
|
||||
StringUtils.getStackTrace(exception),
|
||||
i18n("install.failed"),
|
||||
MessageDialogPane.MessageType.ERROR);
|
||||
}
|
||||
}),
|
||||
i18n("modpack.installing"),
|
||||
TaskCancellationAction.NORMAL);
|
||||
}
|
||||
|
||||
private static void installBundledThenJoin(HMCLGameRepository repository, Path bundled) {
|
||||
Controllers.taskDialog(
|
||||
Task.composeAsync(Schedulers.io(), () -> {
|
||||
Modpack modpack = ModpackHelper.readModpackManifest(bundled);
|
||||
String name = LocalModpackPage.processModpackName(modpack.getName());
|
||||
if (StringUtils.isBlank(name)) {
|
||||
name = "TDLM";
|
||||
}
|
||||
GameInstanceID instanceId = new GameInstanceID(name);
|
||||
return ModpackHelper.getInstallTask(repository, bundled, instanceId, modpack, null)
|
||||
.thenRunAsync(Schedulers.javafx(), () -> {
|
||||
repository.setSelectedInstance(repository.getInstance(instanceId));
|
||||
try {
|
||||
Files.deleteIfExists(bundled);
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to delete bundled modpack: " + bundled, e);
|
||||
}
|
||||
});
|
||||
})
|
||||
.whenComplete(Schedulers.javafx(), (ignored, exception) -> {
|
||||
if (exception != null) {
|
||||
LOG.warning("Failed to install bundled modpack", exception);
|
||||
Controllers.dialog(
|
||||
StringUtils.getStackTrace(exception),
|
||||
i18n("install.failed"),
|
||||
MessageDialogPane.MessageType.ERROR);
|
||||
return;
|
||||
}
|
||||
joinServer(GameDirectoryManager.getSelectedRepository().getSelectedInstance());
|
||||
}),
|
||||
i18n("modpack.installing"),
|
||||
TaskCancellationAction.NO_CANCEL);
|
||||
}
|
||||
|
||||
/// Installs when missing; selects an existing instance otherwise.
|
||||
///
|
||||
/// Differential updates for an installed server modpack run during launch via
|
||||
/// [org.jackhuang.hmcl.modpack.server.ServerModpackCompletionTask].
|
||||
private static Task<?> ensureRemoteClientInstalled(
|
||||
HMCLGameRepository repository, ServerModpackManifest manifest) throws IOException {
|
||||
Modpack modpack = manifest.toModpack();
|
||||
String name = LocalModpackPage.processModpackName(manifest.getName());
|
||||
if (StringUtils.isBlank(name)) {
|
||||
name = "TDLM";
|
||||
}
|
||||
GameInstanceID instanceId = new GameInstanceID(name);
|
||||
|
||||
if (repository.hasInstance(instanceId)) {
|
||||
repository.setSelectedInstance(repository.getInstance(instanceId));
|
||||
return Task.completed(null);
|
||||
}
|
||||
|
||||
return ModpackHelper.getInstallTask(repository, manifest, instanceId, modpack)
|
||||
.thenRunAsync(Schedulers.javafx(), () ->
|
||||
repository.setSelectedInstance(repository.getInstance(instanceId)));
|
||||
}
|
||||
|
||||
public static void deleteInstance(HMCLGameInstance gameInstance) {
|
||||
@@ -257,8 +398,10 @@ public final class Instances {
|
||||
.addQuestion(new PromptDialogPane.Builder.BooleanQuestion(i18n("instance.manage.duplicate.duplicate_save"), false)));
|
||||
}
|
||||
|
||||
/// Updates via fileApi through the Start flow — no install wizard.
|
||||
public static void updateInstance(HMCLGameInstance gameInstance) {
|
||||
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(gameInstance.getRepository(), gameInstance.getId()));
|
||||
gameInstance.getRepository().setSelectedInstance(gameInstance);
|
||||
ensureClientAndJoin();
|
||||
}
|
||||
|
||||
/// Queues an asset check with a fresh asset index for the supplied instance.
|
||||
@@ -338,14 +481,7 @@ public final class Instances {
|
||||
@SafeVarargs
|
||||
public static void launch(@Nullable HMCLGameInstance gameInstance, Consumer<LauncherHelper>... injecters) {
|
||||
if (gameInstance == null) {
|
||||
JFXButton installModpack = new JFXButton(i18n("install.modpack"));
|
||||
installModpack.getStyleClass().add("dialog-accept");
|
||||
installModpack.setOnAction(e -> installServerModpack());
|
||||
|
||||
Controllers.confirmAction(i18n("instance.empty.launch"), i18n("launch.failed"),
|
||||
MessageDialogPane.MessageType.ERROR,
|
||||
installModpack,
|
||||
null);
|
||||
ensureClientAndJoin();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -412,11 +548,10 @@ public final class Instances {
|
||||
Controllers.navigate(Controllers.getSettingsPage());
|
||||
}
|
||||
|
||||
/// Instance management UI is disabled for the dedicated server-client launcher.
|
||||
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());
|
||||
LOG.info("Ignoring modifyGameSettings for " + gameInstance.getId()
|
||||
+ ": instance management UI is disabled");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -58,6 +58,9 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
BindingMapping.of(GameDirectoryManager.selectedRepositoryProperty())
|
||||
.flatMap(HMCLGameRepository::snapshotProperty);
|
||||
|
||||
/// Bumped when the home join button starts a new remote-update check, so stale results are ignored.
|
||||
private int updateCheckGeneration;
|
||||
|
||||
{
|
||||
HBox titleNode = new HBox(8);
|
||||
titleNode.setPadding(new Insets(0, 0, 0, 2));
|
||||
@@ -135,18 +138,20 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
card.setMaxHeight(Region.USE_PREF_SIZE);
|
||||
StackPane.setAlignment(card, Pos.BOTTOM_CENTER);
|
||||
|
||||
// Start always installs/updates from fileApi then joins — never opens install or instance UI.
|
||||
FXUtils.setOnActionWithCooldown(joinButton, MainPage.this::launch);
|
||||
|
||||
FXUtils.onChangeAndOperate(currentGameProperty(), currentGame -> {
|
||||
refreshJoinButton(joinButton, currentGame);
|
||||
if (currentGame == null) {
|
||||
joinButton.setText(i18n("instance.launch.empty"));
|
||||
FXUtils.setOnActionWithCooldown(joinButton, MainPage.this::launchNoGame);
|
||||
packValue.setText(i18n("home.not_installed"));
|
||||
routeFact.setVisible(false);
|
||||
routeFact.setManaged(false);
|
||||
serverFact.setVisible(false);
|
||||
serverFact.setManaged(false);
|
||||
routeFact.setOnMouseClicked(null);
|
||||
routeFact.setCursor(Cursor.DEFAULT);
|
||||
} else {
|
||||
joinButton.setText(i18n("server.join"));
|
||||
FXUtils.setOnActionWithCooldown(joinButton, MainPage.this::launch);
|
||||
packValue.setText(currentGame.getId().toString());
|
||||
ServerEndpoint.@Nullable Route route = ServerEndpoint.selected(currentGame.getModpackConfigurationFile());
|
||||
routeFact.setVisible(route != null);
|
||||
@@ -181,6 +186,25 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
getChildren().addAll(art, hero, card);
|
||||
}
|
||||
|
||||
/// Sets the home button to install / update / join based on the selected instance and remote manifest.
|
||||
private void refreshJoinButton(JFXButton joinButton, @Nullable HMCLGameInstance currentGame) {
|
||||
int generation = ++updateCheckGeneration;
|
||||
if (currentGame == null) {
|
||||
joinButton.setText(i18n("instance.launch.empty"));
|
||||
return;
|
||||
}
|
||||
joinButton.setText(i18n("server.join"));
|
||||
Schedulers.io().execute(() -> {
|
||||
boolean updateAvailable = Instances.isClientUpdateAvailable(currentGame);
|
||||
FXUtils.runInFX(() -> {
|
||||
if (generation != updateCheckGeneration || getCurrentGame() != currentGame) {
|
||||
return;
|
||||
}
|
||||
joinButton.setText(i18n(updateAvailable ? "server.update" : "server.join"));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Pings the server behind `route` in the background and shows the result in `serverValue`.
|
||||
///
|
||||
/// WebSocket routes cannot be pinged with plain TCP, so the status is hidden for them.
|
||||
@@ -214,14 +238,9 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
return label;
|
||||
}
|
||||
|
||||
/// Installs or updates the client from fileApi when needed, then launches into the server.
|
||||
private void launch() {
|
||||
HMCLGameRepository repository = GameDirectoryManager.getSelectedRepository();
|
||||
Instances.joinServer(repository.getSelectedInstance());
|
||||
}
|
||||
|
||||
/// Opens the server modpack install/update flow when no instance is selected.
|
||||
private void launchNoGame() {
|
||||
Instances.installServerModpack();
|
||||
Instances.ensureClientAndJoin();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -4,7 +4,6 @@ package org.jackhuang.hmcl.ui.main;
|
||||
|
||||
import com.jfoenix.controls.JFXPopup;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import org.jackhuang.hmcl.game.HMCLGameInstance;
|
||||
import org.jackhuang.hmcl.game.ModpackHelper;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.setting.GameDirectoryManager;
|
||||
@@ -17,8 +16,6 @@ import org.jackhuang.hmcl.ui.account.AccountListPopupMenu;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
|
||||
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.instances.GameAdvancedListItem;
|
||||
import org.jackhuang.hmcl.ui.instances.Instances;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@@ -43,20 +40,7 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
accountListItem.setOnAction(e -> AccountListPopupMenu.show(accountListItem, JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT, 0, -accountListItem.getHeight()));
|
||||
accountListItem.accountProperty().bind(Accounts.selectedAccountProperty());
|
||||
|
||||
// second item in left sidebar
|
||||
GameAdvancedListItem gameInstanceItem = new GameAdvancedListItem();
|
||||
gameInstanceItem.setOnAction(e -> {
|
||||
@Nullable HMCLGameInstance instance = GameDirectoryManager.getSelectedRepository().getSelectedInstance();
|
||||
if (instance == null) {
|
||||
Instances.installServerModpack();
|
||||
} else {
|
||||
Instances.modifyGameSettings(instance);
|
||||
}
|
||||
});
|
||||
if (AnimationUtils.isAnimationEnabled()) {
|
||||
gameInstanceItem.setPrepareAction(Controllers::prepareGameInstancePage);
|
||||
}
|
||||
|
||||
// Instance management entry removed: Start on the home page installs/updates and joins.
|
||||
AdvancedListItem launcherSettingsItem = new AdvancedListItem();
|
||||
launcherSettingsItem.setLeftIcon(SVG.SETTINGS);
|
||||
launcherSettingsItem.setTitle(i18n("settings"));
|
||||
@@ -68,9 +52,8 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
launcherSettingsItem.setPrepareAction(Controllers::prepareSettingsPage);
|
||||
}
|
||||
|
||||
// The sidebar holds the modpack and settings entries; the account sits at the bottom-left.
|
||||
// Sidebar: settings only; account sits at the bottom-left.
|
||||
getSidebar()
|
||||
.add(gameInstanceItem)
|
||||
.add(launcherSettingsItem);
|
||||
setSidebarBottom(accountListItem);
|
||||
|
||||
@@ -92,9 +75,9 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
modpacks -> {
|
||||
Path file = modpacks.get(0);
|
||||
if (ModpackHelper.isFileModpackByExtension(file)) {
|
||||
Controllers.getDecorator().startWizard(
|
||||
new ModpackInstallWizardProvider(GameDirectoryManager.getSelectedRepository(), file),
|
||||
i18n("install.modpack"));
|
||||
// Silent install — no modpack wizard UI.
|
||||
Instances.installLocalModpackSilently(
|
||||
GameDirectoryManager.getSelectedRepository(), file);
|
||||
} else if ("json".equalsIgnoreCase(FileUtils.getExtension(file))) {
|
||||
Instances.installFromJson(GameDirectoryManager.getSelectedRepository(), file);
|
||||
}
|
||||
|
||||
@@ -809,6 +809,7 @@ first_run.invalid=Use 1 to 16 letters, digits or underscores.
|
||||
home.server=Server
|
||||
home.server.unreachable=Unreachable
|
||||
server.join=Join Server
|
||||
server.update=Update
|
||||
instance.launch_and_enter_world=Play World
|
||||
instance.launch.empty=Start Game
|
||||
instance.launch.empty.installing=Installing Game
|
||||
|
||||
@@ -629,6 +629,7 @@ first_run.invalid=一至十六字,限英文數字底綫。
|
||||
home.server=服
|
||||
home.server.unreachable=不通
|
||||
server.join=入服
|
||||
server.update=更新
|
||||
# instance.launch_and_enter_world=
|
||||
instance.launch.empty=啟戲
|
||||
instance.launch.empty.installing=裝戲
|
||||
|
||||
@@ -632,6 +632,7 @@ first_run.invalid=只能用 1 到 16 个英文字母、数字或下划线。
|
||||
home.server=服务器
|
||||
home.server.unreachable=连接不上
|
||||
server.join=进入服务器
|
||||
server.update=更新
|
||||
instance.launch_and_enter_world=进入世界
|
||||
instance.launch.empty=开始游戏
|
||||
instance.launch.empty.installing=安装游戏
|
||||
|
||||
@@ -626,6 +626,7 @@ first_run.invalid=只能使用 1 到 16 個英文字母、數字或底線。
|
||||
home.server=伺服器
|
||||
home.server.unreachable=無法連線
|
||||
server.join=進入伺服器
|
||||
server.update=更新
|
||||
instance.launch_and_enter_world=進入世界
|
||||
instance.launch.empty=開始遊戲
|
||||
instance.launch.empty.installing=安裝遊戲
|
||||
|
||||
+18
@@ -25,6 +25,7 @@ import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.security.DigestInputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
@@ -37,6 +38,8 @@ public final class MojangJavaDownloadTask extends Task<MojangJavaDownloadTask.Re
|
||||
private final Path tempDir;
|
||||
private final Task<MojangJavaRemoteFiles> javaDownloadsTask;
|
||||
private final List<Task<?>> dependencies = new ArrayList<>();
|
||||
private final AtomicInteger completedFiles = new AtomicInteger();
|
||||
private volatile int totalFiles;
|
||||
|
||||
private volatile MojangJavaDownloads.JavaDownload download;
|
||||
|
||||
@@ -147,6 +150,21 @@ public final class MojangJavaDownloadTask extends Task<MojangJavaDownloadTask.Re
|
||||
Files.createSymbolicLink(dest, Paths.get(link.getTarget()));
|
||||
}
|
||||
}
|
||||
|
||||
// Report per-file progress so the bar advances through the download, LZMA decompress and
|
||||
// cache-copy phases; otherwise it stays indeterminate while files appear on disk and players
|
||||
// assume the install is frozen. The callbacks run after each file completes and never touch
|
||||
// the download/decompress logic above.
|
||||
totalFiles = dependencies.size();
|
||||
if (totalFiles > 0) {
|
||||
List<Task<?>> tracked = new ArrayList<>(totalFiles);
|
||||
for (Task<?> dependency : dependencies) {
|
||||
tracked.add(dependency.thenRunAsync(() -> updateProgress(completedFiles.incrementAndGet(), totalFiles)));
|
||||
}
|
||||
dependencies.clear();
|
||||
dependencies.addAll(tracked);
|
||||
updateProgress(0, totalFiles);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+1
-1
@@ -213,6 +213,6 @@ public class ServerModpackCompletionTask extends Task<Void> {
|
||||
public void postExecute() throws Exception {
|
||||
if (manifest == null || StringUtils.isBlank(manifest.getManifest().getFileApi())) return;
|
||||
Files.createDirectories(configurationFile.getParent());
|
||||
JsonUtils.writeToJsonFile(configurationFile, new ModpackConfiguration<>(remoteManifest, this.manifest.getType(), this.manifest.getName(), this.manifest.getVersion(), remoteManifest.getFiles()));
|
||||
JsonUtils.writeToJsonFile(configurationFile, new ModpackConfiguration<>(remoteManifest, this.manifest.getType(), this.manifest.getName(), remoteManifest.getVersion(), remoteManifest.getFiles()));
|
||||
}
|
||||
}
|
||||
+15
-8
@@ -46,26 +46,33 @@ Assets on each tag: `HMCL.exe`, `HMCL.jar`, plus `.sha256` files.
|
||||
In `TDLM/TDLMPack` README §启动, replace the repo-only “安装 TDLM/HMCL” step with one of the URLs above.
|
||||
**Do not** add HMCL as a Gradle/Maven dependency of the pack — pack CI stays pack-only.
|
||||
|
||||
### 4. Client pack fileApi (CDN)
|
||||
### 4. Client pack fileApi (self-hosted VPS)
|
||||
|
||||
HMCL default (`Metadata.DEFAULT_CLIENT_FILE_API`):
|
||||
|
||||
```text
|
||||
https://packages.dandanton.com/tdlm-client/latest
|
||||
http://159.75.5.225:19527/tdlm-client
|
||||
```
|
||||
|
||||
First install / updates fetch `{fileApi}/server-manifest.json` (and `overrides/**` under that root).
|
||||
Override at runtime: `-Dhmcl.client.fileApi=…` or jar attribute `hmcl.client.fileApi`.
|
||||
|
||||
The fileApi is a read-only nginx static mirror on the TDLM VPS (`159.75.5.225`, port `19527`,
|
||||
root `/var/www/tdlm-dl`). Players read it anonymously; integrity is verified by the SHA-1 hashes
|
||||
in `server-manifest.json`.
|
||||
|
||||
**TDLMPack must use the same URL** so `server-manifest.json` embeds it:
|
||||
|
||||
```text
|
||||
TDLM_CLIENT_FILE_API=https://packages.dandanton.com/tdlm-client/latest
|
||||
TDLM_CLIENT_FILE_API=http://159.75.5.225:19527/tdlm-client
|
||||
```
|
||||
|
||||
Set that as the Gitea Actions **variable** on `TDLM/TDLMPack` (pack.yml already passes `vars.TDLM_CLIENT_FILE_API`).
|
||||
Set that as the Gitea Actions **variable** on `TDLM/TDLMPack` (pack.yml passes `vars.TDLM_CLIENT_FILE_API`;
|
||||
`PackBuild.DEFAULT_FILE_API` is the build-time fallback).
|
||||
|
||||
**Publish caveat:** Pack’s current workflow PUTs the client tree to whatever `TDLM_CLIENT_FILE_API` is (upload root = fileApi). Today that defaults to Gitea generic
|
||||
`https://git.nest.potatolab.uk:8443/api/packages/TDLM/generic/tdlm-client/latest`.
|
||||
If you only change the HMCL default to the CDN and leave Pack publishing to Gitea, players will 404 unless you **mirror/sync** Gitea → `packages.dandanton.com/tdlm-client/latest`.
|
||||
If you set `TDLM_CLIENT_FILE_API` to the CDN URL, Pack CI must be able to PUT to that CDN (or you must split “upload origin” from “public fileApi” in Pack — not implemented today). Do not point HMCL at the CDN without one of those two ops paths.
|
||||
**Publish path (upload origin split from public fileApi):** Pack CI no longer PUTs to the read URL.
|
||||
The `pack.yml` "Publish the client Server Modpack to fileApi" step rsyncs `build/pack/client/overrides/`
|
||||
then `server-manifest.json` (manifest last) over SSH to `ubuntu@159.75.5.225:/var/www/tdlm-dl/tdlm-client`.
|
||||
Auth is the repo secret `TDLM_VPS_SSH_KEY`, an ed25519 deploy key confined on the VPS by
|
||||
`command="/usr/bin/rrsync /var/www/tdlm-dl/tdlm-client"` (rsync-only, no shell, no forwarding).
|
||||
The public port 19527 serves GET/HEAD only — there is no public write surface.
|
||||
Reference in new issue
Block a user