add: more brief launching steps

This commit is contained in:
huanghongxun
2020-02-19 13:34:40 +08:00
parent a144a5eee0
commit 1ea9170b2e
14 changed files with 310 additions and 210 deletions

View File

@@ -20,11 +20,7 @@ package org.jackhuang.hmcl.game;
import javafx.application.Platform;
import javafx.stage.Stage;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.CharacterDeletedException;
import org.jackhuang.hmcl.auth.CredentialExpiredException;
import org.jackhuang.hmcl.auth.*;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloadException;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.MaintainTask;
@@ -51,11 +47,7 @@ import org.jackhuang.hmcl.ui.LogWindow;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.util.Log4jLevel;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.function.ExceptionalSupplier;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
import org.jackhuang.hmcl.util.io.ResponseCodeException;
import org.jackhuang.hmcl.util.platform.CommandBuilder;
@@ -68,16 +60,9 @@ import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URL;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Queue;
import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.Lang.mapOf;
@@ -102,9 +87,11 @@ public final class LauncherHelper {
this.setting = profile.getVersionSetting(selectedVersion);
this.launcherVisibility = setting.getLauncherVisibility();
this.showLogs = setting.isShowLogs();
this.launchingStepsPane.setTitle(i18n("version.launch"));
}
private final TaskExecutorDialogPane launchingStepsPane = new TaskExecutorDialogPane(it -> {});
private CountDownLatch launchingLatch;
public void setTestMode() {
launcherVisibility = LauncherVisibility.KEEP;
@@ -140,15 +127,15 @@ public final class LauncherHelper {
Version version = MaintainTask.maintain(repository, repository.getResolvedVersion(selectedVersion));
Optional<String> gameVersion = GameVersion.minecraftVersion(repository.getVersionJar(version));
TaskExecutor executor = Task.runAsync(Schedulers.javafx(), () -> emitStatus(LoadingState.DEPENDENCIES))
.thenComposeAsync(() -> {
TaskExecutor executor = Task.runAsync(() -> {
})
.thenComposeAsync(() -> Task.composeAsync(() -> {
if (setting.isNotCheckGame())
return null;
else
return dependencyManager.checkGameCompletionAsync(version);
})
.thenRunAsync(Schedulers.javafx(), () -> emitStatus(LoadingState.MODS))
.thenComposeAsync(() -> {
}).withStage("launch.state.dependencies"))
.thenComposeAsync(() -> Task.composeAsync(null, () -> {
try {
ModpackConfiguration<?> configuration = ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(selectedVersion));
if ("Curse".equals(configuration.getType()))
@@ -160,9 +147,8 @@ public final class LauncherHelper {
} catch (IOException e) {
return null;
}
})
.thenRunAsync(Schedulers.javafx(), () -> emitStatus(LoadingState.LOGGING_IN))
.thenSupplyAsync(i18n("launch.state.logging_in"), () -> {
}).withStage("launch.state.modpack"))
.thenComposeAsync(() -> Task.supplyAsync((String) null, () -> {
try {
return account.logIn();
} catch (CredentialExpiredException e) {
@@ -172,31 +158,27 @@ public final class LauncherHelper {
LOG.warning("Authentication failed, try playing offline: " + e);
return account.playOffline().orElseThrow(() -> e);
}
})
.thenApplyAsync(Schedulers.javafx(), authInfo -> {
emitStatus(LoadingState.LAUNCHING);
return authInfo;
})
.thenApplyAsync(authInfo -> new HMCLGameLauncher(
repository,
version.getPatches().isEmpty() ? repository.getResolvedVersion(selectedVersion) : version,
authInfo,
setting.toLaunchOptions(profile.getGameDir(), !setting.isNotCheckJVM()),
launcherVisibility == LauncherVisibility.CLOSE
? null // Unnecessary to start listening to game process output when close launcher immediately after game launched.
: new HMCLProcessListener(authInfo, gameVersion.isPresent())
))
.thenComposeAsync(launcher -> { // launcher is prev task's result
}).withStage("launch.state.logging_in"))
.thenComposeAsync(authInfo -> Task.supplyAsync((String) null, () -> {
return new HMCLGameLauncher(
repository,
version.getPatches().isEmpty() ? repository.getResolvedVersion(selectedVersion) : version,
authInfo,
setting.toLaunchOptions(profile.getGameDir(), !setting.isNotCheckJVM()),
launcherVisibility == LauncherVisibility.CLOSE
? null // Unnecessary to start listening to game process output when close launcher immediately after game launched.
: new HMCLProcessListener(authInfo, gameVersion.isPresent())
);
}).thenComposeAsync(launcher -> { // launcher is prev task's result
if (scriptFile == null) {
return new LaunchTask<>(launcher::launch).setName(i18n("version.launch"));
return Task.supplyAsync((String) null, launcher::launch);
} else {
return new LaunchTask<ManagedProcess>(() -> {
return Task.supplyAsync((String) null, () -> {
launcher.makeLaunchScript(scriptFile);
return null;
}).setName(i18n("version.launch_script"));
});
}
})
.thenAcceptAsync(process -> { // process is LaunchTask's result
}).thenAcceptAsync(process -> { // process is LaunchTask's result
if (scriptFile == null) {
PROCESSES.add(process);
if (launcherVisibility == LauncherVisibility.CLOSE)
@@ -212,19 +194,18 @@ public final class LauncherHelper {
Controllers.dialog(i18n("version.launch_script.success", scriptFile.getAbsolutePath()));
});
}
})
}).thenRunAsync(null, Schedulers.defaultScheduler(), () -> {
launchingLatch = new CountDownLatch(1);
launchingLatch.await();
}).withStage("launch.state.waiting_launching"))
.cancellableExecutor();
launchingStepsPane.setExecutor(executor, false);
launchingStepsPane.setExecutor(executor, Lang.immutableListOf(
"launch.state.dependencies",
"launch.state.modpack",
"launch.state.logging_in",
"launch.state.waiting_launching"), false);
executor.addTaskListener(new TaskListener() {
final AtomicInteger finished = new AtomicInteger(0);
@Override
public void onFinished(Task<?> task) {
finished.incrementAndGet();
int runningTasks = executor.getRunningTasks();
Platform.runLater(() -> launchingStepsPane.setProgress(1.0 * finished.get() / runningTasks));
}
@Override
public void onStop(boolean success, TaskExecutor executor) {
@@ -432,15 +413,6 @@ public final class LauncherHelper {
onAccept.run();
}
public void emitStatus(LoadingState state) {
if (state == LoadingState.DONE) {
launchingStepsPane.fireEvent(new DialogCloseEvent());
}
launchingStepsPane.setTitle(state.getLocalizedMessage());
launchingStepsPane.setSubtitle((state.ordinal() + 1) + " / " + LoadingState.values().length);
}
private void checkExit() {
switch (launcherVisibility) {
case HIDE_AND_REOPEN:
@@ -465,19 +437,6 @@ public final class LauncherHelper {
}
}
private static class LaunchTask<T> extends Task<T> {
private final ExceptionalSupplier<T, Exception> supplier;
public LaunchTask(ExceptionalSupplier<T, Exception> supplier) {
this.supplier = supplier;
}
@Override
public void execute() throws Exception {
setResult(supplier.get());
}
}
/**
* The managed process listener.
* Guarantee that one [JavaProcess], one [HMCLProcessListener].
@@ -534,7 +493,7 @@ public final class LauncherHelper {
// these codes will be executed.
if (Controllers.getStage() != null) {
Controllers.getStage().hide();
emitStatus(LoadingState.DONE);
launchingLatch.countDown();
}
});
break;
@@ -543,7 +502,7 @@ public final class LauncherHelper {
break;
case KEEP:
Platform.runLater(() -> {
emitStatus(LoadingState.DONE);
launchingLatch.countDown();
});
break;
case HIDE:
@@ -552,7 +511,7 @@ public final class LauncherHelper {
// these codes will be executed.
if (Controllers.getStage() != null) {
Controllers.getStage().close();
emitStatus(LoadingState.DONE);
launchingLatch.countDown();
}
});
break;

View File

@@ -248,18 +248,13 @@ public final class Controllers {
return pane;
}
public static Region taskDialog(TaskExecutor executor, String title) {
return taskDialog(executor, title, "");
public static TaskExecutorDialogPane taskDialog(TaskExecutor executor, String title) {
return taskDialog(executor, title, null);
}
public static Region taskDialog(TaskExecutor executor, String title, String subtitle) {
return taskDialog(executor, title, subtitle, null);
}
public static Region taskDialog(TaskExecutor executor, String title, String subtitle, Consumer<Region> onCancel) {
public static TaskExecutorDialogPane taskDialog(TaskExecutor executor, String title, Consumer<Region> onCancel) {
TaskExecutorDialogPane pane = new TaskExecutorDialogPane(onCancel);
pane.setTitle(title);
pane.setSubtitle(subtitle);
pane.setExecutor(executor);
dialog(pane);
return pane;

View File

@@ -71,6 +71,10 @@ public final class SVG {
return createSVGPath("M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z", fill, width, height);
}
public static Node dotsHorizontal(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M16,12A2,2 0 0,1 18,10A2,2 0 0,1 20,12A2,2 0 0,1 18,14A2,2 0 0,1 16,12M10,12A2,2 0 0,1 12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12M4,12A2,2 0 0,1 6,10A2,2 0 0,1 8,12A2,2 0 0,1 6,14A2,2 0 0,1 4,12Z", fill, width, height);
}
public static Node delete(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z", fill, width, height);
}
@@ -170,4 +174,8 @@ public final class SVG {
public static Node check(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z", fill, width, height);
}
public static Node arrowRight(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M4,11V13H16L10.5,18.5L11.92,19.92L19.84,12L11.92,4.08L10.5,5.5L16,11H4Z", fill, width, height);
}
}

View File

@@ -53,22 +53,37 @@ public class AdvancedListBox extends ScrollPane {
return this;
}
public AdvancedListBox remove(Node child) {
if (child instanceof Pane)
container.getChildren().remove(child);
public AdvancedListBox add(int index, Node child) {
if (child instanceof Pane || child instanceof AdvancedListItem)
container.getChildren().add(index, child);
else {
StackPane pane = null;
for (Node node : container.getChildren())
StackPane pane = new StackPane();
pane.getStyleClass().add("advanced-list-box-item");
pane.getChildren().setAll(child);
container.getChildren().add(index, pane);
}
return this;
}
public AdvancedListBox remove(Node child) {
container.getChildren().remove(indexOf(child));
return this;
}
public int indexOf(Node child) {
if (child instanceof Pane) {
return container.getChildren().indexOf(child);
} else {
for (int i = 0; i < container.getChildren().size(); ++i) {
Node node = container.getChildren().get(i);
if (node instanceof StackPane) {
ObservableList<Node> list = ((StackPane) node).getChildren();
if (list.size() == 1 && list.get(0) == child)
pane = (StackPane) node;
return i;
}
if (pane == null)
throw new Error();
container.getChildren().remove(pane);
}
return -1;
}
return this;
}
public AdvancedListBox startCategory(String category) {

View File

@@ -18,18 +18,19 @@
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXProgressBar;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.task.TaskListener;
import org.jackhuang.hmcl.ui.FXUtils;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
@@ -38,14 +39,11 @@ import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
public class TaskExecutorDialogPane extends StackPane {
private TaskExecutor executor;
private Consumer<Region> onCancel;
private final Consumer<FileDownloadTask.SpeedEvent> speedEventHandler;
@FXML
private JFXProgressBar progressBar;
@FXML
private Label lblTitle;
@FXML
private Label lblSubtitle;
@FXML
private Label lblProgress;
@FXML
private JFXButton btnCancel;
@@ -62,10 +60,24 @@ public class TaskExecutorDialogPane extends StackPane {
onCancel.accept(this);
});
lblProgress.textProperty().bind(Bindings.createStringBinding(
() -> taskListPane.finishedTasksProperty().get() + "/" + taskListPane.totTasksProperty().get(),
taskListPane.finishedTasksProperty(), taskListPane.totTasksProperty()
));
speedEventHandler = speedEvent -> {
String unit = "B/s";
double speed = speedEvent.getSpeed();
if (speed > 1024) {
speed /= 1024;
unit = "KB/s";
}
if (speed > 1024) {
speed /= 1024;
unit = "MB/s";
}
double finalSpeed = speed;
String finalUnit = unit;
Platform.runLater(() -> {
lblProgress.setText(String.format("%.1f", finalSpeed) + " " + finalUnit);
});
};
FileDownloadTask.speedEvent.channel(FileDownloadTask.SpeedEvent.class).registerWeak(speedEventHandler);
}
public void setExecutor(TaskExecutor executor) {
@@ -73,10 +85,18 @@ public class TaskExecutorDialogPane extends StackPane {
}
public void setExecutor(TaskExecutor executor, boolean autoClose) {
setExecutor(executor, Collections.emptyList(), autoClose);
}
public void setExecutor(TaskExecutor executor, List<String> stages) {
setExecutor(executor, stages, true);
}
public void setExecutor(TaskExecutor executor, List<String> stages, boolean autoClose) {
this.executor = executor;
if (executor != null) {
taskListPane.setExecutor(executor);
taskListPane.setExecutor(executor, stages);
if (autoClose)
executor.addTaskListener(new TaskListener() {
@@ -100,25 +120,6 @@ public class TaskExecutorDialogPane extends StackPane {
lblTitle.setText(currentState);
}
public StringProperty subtitleProperty() {
return lblSubtitle.textProperty();
}
public String getSubtitle() {
return lblSubtitle.getText();
}
public void setSubtitle(String subtitle) {
lblSubtitle.setText(subtitle);
}
public void setProgress(double progress) {
if (progress == Double.MAX_VALUE)
progressBar.setVisible(false);
else
progressBar.setProgress(progress);
}
public void setCancel(Consumer<Region> onCancel) {
this.onCancel = onCancel;

View File

@@ -21,6 +21,8 @@ import com.jfoenix.controls.JFXProgressBar;
import javafx.application.Platform;
import javafx.beans.property.ReadOnlyIntegerProperty;
import javafx.beans.property.ReadOnlyIntegerWrapper;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
@@ -37,20 +39,22 @@ import org.jackhuang.hmcl.mod.ModpackUpdateTask;
import org.jackhuang.hmcl.mod.curse.CurseCompletionTask;
import org.jackhuang.hmcl.mod.curse.CurseInstallTask;
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackInstallTask;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.task.TaskListener;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import java.util.HashMap;
import java.util.Map;
import java.util.*;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class TaskListPane extends StackPane {
private final AdvancedListBox listBox = new AdvancedListBox();
private final Map<Task<?>, ProgressListNode> nodes = new HashMap<>();
private final ReadOnlyIntegerWrapper finishedTasks = new ReadOnlyIntegerWrapper();
private final ReadOnlyIntegerWrapper totTasks = new ReadOnlyIntegerWrapper();
private final List<StageNode> stageNodes = new ArrayList<>();
public TaskListPane() {
listBox.setSpacing(0);
@@ -58,33 +62,35 @@ public final class TaskListPane extends StackPane {
getChildren().setAll(listBox);
}
public ReadOnlyIntegerProperty finishedTasksProperty() {
return finishedTasks.getReadOnlyProperty();
}
public ReadOnlyIntegerProperty totTasksProperty() {
return totTasks.getReadOnlyProperty();
}
public void setExecutor(TaskExecutor executor) {
setExecutor(executor, Collections.emptyList());
}
public void setExecutor(TaskExecutor executor, List<String> stages) {
executor.addTaskListener(new TaskListener() {
@Override
public void onStart() {
Platform.runLater(() -> {
stageNodes.clear();
listBox.clear();
finishedTasks.set(0);
totTasks.set(0);
stageNodes.addAll(stages.stream().map(StageNode::new).collect(Collectors.toList()));
stageNodes.forEach(listBox::add);
});
}
@Override
public void onReady(Task<?> task) {
Platform.runLater(() -> totTasks.set(totTasks.getValue() + 1));
if (task instanceof Task.StageTask) {
Platform.runLater(() -> {
stageNodes.stream().filter(x -> x.stage.equals(task.getStage())).findAny().ifPresent(StageNode::begin);
});
}
}
@Override
public void onRunning(Task<?> task) {
if (!task.getSignificance().shouldShow())
if (!task.getSignificance().shouldShow() || task.getName() == null)
return;
if (task instanceof GameAssetDownloadTask) {
@@ -117,34 +123,77 @@ public final class TaskListPane extends StackPane {
ProgressListNode node = new ProgressListNode(task);
nodes.put(task, node);
Platform.runLater(() -> listBox.add(node));
Platform.runLater(() -> {
StageNode stageNode = stageNodes.stream().filter(x -> x.stage.equals(task.getStage())).findAny().orElse(null);
listBox.add(listBox.indexOf(stageNode) + 1, node);
});
}
@Override
public void onFinished(Task<?> task) {
if (task instanceof Task.StageTask) {
Platform.runLater(() -> {
stageNodes.stream().filter(x -> x.stage.equals(task.getStage())).findAny().ifPresent(StageNode::succeed);
});
}
ProgressListNode node = nodes.remove(task);
if (node == null)
return;
node.unbind();
Platform.runLater(() -> {
listBox.remove(node);
finishedTasks.set(finishedTasks.getValue() + 1);
});
}
@Override
public void onFailed(Task<?> task, Throwable throwable) {
if (task instanceof Task.StageTask) {
Platform.runLater(() -> {
stageNodes.stream().filter(x -> x.stage.equals(task.getStage())).findAny().ifPresent(StageNode::fail);
});
}
ProgressListNode node = nodes.remove(task);
if (node == null)
return;
Platform.runLater(() -> {
node.setThrowable(throwable);
finishedTasks.set(finishedTasks.getValue() + 1);
});
}
});
}
private static class StageNode extends BorderPane {
private final String stage;
private final Label title = new Label();
private boolean started = false;
public StageNode(String stage) {
this.stage = stage;
title.setText(i18n(stage));
BorderPane.setAlignment(title, Pos.CENTER_LEFT);
BorderPane.setMargin(title, new Insets(0, 0, 0, 8));
setPadding(new Insets(0, 0, 8, 4));
setCenter(title);
setLeft(SVG.dotsHorizontal(Theme.blackFillBinding(), 14, 14));
}
public void begin() {
if (started) return;
started = true;
setLeft(FXUtils.limitingSize(SVG.arrowRight(Theme.blackFillBinding(), 14, 14), 14, 14));
}
public void fail() {
setLeft(FXUtils.limitingSize(SVG.close(Theme.blackFillBinding(), 14, 14), 14, 14));
}
public void succeed() {
setLeft(FXUtils.limitingSize(SVG.check(Theme.blackFillBinding(), 14, 14), 14, 14));
}
}
private static class ProgressListNode extends BorderPane {
private final JFXProgressBar bar = new JFXProgressBar();
private final Label title = new Label();

View File

@@ -42,7 +42,6 @@ public interface TaskExecutorDialogWizardDisplayer extends AbstractWizardDisplay
});
pane.setTitle(i18n("message.doing"));
pane.setProgress(Double.MAX_VALUE);
if (settings.containsKey("title")) {
Object title = settings.get("title");
if (title instanceof StringProperty)
@@ -51,14 +50,6 @@ public interface TaskExecutorDialogWizardDisplayer extends AbstractWizardDisplay
pane.setTitle((String) title);
}
if (settings.containsKey("subtitle")) {
Object subtitle = settings.get("subtitle");
if (subtitle instanceof StringProperty)
pane.subtitleProperty().bind((StringProperty) subtitle);
else if (subtitle instanceof String)
pane.setSubtitle((String) subtitle);
}
runInFX(() -> {
TaskExecutor executor = task.cancellableExecutor(new TaskListener() {
@Override

View File

@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import com.jfoenix.controls.JFXButton?>
<?import com.jfoenix.controls.JFXProgressBar?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.layout.*?>
@@ -11,13 +10,9 @@
xmlns:fx="http://javafx.com/fxml"
type="StackPane" FXUtils.limitWidth="500" FXUtils.limitHeight="300">
<BorderPane>
<top>
<JFXProgressBar fx:id="progressBar" StackPane.alignment="TOP_CENTER" />
</top>
<center>
<VBox alignment="TOP_CENTER" style="-fx-padding: 16px;">
<Label fx:id="lblTitle" style="-fx-font-size: 20px;" />
<Label fx:id="lblSubtitle" style="-fx-font-size: 14px;" />
<VBox style="-fx-padding: 16px;">
<Label fx:id="lblTitle" style="-fx-font-size: 14px; -fx-font-weight: BOLD;" />
<ScrollPane fitToHeight="true" fitToWidth="true" VBox.vgrow="ALWAYS">
<TaskListPane fx:id="taskListPane" />

View File

@@ -179,10 +179,10 @@ launch.failed.decompressing_natives=無法解壓縮遊戲資源庫。
launch.failed.download_library=無法下載遊戲相依元件 %s。
launch.failed.executable_permission=無法為啟動檔案新增執行權限。
launch.failed.exited_abnormally=遊戲非正常退出,請查看記錄檔案,或聯絡他人尋求幫助。
launch.state.dependencies=正在處理遊戲相依元件
launch.state.dependencies=處理遊戲相依元件
launch.state.done=啟動完成
launch.state.logging_in=登入
launch.state.modpack=正在下載必要檔案
launch.state.logging_in=登入
launch.state.modpack=下載必要檔案
launch.state.waiting_launching=等待遊戲啟動
launch.wrong_javadir=Java 路徑錯誤,將自動重設為預設 Java 路徑。

View File

@@ -180,10 +180,10 @@ launch.failed.decompressing_natives=未能解压游戏本地库。
launch.failed.download_library=未能下载游戏依赖 %s.
launch.failed.executable_permission=未能为启动文件添加执行权限。
launch.failed.exited_abnormally=游戏非正常退出,请查看日志文件,或联系他人寻求帮助。
launch.state.dependencies=正在处理游戏依赖
launch.state.dependencies=处理游戏依赖
launch.state.done=启动完成
launch.state.logging_in=登录
launch.state.modpack=正在下载必要文件
launch.state.logging_in=登录
launch.state.modpack=下载必要文件
launch.state.waiting_launching=等待游戏启动
launch.wrong_javadir=错误的 Java 路径,将自动重置为默认 Java 路径。

View File

@@ -24,7 +24,10 @@ import org.jackhuang.hmcl.util.function.ExceptionalRunnable;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.*;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
/**
@@ -42,7 +45,7 @@ public final class AsyncTaskExecutor extends TaskExecutor {
@Override
public TaskExecutor start() {
taskListeners.forEach(TaskListener::onStart);
future = executeTasks(Collections.singleton(firstTask))
future = executeTasks(null, Collections.singleton(firstTask))
.thenApplyAsync(exception -> {
boolean success = exception == null;
@@ -95,7 +98,7 @@ public final class AsyncTaskExecutor extends TaskExecutor {
future.cancel(true);
}
private CompletableFuture<Exception> executeTasks(Collection<Task<?>> tasks) {
private CompletableFuture<Exception> executeTasks(Task<?> parentTask, Collection<Task<?>> tasks) {
if (tasks == null || tasks.isEmpty())
return CompletableFuture.completedFuture(null);
@@ -105,7 +108,7 @@ public final class AsyncTaskExecutor extends TaskExecutor {
return CompletableFuture.allOf(tasks.stream()
.map(task -> CompletableFuture.completedFuture(null)
.thenComposeAsync(unused2 -> executeTask(task))
.thenComposeAsync(unused2 -> executeTask(parentTask, task))
).toArray(CompletableFuture<?>[]::new));
})
.thenApplyAsync(unused -> (Exception) null)
@@ -120,11 +123,13 @@ public final class AsyncTaskExecutor extends TaskExecutor {
});
}
private CompletableFuture<?> executeTask(Task<?> task) {
private CompletableFuture<?> executeTask(Task<?> parentTask, Task<?> task) {
return CompletableFuture.completedFuture(null)
.thenComposeAsync(unused -> {
task.setCancelled(this::isCancelled);
task.setState(Task.TaskState.READY);
if (parentTask != null && task.getStage() == null)
task.setStage(parentTask.getStage());
if (task.getSignificance().shouldLog())
Logging.LOG.log(Level.FINE, "Executing task: " + task.getName());
@@ -137,7 +142,7 @@ public final class AsyncTaskExecutor extends TaskExecutor {
return CompletableFuture.completedFuture(null);
}
})
.thenComposeAsync(unused -> executeTasks(task.getDependents()))
.thenComposeAsync(unused -> executeTasks(task, task.getDependents()))
.thenComposeAsync(dependentsException -> {
boolean isDependentsSucceeded = dependentsException == null;
@@ -158,7 +163,7 @@ public final class AsyncTaskExecutor extends TaskExecutor {
rethrow(throwable);
});
})
.thenComposeAsync(unused -> executeTasks(task.getDependencies()))
.thenComposeAsync(unused -> executeTasks(task, task.getDependencies()))
.thenComposeAsync(dependenciesException -> {
boolean isDependenciesSucceeded = dependenciesException == null;

View File

@@ -23,14 +23,7 @@ import org.jackhuang.hmcl.util.function.ExceptionalRunnable;
import java.util.Collection;
import java.util.Collections;
import java.util.Objects;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
@@ -47,7 +40,7 @@ public class CancellableTaskExecutor extends TaskExecutor {
public TaskExecutor start() {
taskListeners.forEach(TaskListener::onStart);
workerQueue.add(Schedulers.schedule(scheduler, wrap(() -> {
boolean flag = executeTasks(Collections.singleton(firstTask));
boolean flag = executeTasks(null, Collections.singleton(firstTask));
taskListeners.forEach(it -> it.onStop(flag, this));
})));
return this;
@@ -58,7 +51,7 @@ public class CancellableTaskExecutor extends TaskExecutor {
taskListeners.forEach(TaskListener::onStart);
AtomicBoolean flag = new AtomicBoolean(true);
Future<?> future = Schedulers.schedule(scheduler, wrap(() -> {
flag.set(executeTasks(Collections.singleton(firstTask)));
flag.set(executeTasks(null, Collections.singleton(firstTask)));
taskListeners.forEach(it -> it.onStop(flag.get(), this));
}));
workerQueue.add(future);
@@ -82,7 +75,7 @@ public class CancellableTaskExecutor extends TaskExecutor {
}
}
private boolean executeTasks(Collection<? extends Task<?>> tasks) throws InterruptedException {
private boolean executeTasks(Task<?> parentTask, Collection<? extends Task<?>> tasks) throws InterruptedException {
if (tasks.isEmpty())
return true;
@@ -92,7 +85,7 @@ public class CancellableTaskExecutor extends TaskExecutor {
for (Task<?> task : tasks) {
if (cancelled.get())
return false;
Invoker invoker = new Invoker(task, latch, success);
Invoker invoker = new Invoker(parentTask, task, latch, success);
try {
Future<?> future = Schedulers.schedule(scheduler, invoker);
workerQueue.add(future);
@@ -112,7 +105,7 @@ public class CancellableTaskExecutor extends TaskExecutor {
return success.get() && !cancelled.get();
}
private boolean executeTask(Task<?> task) {
private boolean executeTask(Task<?> parentTask, Task<?> task) {
task.setCancelled(this::isCancelled);
if (cancelled.get()) {
@@ -122,6 +115,8 @@ public class CancellableTaskExecutor extends TaskExecutor {
}
task.setState(Task.TaskState.READY);
if (parentTask != null && task.getStage() == null)
task.setStage(parentTask.getStage());
if (task.getSignificance().shouldLog())
Logging.LOG.log(Level.FINE, "Executing task: " + task.getName());
@@ -140,8 +135,12 @@ public class CancellableTaskExecutor extends TaskExecutor {
}
Collection<? extends Task<?>> dependents = task.getDependents();
boolean doDependentsSucceeded = executeTasks(dependents);
Exception dependentsException = dependents.stream().map(Task::getException).filter(Objects::nonNull).findAny().orElse(null);
boolean doDependentsSucceeded = executeTasks(task, dependents);
Exception dependentsException = dependents.stream().map(Task::getException)
.filter(Objects::nonNull)
.filter(x -> !(x instanceof CancellationException))
.filter(x -> !(x instanceof InterruptedException))
.findAny().orElse(null);
if (!doDependentsSucceeded && task.isRelyingOnDependents() || cancelled.get()) {
task.setException(dependentsException);
throw new CancellationException();
@@ -163,8 +162,12 @@ public class CancellableTaskExecutor extends TaskExecutor {
}
Collection<? extends Task<?>> dependencies = task.getDependencies();
boolean doDependenciesSucceeded = executeTasks(dependencies);
Exception dependenciesException = dependencies.stream().map(Task::getException).filter(Objects::nonNull).findAny().orElse(null);
boolean doDependenciesSucceeded = executeTasks(task, dependencies);
Exception dependenciesException = dependencies.stream().map(Task::getException)
.filter(Objects::nonNull)
.filter(x -> !(x instanceof CancellationException))
.filter(x -> !(x instanceof InterruptedException))
.findAny().orElse(null);
if (doDependenciesSucceeded)
task.setDependenciesSucceeded();
@@ -250,11 +253,13 @@ public class CancellableTaskExecutor extends TaskExecutor {
private class Invoker implements Runnable {
private final Task<?> parentTask;
private final Task<?> task;
private final CountDownLatch latch;
private final AtomicBoolean success;
public Invoker(Task<?> task, CountDownLatch latch, AtomicBoolean success) {
public Invoker(Task<?> parentTask, Task<?> task, CountDownLatch latch, AtomicBoolean success) {
this.parentTask = parentTask;
this.task = task;
this.latch = latch;
this.success = success;
@@ -264,7 +269,7 @@ public class CancellableTaskExecutor extends TaskExecutor {
public void run() {
try {
Thread.currentThread().setName(task.getName());
if (!executeTask(task))
if (!executeTask(parentTask, task))
success.set(false);
} finally {
latch.countDown();

View File

@@ -17,13 +17,11 @@
*/
package org.jackhuang.hmcl.task;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.util.CacheRepository;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.io.ResponseCodeException;
import org.jackhuang.hmcl.util.io.*;
import java.io.File;
import java.io.IOException;
@@ -35,10 +33,8 @@ import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import static java.util.Objects.requireNonNull;
@@ -279,7 +275,7 @@ public class FileDownloadTask extends Task<Void> {
updateProgress(downloaded, contentLength);
long now = System.currentTimeMillis();
if (now - lastTime >= 1000) {
updateMessage((downloaded - lastDownloaded) / 1024 + "KB/s");
updateDownloadSpeed(downloaded - lastDownloaded);
lastDownloaded = downloaded;
lastTime = now;
}
@@ -337,4 +333,39 @@ public class FileDownloadTask extends Task<Void> {
throw new DownloadException(urls.get(0), exception);
}
private static final Timer timer = new Timer("DownloadSpeedRecorder", true);
private static final AtomicInteger downloadSpeed = new AtomicInteger(0);
public static final EventBus speedEvent = new EventBus();
static {
timer.schedule(new TimerTask() {
@Override
public void run() {
speedEvent.fireEvent(new SpeedEvent(speedEvent, downloadSpeed.getAndSet(0)));
}
}, 0, 1000);
}
private static void updateDownloadSpeed(int speed) {
downloadSpeed.addAndGet(speed);
}
public static class SpeedEvent extends Event {
private final int speed;
public SpeedEvent(Object source, int speed) {
super(source);
this.speed = speed;
}
/**
* Download speed in byte/sec.
* @return
*/
public int getSpeed() {
return speed;
}
}
}

View File

@@ -79,6 +79,25 @@ public abstract class Task<T> {
return cancelled != null ? cancelled.get() : false;
}
// stage
private String stage = null;
/**
* Stage of task implies the goal of this task, for grouping tasks.
* Stage will inherit from the parent task.
*/
public String getStage() {
return stage;
}
/**
* You must initialize stage in preExecute.
* @param stage the stage
*/
public void setStage(String stage) {
this.stage = stage;
}
// state
private TaskState state = TaskState.READY;
@@ -344,18 +363,18 @@ public abstract class Task<T> {
}
public final TaskExecutor cancellableExecutor() {
return new CancellableTaskExecutor(this);
return new AsyncTaskExecutor(this);
}
public final TaskExecutor cancellableExecutor(boolean start) {
TaskExecutor executor = new CancellableTaskExecutor(this);
TaskExecutor executor = new AsyncTaskExecutor(this);
if (start)
executor.start();
return executor;
}
public final TaskExecutor cancellableExecutor(TaskListener taskListener) {
TaskExecutor executor = new CancellableTaskExecutor(this);
TaskExecutor executor = new AsyncTaskExecutor(this);
executor.addTaskListener(taskListener);
return executor;
}
@@ -743,6 +762,12 @@ public abstract class Task<T> {
return whenComplete(executor, () -> success.accept(getResult()), failure);
}
public Task<T> withStage(String stage) {
StageTask<T> task = new StageTask<>(this);
task.setStage(stage);
return task;
}
public static Task<Void> runAsync(ExceptionalRunnable<?> closure) {
return runAsync(Schedulers.defaultScheduler(), closure);
}
@@ -760,6 +785,10 @@ public abstract class Task<T> {
}
public static <T> Task<T> composeAsync(ExceptionalSupplier<Task<T>, ?> fn) {
return composeAsync(getCaller(), fn);
}
public static <T> Task<T> composeAsync(String name, ExceptionalSupplier<Task<T>, ?> fn) {
return new Task<T>() {
Task<T> then;
@@ -774,7 +803,7 @@ public abstract class Task<T> {
public Collection<Task<?>> getDependencies() {
return then == null ? Collections.emptySet() : Collections.singleton(then);
}
};
}.setName(name);
}
public static <V> Task<V> supplyAsync(Callable<V> callable) {
@@ -960,4 +989,21 @@ public abstract class Task<T> {
return relyingOnDependents;
}
}
public static class StageTask<T> extends Task<T> {
private final Task<T> task;
StageTask(Task<T> task) {
this.task = task;
}
@Override
public Collection<Task<?>> getDependents() {
return Collections.singleton(task);
}
@Override
public void execute() throws Exception {
setResult(task.getResult());
}
}
}