Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
c9c56a6
Feature (Draft): Terracotta
burningtnt Aug 7, 2025
a4d6580
Use cursor hand for codes.
burningtnt Aug 7, 2025
4618a51
Feature: MacOS Support.
burningtnt Aug 7, 2025
8f664ed
Fix.
burningtnt Aug 7, 2025
506620b
Fix
burningtnt Aug 7, 2025
e655de7
Enhance UI.
burningtnt Aug 8, 2025
d48cfc3
Enhance UI.
burningtnt Aug 8, 2025
aead89d
Feature: Verify intergrity before lauching terracotta.
burningtnt Aug 10, 2025
1529d3e
Feature: Initialize Translations.
burningtnt Aug 10, 2025
f0fc936
Fix: Make terracotta daemon a detached process on Windows.
burningtnt Aug 10, 2025
3d3ee20
Move configs into dedicated json file. Feature: Display update messag…
burningtnt Aug 11, 2025
e7021e7
Feature: Delete out of date terracotta cores.
burningtnt Aug 11, 2025
a97eb7e
Revise Translation.
burningtnt Aug 11, 2025
851a312
Fix: Encode URL before concating query arguments. Code cleanup.
burningtnt Aug 12, 2025
fa7ca1c
Bump to Terracotta 0.3.8-rc.3
burningtnt Aug 12, 2025
6229aae
Code cleanup
burningtnt Aug 12, 2025
3d84ba6
Bump to Terracotta 0.3.8-rc.4
burningtnt Aug 12, 2025
58c2914
Bump to 0.3.8-rc.5
burningtnt Aug 15, 2025
27db2d9
Merge remote-tracking branch 'official/main' into feature/terracotta
burningtnt Aug 15, 2025
04f19f4
Fix.
burningtnt Aug 15, 2025
5306ddf
Fix: IDE Run/Debug.
burningtnt Aug 15, 2025
f14f6fc
Update Terracotta download links.
burningtnt Aug 16, 2025
27c3084
Revert "Fix: IDE Run/Debug."
burningtnt Aug 23, 2025
d089f3c
Merge remote-tracking branch 'official/main' into feature/terracotta
burningtnt Aug 23, 2025
a0b7254
Merge remote-tracking branch 'official/main' into feature/terracotta
burningtnt Aug 27, 2025
297b54d
Feature: Display player list.
burningtnt Sep 7, 2025
f525745
Feature: Display Copy Button more explicitly
burningtnt Sep 7, 2025
475688e
Fix: Invalid state deserializing. Bump Terracotta to 0.3.9-rc.1.
burningtnt Sep 7, 2025
b4f1419
Feature: 8mi tech Terracotta mirror.
burningtnt Sep 7, 2025
3151fc4
Bump Terracotta to 0.3.9-rc.3.
burningtnt Sep 7, 2025
3fd2b35
Code cleanup. Feature: Recover terracotta from errors. Fix: Terracott…
burningtnt Sep 9, 2025
6787423
Fix: Missing translations.
burningtnt Sep 9, 2025
3aaa2b0
Fix: checkstyle.
burningtnt Sep 9, 2025
9ea7e9b
Code cleanup.
burningtnt Sep 9, 2025
41a446a
Increase memory used by checkstyle
burningtnt Sep 9, 2025
df4f04b
Fix: Install. Feature: Animations.
burningtnt Sep 10, 2025
d1c9e3c
Feature: Animations.
burningtnt Sep 11, 2025
d56f7c5
Bump Terracotta to 0.3.9-rc.3
burningtnt Sep 11, 2025
d7dff5f
Merge remote-tracking branch 'official/main' into feature/terracotta
burningtnt Sep 12, 2025
32e65d1
Update to Java 17. Bump Terracotta to 0.3.9-rc.5.
burningtnt Sep 12, 2025
e86d18c
Feature: Selecting Room Code.
burningtnt Sep 12, 2025
4d1f27f
Code cleanup.
burningtnt Sep 12, 2025
1529b5f
Remove unused hyper-link callback.
burningtnt Sep 12, 2025
f814d6b
Fix: checkstyle.
burningtnt Sep 12, 2025
a493d86
Feature: Bump Terracotta to 0.3.9-rc.5. Enhance Player list UI.
burningtnt Sep 13, 2025
8becd37
Fix.
burningtnt Sep 13, 2025
5926146
Refactor native resolving.
burningtnt Sep 13, 2025
605b1d3
Add copyright contents.
burningtnt Sep 13, 2025
cb58b31
Fix: checkstyle
burningtnt Sep 13, 2025
8eefc7f
Fix: Remove failCounter in BackgroundDaemon
burningtnt Sep 13, 2025
13645eb
Merge remote-tracking branch 'official/main' into feature/terracotta
burningtnt Sep 13, 2025
02d208f
Merge remote-tracking branch 'official/main' into feature/terracotta
burningtnt Sep 14, 2025
58dbb80
Feature: Enabling users to setup Multiplayer Core from local files.
burningtnt Sep 19, 2025
c840451
Merge remote-tracking branch 'official/main' into feature/terracotta
burningtnt Sep 19, 2025
6b9ac95
Update terracotta pkg name.
burningtnt Sep 19, 2025
fb2c844
Feature: User frinedly error message while local archive doesn't match.
burningtnt Sep 20, 2025
8f5e208
Feature: Display third-party download links with description.
burningtnt Sep 20, 2025
d999ede
Shuffle third-party package links to perform a load balance
burningtnt Sep 20, 2025
d7e1ca7
Fix: Auto Terracotta package installation. Feature: Using classifier …
burningtnt Sep 20, 2025
360f10c
Feature: Migrade 'icon.png' placeholder to terracotta.png.
burningtnt Sep 20, 2025
7eeea37
Inform users of the filename before navigating to third-party downloa…
burningtnt Sep 20, 2025
21fad8e
Bump Terracotta to 0.3.9-rc.7. Feature: Tencent QQ Group third-party …
burningtnt Sep 20, 2025
bddc61c
Feature: Install Terracotta from local package while downloading.
burningtnt Sep 20, 2025
bdd105c
Merge remote-tracking branch 'official/main' into feature/terracotta
burningtnt Sep 20, 2025
91c897d
Code cleaup.
burningtnt Sep 20, 2025
8601f07
Update I18N.
burningtnt Sep 21, 2025
7bf3f69
Fix I18N.
burningtnt Sep 21, 2025
56f1079
Update I18N for Traditional Chinese
burningtnt Sep 21, 2025
eefba50
Update I18N.
burningtnt Sep 21, 2025
2a6c8e6
Update I18N for Traditional Chinese
burningtnt Sep 21, 2025
8a9d635
Update I18N for English.
burningtnt Sep 21, 2025
60dd58d
Inline lambda.
burningtnt Sep 24, 2025
b0c03e2
Update I18N to weaken the relationship between Terracotta and HMCL.
burningtnt Sep 24, 2025
816ddbe
Enable fetching Terracotta binary from Gitee.
burningtnt Sep 27, 2025
1aabff6
Merge remote-tracking branch 'official/main' into feature/terracotta
burningtnt Sep 27, 2025
fb1afe5
Remove irrelevant lines.
burningtnt Sep 27, 2025
49cb7c2
Merge remote-tracking branch 'official/main' into feature/terracotta
burningtnt Sep 27, 2025
196281e
Feature: Stop downloading process after user has installed terracotta…
burningtnt Sep 30, 2025
0f6a6b7
Bump Terracotta to 0.3.9-rc.9
burningtnt Sep 30, 2025
106b5ad
Code cleanup.
burningtnt Sep 30, 2025
5e2f848
Code cleanup.
burningtnt Sep 30, 2025
f2a160c
Nothing.
burningtnt Sep 30, 2025
e54ac5f
Feature: Enable users to export a log bundle when Terracotta is in 'e…
burningtnt Oct 1, 2025
f8f52db
Add Javadoc.
burningtnt Oct 1, 2025
4b237aa
Revise I18N logic.
burningtnt Oct 1, 2025
6351e30
Update I18N 'terracotta.status.exception.back'.
burningtnt Oct 1, 2025
2addb91
Update terracotta.json
burningtnt Oct 1, 2025
7286a96
Bump Terracotta to '0.3.9-rc.10'. Specific 'fetch' query argument whe…
burningtnt Oct 1, 2025
8ae0e2b
Merge remote-tracking branch 'official/main' into feature/terracotta
burningtnt Oct 3, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/check-codes.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ jobs:
java-version: '17'
java-package: 'jdk+fx'
- name: Check Codes
run: ./gradlew checkstyle checkTranslations --no-daemon --parallel
run: ./gradlew checkstyle checkTranslations --no-daemon --parallel --stacktrace
33 changes: 17 additions & 16 deletions HMCL/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,25 @@ tasks.compileJava {
options.compilerArgs.add("--add-exports=java.base/jdk.internal.loader=ALL-UNNAMED")
}

