-
Notifications
You must be signed in to change notification settings - Fork 779
Terracotta | 陶瓦联机 #4215
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Terracotta | 陶瓦联机 #4215
Changes from all commits
Commits
Show all changes
89 commits
Select commit
Hold shift + click to select a range
c9c56a6
Feature (Draft): Terracotta
burningtnt a4d6580
Use cursor hand for codes.
burningtnt 4618a51
Feature: MacOS Support.
burningtnt 8f664ed
Fix.
burningtnt 506620b
Fix
burningtnt e655de7
Enhance UI.
burningtnt d48cfc3
Enhance UI.
burningtnt aead89d
Feature: Verify intergrity before lauching terracotta.
burningtnt 1529d3e
Feature: Initialize Translations.
burningtnt f0fc936
Fix: Make terracotta daemon a detached process on Windows.
burningtnt 3d3ee20
Move configs into dedicated json file. Feature: Display update messag…
burningtnt e7021e7
Feature: Delete out of date terracotta cores.
burningtnt a97eb7e
Revise Translation.
burningtnt 851a312
Fix: Encode URL before concating query arguments. Code cleanup.
burningtnt fa7ca1c
Bump to Terracotta 0.3.8-rc.3
burningtnt 6229aae
Code cleanup
burningtnt 3d84ba6
Bump to Terracotta 0.3.8-rc.4
burningtnt 58c2914
Bump to 0.3.8-rc.5
burningtnt 27db2d9
Merge remote-tracking branch 'official/main' into feature/terracotta
burningtnt 04f19f4
Fix.
burningtnt 5306ddf
Fix: IDE Run/Debug.
burningtnt f14f6fc
Update Terracotta download links.
burningtnt 27c3084
Revert "Fix: IDE Run/Debug."
burningtnt d089f3c
Merge remote-tracking branch 'official/main' into feature/terracotta
burningtnt a0b7254
Merge remote-tracking branch 'official/main' into feature/terracotta
burningtnt 297b54d
Feature: Display player list.
burningtnt f525745
Feature: Display Copy Button more explicitly
burningtnt 475688e
Fix: Invalid state deserializing. Bump Terracotta to 0.3.9-rc.1.
burningtnt b4f1419
Feature: 8mi tech Terracotta mirror.
burningtnt 3151fc4
Bump Terracotta to 0.3.9-rc.3.
burningtnt 3fd2b35
Code cleanup. Feature: Recover terracotta from errors. Fix: Terracott…
burningtnt 6787423
Fix: Missing translations.
burningtnt 3aaa2b0
Fix: checkstyle.
burningtnt 9ea7e9b
Code cleanup.
burningtnt 41a446a
Increase memory used by checkstyle
burningtnt df4f04b
Fix: Install. Feature: Animations.
burningtnt d1c9e3c
Feature: Animations.
burningtnt d56f7c5
Bump Terracotta to 0.3.9-rc.3
burningtnt d7dff5f
Merge remote-tracking branch 'official/main' into feature/terracotta
burningtnt 32e65d1
Update to Java 17. Bump Terracotta to 0.3.9-rc.5.
burningtnt e86d18c
Feature: Selecting Room Code.
burningtnt 4d1f27f
Code cleanup.
burningtnt 1529b5f
Remove unused hyper-link callback.
burningtnt f814d6b
Fix: checkstyle.
burningtnt a493d86
Feature: Bump Terracotta to 0.3.9-rc.5. Enhance Player list UI.
burningtnt 8becd37
Fix.
burningtnt 5926146
Refactor native resolving.
burningtnt 605b1d3
Add copyright contents.
burningtnt cb58b31
Fix: checkstyle
burningtnt 8eefc7f
Fix: Remove failCounter in BackgroundDaemon
burningtnt 13645eb
Merge remote-tracking branch 'official/main' into feature/terracotta
burningtnt 02d208f
Merge remote-tracking branch 'official/main' into feature/terracotta
burningtnt 58dbb80
Feature: Enabling users to setup Multiplayer Core from local files.
burningtnt c840451
Merge remote-tracking branch 'official/main' into feature/terracotta
burningtnt 6b9ac95
Update terracotta pkg name.
burningtnt fb2c844
Feature: User frinedly error message while local archive doesn't match.
burningtnt 8f5e208
Feature: Display third-party download links with description.
burningtnt d999ede
Shuffle third-party package links to perform a load balance
burningtnt d7e1ca7
Fix: Auto Terracotta package installation. Feature: Using classifier …
burningtnt 360f10c
Feature: Migrade 'icon.png' placeholder to terracotta.png.
burningtnt 7eeea37
Inform users of the filename before navigating to third-party downloa…
burningtnt 21fad8e
Bump Terracotta to 0.3.9-rc.7. Feature: Tencent QQ Group third-party …
burningtnt bddc61c
Feature: Install Terracotta from local package while downloading.
burningtnt bdd105c
Merge remote-tracking branch 'official/main' into feature/terracotta
burningtnt 91c897d
Code cleaup.
burningtnt 8601f07
Update I18N.
burningtnt 7bf3f69
Fix I18N.
burningtnt 56f1079
Update I18N for Traditional Chinese
burningtnt eefba50
Update I18N.
burningtnt 2a6c8e6
Update I18N for Traditional Chinese
burningtnt 8a9d635
Update I18N for English.
burningtnt 60dd58d
Inline lambda.
burningtnt b0c03e2
Update I18N to weaken the relationship between Terracotta and HMCL.
burningtnt 816ddbe
Enable fetching Terracotta binary from Gitee.
burningtnt 1aabff6
Merge remote-tracking branch 'official/main' into feature/terracotta
burningtnt fb1afe5
Remove irrelevant lines.
burningtnt 49cb7c2
Merge remote-tracking branch 'official/main' into feature/terracotta
burningtnt 196281e
Feature: Stop downloading process after user has installed terracotta…
burningtnt 0f6a6b7
Bump Terracotta to 0.3.9-rc.9
burningtnt 106b5ad
Code cleanup.
burningtnt 5e2f848
Code cleanup.
burningtnt f2a160c
Nothing.
burningtnt e54ac5f
Feature: Enable users to export a log bundle when Terracotta is in 'e…
burningtnt f8f52db
Add Javadoc.
burningtnt 4b237aa
Revise I18N logic.
burningtnt 6351e30
Update I18N 'terracotta.status.exception.back'.
burningtnt 2addb91
Update terracotta.json
burningtnt 7286a96
Bump Terracotta to '0.3.9-rc.10'. Specific 'fetch' query argument whe…
burningtnt 8ae0e2b
Merge remote-tracking branch 'official/main' into feature/terracotta
burningtnt File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
325 changes: 325 additions & 0 deletions
325
HMCL/src/main/java/org/jackhuang/hmcl/terracotta/TerracottaManager.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) { | ||
burningtnt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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; | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.