Feature: MacOS Support.

This commit is contained in:
burningtnt 2025-08-07 22:39:49 +08:00
parent a4d6580793
commit 4618a51934
No known key found for this signature in database
GPG Key ID: 18A43F21F9ACE8C4
6 changed files with 144 additions and 24 deletions

View File

@ -138,16 +138,21 @@ public final class TerracottaManager {
ReadOnlyDoubleWrapper progress = new ReadOnlyDoubleWrapper(0);
TerracottaState.Preparing preparing = new TerracottaState.Preparing(progress.getReadOnlyProperty());
Objects.requireNonNull(TerracottaMetadata.PROVIDER).install(progress).thenRunAsync(() -> {
TerracottaState.Launching launching = new TerracottaState.Launching();
if (compareAndSet(preparing, launching)) {
launch(launching);
}
}).whenComplete(exception -> {
compareAndSet(preparing, TerracottaState.Fatal.INSTANCE);
}).start();
try {
Objects.requireNonNull(TerracottaMetadata.PROVIDER).install(progress).thenRunAsync(() -> {
TerracottaState.Launching launching = new TerracottaState.Launching();
if (compareAndSet(preparing, launching)) {
launch(launching);
}
}).whenComplete(exception -> {
compareAndSet(preparing, TerracottaState.Fatal.INSTANCE);
}).start();
return setState(preparing);
return setState(preparing);
} catch (Exception e) {
setState(TerracottaState.Fatal.INSTANCE);
return null;
}
}
private static void launch(TerracottaState.Launching state) {

View File

@ -1,9 +1,11 @@
package org.jackhuang.hmcl.ui.terracotta.core;
import org.jackhuang.hmcl.ui.terracotta.core.provider.ExecutableProvider;
import org.jackhuang.hmcl.ui.terracotta.core.provider.GeneralProvider;
import org.jackhuang.hmcl.ui.terracotta.core.provider.ITerracottaProvider;
import org.jackhuang.hmcl.ui.terracotta.core.provider.MacOSProvider;
import java.net.URI;
import java.util.List;
public final class TerracottaMetadata {
private TerracottaMetadata() {
@ -11,15 +13,28 @@ public final class TerracottaMetadata {
public static final String VERSION = "0.3.8-rc.1";
public static final URI WINDOWS_X86_64 = URI.create(String.format("https://github.com/burningtnt/Terracotta/releases/download/V%1$s/terracotta-%1$s-windows-x86_64.exe", VERSION));
public static final URI WINDOWS_ARM64 = URI.create(String.format("https://github.com/burningtnt/Terracotta/releases/download/V%1$s/terracotta-%1$s-windows-arm64.exe", VERSION));
public static final URI LINUX_X86_64 = URI.create(String.format("https://github.com/burningtnt/Terracotta/releases/download/V%1$s/terracotta-%1$s-linux-x86_64", VERSION));
public static final URI LINUX_ARM64 = URI.create(String.format("https://github.com/burningtnt/Terracotta/releases/download/V%1$s/terracotta-%1$s-linux-arm64", VERSION));
public static final List<URI> WINDOWS_X86_64 = create(String.format("https://github.com/burningtnt/Terracotta/releases/download/V%1$s/terracotta-%1$s-windows-x86_64.exe", VERSION));
public static final List<URI> WINDOWS_ARM64 = create(String.format("https://github.com/burningtnt/Terracotta/releases/download/V%1$s/terracotta-%1$s-windows-arm64.exe", VERSION));
public static final List<URI> LINUX_X86_64 = create(String.format("https://github.com/burningtnt/Terracotta/releases/download/V%1$s/terracotta-%1$s-linux-x86_64", VERSION));
public static final List<URI> LINUX_ARM64 = create(String.format("https://github.com/burningtnt/Terracotta/releases/download/V%1$s/terracotta-%1$s-linux-arm64", VERSION));
public static final List<URI> MACOS_INSTALLER_X86_64 = create(String.format("https://github.com/burningtnt/Terracotta/releases/download/V%1$s/terracotta-%1$s-macos-x86_64.pkg", VERSION));
public static final List<URI> MACOS_INSTALLER_ARM64 = create(String.format("https://github.com/burningtnt/Terracotta/releases/download/V%1$s/terracotta-%1$s-macos-arm64.pkg", VERSION));
public static final List<URI> MACOS_BIN_X86_64 = create(String.format("https://github.com/burningtnt/Terracotta/releases/download/V%1$s/terracotta-%1$s-macos-x86_64", VERSION));
public static final List<URI> MACOS_BIN_ARM64 = create(String.format("https://github.com/burningtnt/Terracotta/releases/download/V%1$s/terracotta-%1$s-macos-arm64", VERSION));
private static List<URI> create(String s) {
return List.of(URI.create("https://ghfast.top/" + s), URI.create("https://cdn.crashmc.com/" + s), URI.create(s));
}
public static final ITerracottaProvider PROVIDER = locateProvider();
private static ITerracottaProvider locateProvider() {
if (ExecutableProvider.TARGET != null) {
return new ExecutableProvider();
if (GeneralProvider.TARGET != null) {
return new GeneralProvider();
} else if (MacOSProvider.INSTALLER != null) {
return new MacOSProvider();
} else {
return null;
}

View File

@ -11,22 +11,23 @@ import org.jackhuang.hmcl.util.platform.Platform;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.PosixFilePermission;
import java.util.List;
import java.util.Map;
import java.util.Set;
public final class ExecutableProvider implements ITerracottaProvider {
public static final URI TARGET = Map.of(
public final class GeneralProvider implements ITerracottaProvider {
public static final List<URI> TARGET = Map.of(
Platform.WINDOWS_X86_64, TerracottaMetadata.WINDOWS_X86_64,
Platform.WINDOWS_ARM64, TerracottaMetadata.WINDOWS_ARM64,
Platform.LINUX_X86_64, TerracottaMetadata.LINUX_X86_64,
Platform.LINUX_ARM64, TerracottaMetadata.LINUX_ARM64
).get(Platform.SYSTEM_PLATFORM);
private static final Path PATH = Metadata.DEPENDENCIES_DIRECTORY.resolve(String.format(
"terracota/%s/%s", TerracottaMetadata.VERSION, Path.of(TARGET.getPath()).getFileName()
)).toAbsolutePath();
private static final Path PATH = TARGET != null ? Metadata.DEPENDENCIES_DIRECTORY.resolve(String.format(
"terracota/%s/%s", TerracottaMetadata.VERSION, Path.of(TARGET.get(0).getPath()).getFileName()
)).toAbsolutePath() : null;
@Override
public boolean exist() {
@ -35,8 +36,12 @@ public final class ExecutableProvider implements ITerracottaProvider {
@Override
public Task<?> install(DoubleProperty progress) {
Task<?> task = new FileDownloadTask(TARGET, PATH);
Path tmp = PATH.resolveSibling(PATH.getFileName() + ".tmp");
Task<?> task = new FileDownloadTask(TARGET, tmp);
progress.bind(task.progressProperty());
task = task.thenRunAsync(() -> Files.move(tmp, PATH, StandardCopyOption.REPLACE_EXISTING));
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
task = task.thenRunAsync(() -> Files.setPosixFilePermissions(PATH, Set.of(
PosixFilePermission.OWNER_READ,

View File

@ -3,13 +3,14 @@ package org.jackhuang.hmcl.ui.terracotta.core.provider;
import javafx.beans.property.DoubleProperty;
import org.jackhuang.hmcl.task.Task;
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
public interface ITerracottaProvider {
boolean exist();
Task<?> install(DoubleProperty progress);
Task<?> install(DoubleProperty progress) throws IOException;
List<String> launch(Path path);
}

View File

@ -0,0 +1,93 @@
package org.jackhuang.hmcl.ui.terracotta.core.provider;
import javafx.beans.property.DoubleProperty;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.terracotta.core.TerracottaMetadata;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.ManagedProcess;
import org.jackhuang.hmcl.util.platform.SystemUtils;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.PosixFilePermission;
import java.util.List;
import java.util.Set;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class MacOSProvider implements ITerracottaProvider {
public static final List<URI> INSTALLER, BINARY;
static {
if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
INSTALLER = TerracottaMetadata.MACOS_INSTALLER_X86_64;
BINARY = TerracottaMetadata.MACOS_BIN_X86_64;
} else if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
INSTALLER = TerracottaMetadata.MACOS_INSTALLER_ARM64;
BINARY = TerracottaMetadata.MACOS_BIN_ARM64;
} else {
INSTALLER = null;
BINARY = null;
}
}
private static final Path PATH = BINARY != null ? Metadata.DEPENDENCIES_DIRECTORY.resolve(String.format(
"terracota/%s/%s", TerracottaMetadata.VERSION, Path.of(BINARY.get(0).getPath()).getFileName()
)).toAbsolutePath() : null;
@Override
public boolean exist() {
return Files.exists(Path.of("/Applications/terracotta.app")) && Files.exists(PATH);
}
@Override
public Task<?> install(DoubleProperty progress) throws IOException {
Path installer = Files.createTempFile("hmcl-terracotta-installer-", ".pkg").toAbsolutePath();
Task<?> installerTask = new FileDownloadTask(INSTALLER, installer);
Path binary = PATH.resolveSibling(PATH.getFileName() + ".tmp");
Task<?> binaryTask = new FileDownloadTask(BINARY, binary);
progress.bind(installerTask.progressProperty().add(binaryTask.progressProperty()).multiply(0.3));
installerTask = installerTask.thenComposeAsync(() -> {
ManagedProcess process = new ManagedProcess(new ProcessBuilder(
"osascript",
"-e",
String.format(
"do shell script \"%s\" with prompt \"%s\" with administrator privileges",
String.format("installer -pkg %s -target /Applications", installer),
i18n("terracotta.sudo_installing")
)
));
process.pumpInputStream(SystemUtils::onLogLine);
process.pumpErrorStream(SystemUtils::onLogLine);
return Task.fromCompletableFuture(process.getProcess().onExit());
});
binaryTask = binaryTask.thenRunAsync(() -> {
Files.move(binary, PATH, StandardCopyOption.REPLACE_EXISTING);
Files.setPosixFilePermissions(PATH, Set.of(
PosixFilePermission.OWNER_READ,
PosixFilePermission.OWNER_WRITE,
PosixFilePermission.OWNER_EXECUTE,
PosixFilePermission.GROUP_READ,
PosixFilePermission.GROUP_EXECUTE,
PosixFilePermission.OTHERS_READ,
PosixFilePermission.OTHERS_EXECUTE
));
});
return Task.allOf(installerTask, binaryTask);
}
@Override
public List<String> launch(Path path) {
return List.of(PATH.toString(), "--hmcl", path.toString());
}
}

View File

@ -1195,6 +1195,7 @@ terracotta=多人联机
terracotta.status=状态
terracotta.back=退出
terracotta.network_warning=多人联机基于 p2p最终联机体验和您的网络情况有较大关系。
terracotta.sudo_installing=HMCL 需要验证您的密码才能安装多人联机核心。
terracotta.status.uninitialized=未下载多人联机核心
terracotta.status.uninitialized.title=下载多人联机核心(约 8MB
terracotta.status.uninitialized.desc=您承诺,多人联机全过程中,您将严格遵守您所在国家或地区的全部法律法规
@ -1227,7 +1228,7 @@ terracotta.status.exception.desc.ping_host_rst=房间连接断开:房间已关
terracotta.status.exception.desc.guest_et_crash=加入房间失败EasyTier 已崩溃,请向开发者反馈该问题
terracotta.status.exception.desc.host_et_crash=创建房间失败EasyTier 已崩溃,请向开发者反馈该问题
terracotta.status.exception.desc.ping_server_rst=房间已关闭:您已退出游戏存档,房间已自动关闭
terracotta.status.fatal=严重错误,您的设备可能无法正常使用多人联机功能
terracotta.status.fatal=严重错误,您的设备无法正常使用多人联机功能
unofficial.hint=你正在使用非官方构建的 HMCL。我们无法保证其安全性请注意甄别。