val addOpens = listOf(
"java.base/java.lang",
"java.base/java.lang.reflect",
"java.base/jdk.internal.loader",
"javafx.base/com.sun.javafx.binding",
"javafx.base/com.sun.javafx.event",
"javafx.base/com.sun.javafx.runtime",
"javafx.graphics/javafx.css",
"javafx.graphics/com.sun.javafx.stage",
"javafx.graphics/com.sun.prism",
"javafx.controls/com.sun.javafx.scene.control",
"javafx.controls/com.sun.javafx.scene.control.behavior",
"javafx.controls/javafx.scene.control.skin",
"jdk.attach/sun.tools.attach",
)

val hmclProperties = buildList {
add("hmcl.version" to project.version.toString())
add("hmcl.add-opens" to addOpens.joinToString(" "))
System.getenv("GITHUB_SHA")?.let {
add("hmcl.version.hash" to it)
}
Expand All @@ -149,22 +166,6 @@ val createPropertiesFile by tasks.registering {
}
}

val addOpens = listOf(
"java.base/java.lang",
"java.base/java.lang.reflect",
"java.base/jdk.internal.loader",
"javafx.base/com.sun.javafx.binding",
"javafx.base/com.sun.javafx.event",
"javafx.base/com.sun.javafx.runtime",
"javafx.graphics/javafx.css",
"javafx.graphics/com.sun.javafx.stage",
"javafx.graphics/com.sun.prism",
"javafx.controls/com.sun.javafx.scene.control",
"javafx.controls/com.sun.javafx.scene.control.behavior",
"javafx.controls/javafx.scene.control.skin",
"jdk.attach/sun.tools.attach",
)

tasks.jar {
enabled = false
dependsOn(tasks["shadowJar"])
Expand Down
10 changes: 9 additions & 1 deletion HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -915,7 +915,15 @@ public void onExit(int exitCode, ExitType exitType) {

}

public static final Queue<WeakReference<ManagedProcess>> PROCESSES = new ConcurrentLinkedQueue<>();
private static final Queue<WeakReference<ManagedProcess>> PROCESSES = new ConcurrentLinkedQueue<>();

public static int countMangedProcesses() {
PROCESSES.removeIf(it -> {
ManagedProcess process = it.get();
return process == null || !process.isRunning();
});
return PROCESSES.size();
}

public static void stopManagedProcesses() {
while (!PROCESSES.isEmpty())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,325 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 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.terracotta;

import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken;
import javafx.application.Platform;
import javafx.beans.property.ReadOnlyDoubleWrapper;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.task.DownloadException;
import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.terracotta.provider.ITerracottaProvider;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.InvocationDispatcher;
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.io.NetworkUtils;
import org.jackhuang.hmcl.util.platform.ManagedProcess;
import org.jackhuang.hmcl.util.platform.SystemUtils;
import org.jackhuang.hmcl.util.tree.TarFileTree;
import org.jetbrains.annotations.Nullable;

import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.LockSupport;

import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;

public final class TerracottaManager {
private TerracottaManager() {
}

private static final AtomicReference<TerracottaState> STATE_V = new AtomicReference<>(TerracottaState.Bootstrap.INSTANCE);
private static final ReadOnlyObjectWrapper<TerracottaState> STATE = new ReadOnlyObjectWrapper<>(STATE_V.getPlain());
private static final InvocationDispatcher<TerracottaState> STATE_D = InvocationDispatcher.runOn(Platform::runLater, STATE::set);

static {
Task.runAsync(() -> {
if (TerracottaMetadata.PROVIDER == null) {
setState(new TerracottaState.Fatal(TerracottaState.Fatal.Type.OS));
LOG.warning("Terracotta hasn't support your OS: " + org.jackhuang.hmcl.util.platform.Platform.SYSTEM_PLATFORM);
} else {
switch (TerracottaMetadata.PROVIDER.status()) {
case NOT_EXIST -> setState(new TerracottaState.Uninitialized(false));
case LEGACY_VERSION -> setState(new TerracottaState.Uninitialized(true));
case READY -> launch(setState(new TerracottaState.Launching()));
}
}
}).whenComplete(exception -> {
if (exception != null) {
compareAndSet(TerracottaState.Bootstrap.INSTANCE, new TerracottaState.Fatal(TerracottaState.Fatal.Type.UNKNOWN));
}
}).start();
}

public static ReadOnlyObjectProperty<TerracottaState> stateProperty() {
return STATE.getReadOnlyProperty();
}

static {
Lang.thread(() -> {
while (true) {
TerracottaState state = STATE_V.get();
if (!(state instanceof TerracottaState.PortSpecific portSpecific)) {
LockSupport.parkNanos(500_000);
continue;
}

int port = portSpecific.port;
int index = state instanceof TerracottaState.Ready ready ? ready.index : Integer.MIN_VALUE;

TerracottaState next;
try {
next = new GetTask(URI.create(String.format("http://127.0.0.1:%d/state", port)))
.setSignificance(Task.TaskSignificance.MINOR)
.thenApplyAsync(jsonString -> {
TerracottaState.Ready object = JsonUtils.fromNonNullJson(jsonString, TypeToken.get(TerracottaState.Ready.class));
if (object.index <= index) {
return null;
}

object.port = port;
return object;
})
.setSignificance(Task.TaskSignificance.MINOR)
.run();
} catch (Exception e) {
LOG.warning("Cannot fetch state from Terracotta.", e);
next = new TerracottaState.Fatal(TerracottaState.Fatal.Type.TERRACOTTA);
}

if (next != null) {
compareAndSet(state, next);
}

LockSupport.parkNanos(500_000);
}
}, "Terracotta Background Daemon", true);
}

public static boolean validate(Path file) {
return FileUtils.getName(file).equalsIgnoreCase(TerracottaMetadata.PACKAGE_NAME);
}

public static TerracottaState.Preparing install(@Nullable Path file) {
FXUtils.checkFxUserThread();

TerracottaState state = STATE_V.get();
if (!(state instanceof TerracottaState.Uninitialized ||
state instanceof TerracottaState.Preparing preparing && preparing.hasInstallFence() ||
state instanceof TerracottaState.Fatal && ((TerracottaState.Fatal) state).isRecoverable())
) {
return null;
}

if (file != null && !FileUtils.getName(file).equalsIgnoreCase(TerracottaMetadata.PACKAGE_NAME)) {
return null;
}

TerracottaState.Preparing preparing;
if (state instanceof TerracottaState.Preparing it) {
preparing = it;
} else {
preparing = new TerracottaState.Preparing(new ReadOnlyDoubleWrapper(-1));
}

Task.supplyAsync(Schedulers.io(), () -> {
return file != null ? TarFileTree.open(file) : null;
}).thenComposeAsync(Schedulers.javafx(), tree -> {
return getProvider().install(preparing, tree).whenComplete(exception -> {
if (tree != null) {
tree.close();
}
if (exception != null) {
throw exception;
}
});
}).whenComplete(exception -> {
if (exception == null) {
try {
TerracottaMetadata.removeLegacyVersionFiles();
} catch (IOException e) {
LOG.warning("Unable to remove legacy terracotta files.", e);
}

TerracottaState.Launching launching = new TerracottaState.Launching();
if (compareAndSet(preparing, launching)) {
launch(launching);
}
} else if (exception instanceof ITerracottaProvider.ArchiveFileMissingException) {
LOG.warning("Cannot install terracotta from local package.", exception);
compareAndSet(preparing, new TerracottaState.Fatal(TerracottaState.Fatal.Type.INSTALL));
} else if (exception instanceof DownloadException) {
compareAndSet(preparing, new TerracottaState.Fatal(TerracottaState.Fatal.Type.NETWORK));
} else {
compareAndSet(preparing, new TerracottaState.Fatal(TerracottaState.Fatal.Type.INSTALL));
}
}).start();

return setState(preparing);
}

private static ITerracottaProvider getProvider() {
ITerracottaProvider provider = TerracottaMetadata.PROVIDER;
if (provider == null) {
throw new AssertionError("Terracotta Provider must NOT be null.");
}
return provider;
}

public static TerracottaState recover(@Nullable Path file) {
FXUtils.checkFxUserThread();

TerracottaState state = STATE_V.get();
if (!(state instanceof TerracottaState.Fatal && ((TerracottaState.Fatal) state).isRecoverable())) {
return null;
}

try {
return switch (getProvider().status()) {
case NOT_EXIST, LEGACY_VERSION -> install(file);
case READY -> {
TerracottaState.Launching launching = setState(new TerracottaState.Launching());
launch(launching);
yield launching;
}
};
} catch (NullPointerException | IOException e) {
LOG.warning("Cannot determine Terracotta state.", e);
return setState(new TerracottaState.Fatal(TerracottaState.Fatal.Type.UNKNOWN));
}
}

private static void launch(TerracottaState.Launching state) {
Task.supplyAsync(() -> {
Path path = Files.createTempDirectory(String.format("hmcl-terracotta-%d", ThreadLocalRandom.current().nextLong())).resolve("http").toAbsolutePath();
ManagedProcess process = new ManagedProcess(new ProcessBuilder(getProvider().ofCommandLine(path)));
process.pumpInputStream(SystemUtils::onLogLine);
process.pumpErrorStream(SystemUtils::onLogLine);

long exitTime = -1;
while (true) {
if (Files.exists(path)) {
JsonObject object = JsonUtils.fromNonNullJson(Files.readString(path), JsonObject.class);
return object.get("port").getAsInt();
}

if (!process.isRunning()) {
if (exitTime == -1) {
exitTime = System.currentTimeMillis();
} else if (System.currentTimeMillis() - exitTime >= 10000) {
throw new IllegalStateException("Process has exited for 10s.");
}
}
}
}).whenComplete(Schedulers.javafx(), (port, exception) -> {
TerracottaState next;
if (exception == null) {
next = new TerracottaState.Unknown(port);
} else {
next = new TerracottaState.Fatal(TerracottaState.Fatal.Type.TERRACOTTA);
}
compareAndSet(state, next);
}).start();
}

public static Task<String> exportLogs() {
if (STATE_V.get() instanceof TerracottaState.PortSpecific portSpecific) {
return new GetTask(URI.create(String.format("http://127.0.0.1:%d/log?fetch=true", portSpecific.port)))
.setSignificance(Task.TaskSignificance.MINOR);
}
return Task.completed(null);
}

public static TerracottaState.Waiting setWaiting() {
TerracottaState state = STATE_V.get();
if (state instanceof TerracottaState.PortSpecific portSpecific) {
new GetTask(URI.create(String.format("http://127.0.0.1:%d/state/ide", portSpecific.port)))
.setSignificance(Task.TaskSignificance.MINOR)
.start();
return new TerracottaState.Waiting(-1, -1, null);
}
return null;
}

private static String getPlayerName() {
Account account = Accounts.getSelectedAccount();
return account != null ? account.getCharacter() : i18n("terracotta.player_anonymous");
}

public static TerracottaState.HostScanning setScanning() {
TerracottaState state = STATE_V.get();
if (state instanceof TerracottaState.PortSpecific portSpecific) {
new GetTask(NetworkUtils.toURI(String.format(
"http://127.0.0.1:%d/state/scanning?player=%s", portSpecific.port, getPlayerName()))
).setSignificance(Task.TaskSignificance.MINOR).start();

return new TerracottaState.HostScanning(-1, -1, null);
}
return null;
}

public static Task<TerracottaState.GuestStarting> setGuesting(String room) {
TerracottaState state = STATE_V.get();
if (state instanceof TerracottaState.PortSpecific portSpecific) {
return new GetTask(NetworkUtils.toURI(String.format(
"http://127.0.0.1:%d/state/guesting?room=%s&player=%s", portSpecific.port, room, getPlayerName()
)))
.setSignificance(Task.TaskSignificance.MINOR)
.thenSupplyAsync(() -> new TerracottaState.GuestStarting(-1, -1, null))
.setSignificance(Task.TaskSignificance.MINOR);
} else {
return null;
}
}

private static <T extends TerracottaState> T setState(T value) {
if (value == null) {
throw new AssertionError();
}

STATE_V.set(value);
STATE_D.accept(value);
return value;
}

private static boolean compareAndSet(TerracottaState previous, TerracottaState next) {
if (next == null) {
throw new AssertionError();
}

if (STATE_V.compareAndSet(previous, next)) {
STATE_D.accept(next);
return true;
} else {
return false;
}
}
}
Loading