Implement optional mods and cancel buttons

This commit is contained in:
comp500 2019-08-11 17:49:56 +01:00
parent 5a54a90f59
commit 7946377159
8 changed files with 182 additions and 36 deletions

View File

@ -3,6 +3,7 @@ package link.infra.packwiz.installer;
import link.infra.packwiz.installer.metadata.SpaceSafeURI; import link.infra.packwiz.installer.metadata.SpaceSafeURI;
import link.infra.packwiz.installer.ui.CLIHandler; import link.infra.packwiz.installer.ui.CLIHandler;
import link.infra.packwiz.installer.ui.IUserInterface; import link.infra.packwiz.installer.ui.IUserInterface;
import link.infra.packwiz.installer.ui.InputStateHandler;
import link.infra.packwiz.installer.ui.InstallWindow; import link.infra.packwiz.installer.ui.InstallWindow;
import org.apache.commons.cli.*; import org.apache.commons.cli.*;
@ -10,10 +11,11 @@ import javax.swing.*;
import java.awt.*; import java.awt.*;
import java.net.URISyntaxException; import java.net.URISyntaxException;
@SuppressWarnings("unused")
public class Main { public class Main {
// Actual main() is in RequiresBootstrap! // Actual main() is in RequiresBootstrap!
@SuppressWarnings("unused")
public Main(String[] args) { public Main(String[] args) {
// Big overarching try/catch just in case everything breaks // Big overarching try/catch just in case everything breaks
try { try {
@ -79,7 +81,8 @@ public class Main {
ui.setTitle(title); ui.setTitle(title);
} }
ui.show(); InputStateHandler inputStateHandler = new InputStateHandler();
ui.show(inputStateHandler);
UpdateManager.Options uOptions = new UpdateManager.Options(); UpdateManager.Options uOptions = new UpdateManager.Options();
@ -111,7 +114,7 @@ public class Main {
try { try {
ui.executeManager(() -> { ui.executeManager(() -> {
try { try {
new UpdateManager(uOptions, ui); new UpdateManager(uOptions, ui, inputStateHandler);
} catch (Exception e) { } catch (Exception e) {
// TODO: better error message? // TODO: better error message?
ui.handleExceptionAndExit(e); ui.handleExceptionAndExit(e);

View File

@ -16,6 +16,7 @@ import link.infra.packwiz.installer.metadata.hash.HashUtils;
import link.infra.packwiz.installer.request.HandlerManager; import link.infra.packwiz.installer.request.HandlerManager;
import link.infra.packwiz.installer.ui.IExceptionDetails; import link.infra.packwiz.installer.ui.IExceptionDetails;
import link.infra.packwiz.installer.ui.IUserInterface; import link.infra.packwiz.installer.ui.IUserInterface;
import link.infra.packwiz.installer.ui.InputStateHandler;
import link.infra.packwiz.installer.ui.InstallProgress; import link.infra.packwiz.installer.ui.InstallProgress;
import okio.Okio; import okio.Okio;
import okio.Source; import okio.Source;
@ -33,6 +34,7 @@ public class UpdateManager {
public final IUserInterface ui; public final IUserInterface ui;
private boolean cancelled; private boolean cancelled;
private boolean cancelledStartGame = false; private boolean cancelledStartGame = false;
private InputStateHandler stateHandler;
public static class Options { public static class Options {
SpaceSafeURI downloadURI = null; SpaceSafeURI downloadURI = null;
@ -92,9 +94,10 @@ public class UpdateManager {
} }
} }
UpdateManager(Options opts, IUserInterface ui) { UpdateManager(Options opts, IUserInterface ui, InputStateHandler inputStateHandler) {
this.opts = opts; this.opts = opts;
this.ui = ui; this.ui = ui;
this.stateHandler = inputStateHandler;
this.start(); this.start();
} }
@ -114,6 +117,11 @@ public class UpdateManager {
return; return;
} }
if (stateHandler.getCancelButton()) {
showCancellationDialog();
handleCancellation();
}
ui.submitProgress(new InstallProgress("Loading pack file...")); ui.submitProgress(new InstallProgress("Loading pack file..."));
GeneralHashingSource packFileSource; GeneralHashingSource packFileSource;
try { try {
@ -133,6 +141,11 @@ public class UpdateManager {
return; return;
} }
if (stateHandler.getCancelButton()) {
showCancellationDialog();
handleCancellation();
}
ui.submitProgress(new InstallProgress("Checking local files...")); ui.submitProgress(new InstallProgress("Checking local files..."));
// Invalidation checking must be done here, as it must happen before pack/index hashes are checked // Invalidation checking must be done here, as it must happen before pack/index hashes are checked
@ -162,11 +175,18 @@ public class UpdateManager {
if (manifest.packFileHash != null && packFileSource.hashIsEqual(manifest.packFileHash) && invalidatedUris.isEmpty()) { if (manifest.packFileHash != null && packFileSource.hashIsEqual(manifest.packFileHash) && invalidatedUris.isEmpty()) {
System.out.println("Modpack is already up to date!"); System.out.println("Modpack is already up to date!");
// todo: --force? // todo: --force?
if (!stateHandler.getOptionsButton()) {
return; return;
} }
}
System.out.println("Modpack name: " + pf.name); System.out.println("Modpack name: " + pf.name);
if (stateHandler.getCancelButton()) {
showCancellationDialog();
handleCancellation();
}
try { try {
// This is badly written, I'll probably heavily refactor it at some point // This is badly written, I'll probably heavily refactor it at some point
processIndex(HandlerManager.getNewLoc(opts.downloadURI, pf.index.file), processIndex(HandlerManager.getNewLoc(opts.downloadURI, pf.index.file),
@ -175,15 +195,7 @@ public class UpdateManager {
ui.handleExceptionAndExit(e1); ui.handleExceptionAndExit(e1);
} }
if (cancelled) { handleCancellation();
System.out.println("Update cancelled by user!");
System.exit(1);
return;
} else if (cancelledStartGame) {
System.out.println("Update cancelled by user! Continuing to start game...");
System.exit(0);
return;
}
// TODO: update MMC params, java args etc // TODO: update MMC params, java args etc
@ -205,8 +217,10 @@ public class UpdateManager {
private void processIndex(SpaceSafeURI indexUri, Hash indexHash, String hashFormat, ManifestFile manifest, List<SpaceSafeURI> invalidatedUris) { private void processIndex(SpaceSafeURI indexUri, Hash indexHash, String hashFormat, ManifestFile manifest, List<SpaceSafeURI> invalidatedUris) {
if (manifest.indexFileHash != null && manifest.indexFileHash.equals(indexHash) && invalidatedUris.isEmpty()) { if (manifest.indexFileHash != null && manifest.indexFileHash.equals(indexHash) && invalidatedUris.isEmpty()) {
System.out.println("Modpack files are already up to date!"); System.out.println("Modpack files are already up to date!");
if (!stateHandler.getOptionsButton()) {
return; return;
} }
}
manifest.indexFileHash = indexHash; manifest.indexFileHash = indexHash;
GeneralHashingSource indexFileSource; GeneralHashingSource indexFileSource;
@ -233,6 +247,11 @@ public class UpdateManager {
return; return;
} }
if (stateHandler.getCancelButton()) {
showCancellationDialog();
return;
}
if (manifest.cachedFiles == null) { if (manifest.cachedFiles == null) {
manifest.cachedFiles = new HashMap<>(); manifest.cachedFiles = new HashMap<>();
} }
@ -269,13 +288,21 @@ public class UpdateManager {
} }
} }
} }
if (stateHandler.getCancelButton()) {
showCancellationDialog();
return;
}
ui.submitProgress(new InstallProgress("Comparing new files...")); ui.submitProgress(new InstallProgress("Comparing new files..."));
// TODO: progress bar, parallelify // TODO: progress bar?
List<DownloadTask> tasks = DownloadTask.createTasksFromIndex(indexFile, indexFile.hashFormat, opts.side); List<DownloadTask> tasks = DownloadTask.createTasksFromIndex(indexFile, indexFile.hashFormat, opts.side);
// If the side changes, invalidate EVERYTHING just in case // If the side changes, invalidate EVERYTHING just in case
// Might not be needed, but done just to be safe // Might not be needed, but done just to be safe
boolean invalidateAll = !opts.side.equals(manifest.cachedSide); boolean invalidateAll = !opts.side.equals(manifest.cachedSide);
if (invalidateAll) {
System.out.println("Side changed, invalidating all mods");
}
tasks.forEach(f -> { tasks.forEach(f -> {
// TODO: should linkedfile be checked as well? should this be done in the download section? // TODO: should linkedfile be checked as well? should this be done in the download section?
if (invalidateAll) { if (invalidateAll) {
@ -291,7 +318,14 @@ public class UpdateManager {
// If it is null, the DownloadTask will make a new empty cachedFile // If it is null, the DownloadTask will make a new empty cachedFile
f.updateFromCache(file); f.updateFromCache(file);
}); });
tasks.forEach(f -> f.downloadMetadata(indexFile, indexUri));
if (stateHandler.getCancelButton()) {
showCancellationDialog();
return;
}
// Let's hope downloadMetadata is a pure function!!!
tasks.parallelStream().forEach(f -> f.downloadMetadata(indexFile, indexUri));
List<IExceptionDetails> failedTasks = tasks.stream().filter(t -> t.getException() != null).collect(Collectors.toList()); List<IExceptionDetails> failedTasks = tasks.stream().filter(t -> t.getException() != null).collect(Collectors.toList());
if (failedTasks.size() > 0) { if (failedTasks.size() > 0) {
@ -315,10 +349,15 @@ public class UpdateManager {
} }
} }
if (stateHandler.getCancelButton()) {
showCancellationDialog();
return;
}
List<DownloadTask> nonFailedFirstTasks = tasks.stream().filter(t -> t.getException() == null).collect(Collectors.toList()); List<DownloadTask> nonFailedFirstTasks = tasks.stream().filter(t -> t.getException() == null).collect(Collectors.toList());
List<DownloadTask> optionTasks = nonFailedFirstTasks.stream().filter(DownloadTask::correctSide).filter(DownloadTask::isOptional).collect(Collectors.toList()); List<DownloadTask> optionTasks = nonFailedFirstTasks.stream().filter(DownloadTask::correctSide).filter(DownloadTask::isOptional).collect(Collectors.toList());
// If options changed, present all options again // If options changed, present all options again
if (optionTasks.stream().anyMatch(DownloadTask::isNewOptional)) { if (stateHandler.getOptionsButton() || optionTasks.stream().anyMatch(DownloadTask::isNewOptional)) {
// new ArrayList is requires so it's an IOptionDetails rather than a DownloadTask list // new ArrayList is requires so it's an IOptionDetails rather than a DownloadTask list
Future<Boolean> cancelledResult = ui.showOptions(new ArrayList<>(optionTasks)); Future<Boolean> cancelledResult = ui.showOptions(new ArrayList<>(optionTasks));
try { try {
@ -332,6 +371,7 @@ public class UpdateManager {
ui.handleExceptionAndExit(e); ui.handleExceptionAndExit(e);
} }
} }
ui.disableOptionsButton();
// TODO: different thread pool type? // TODO: different thread pool type?
ExecutorService threadPool = Executors.newFixedThreadPool(10); ExecutorService threadPool = Executors.newFixedThreadPool(10);
@ -355,7 +395,7 @@ public class UpdateManager {
if (task.getException() != null) { if (task.getException() != null) {
ManifestFile.File file = task.cachedFile.getRevert(); ManifestFile.File file = task.cachedFile.getRevert();
if (file != null) { if (file != null) {
manifest.cachedFiles.put(task.metadata.file, file); manifest.cachedFiles.putIfAbsent(task.metadata.file, file);
} }
} else { } else {
// idiot, if it wasn't there in the first place it won't magically appear there // idiot, if it wasn't there in the first place it won't magically appear there
@ -376,6 +416,13 @@ public class UpdateManager {
progress = "Failed to download, unknown reason"; progress = "Failed to download, unknown reason";
} }
ui.submitProgress(new InstallProgress(progress, i + 1, tasks.size())); ui.submitProgress(new InstallProgress(progress, i + 1, tasks.size()));
if (stateHandler.getCancelButton()) {
// Stop all tasks, don't launch the game (it's in an invalid state!)
threadPool.shutdown();
cancelled = true;
return;
}
} }
List<IExceptionDetails> failedTasks2ElectricBoogaloo = nonFailedFirstTasks.stream().filter(t -> t.getException() != null).collect(Collectors.toList()); List<IExceptionDetails> failedTasks2ElectricBoogaloo = nonFailedFirstTasks.stream().filter(t -> t.getException() != null).collect(Collectors.toList());
@ -399,4 +446,34 @@ public class UpdateManager {
} }
} }
} }
private void showCancellationDialog() {
IExceptionDetails.ExceptionListResult exceptionListResult;
try {
exceptionListResult = ui.showCancellationDialog().get();
} catch (InterruptedException | ExecutionException e) {
// Interrupted means cancelled???
ui.handleExceptionAndExit(e);
return;
}
switch (exceptionListResult) {
case CONTINUE:
throw new RuntimeException("Continuation not allowed here!");
case CANCEL:
cancelled = true;
return;
case IGNORE:
cancelledStartGame = true;
}
}
private void handleCancellation() {
if (cancelled) {
System.out.println("Update cancelled by user!");
System.exit(1);
} else if (cancelledStartGame) {
System.out.println("Update cancelled by user! Continuing to start game...");
System.exit(0);
}
}
} }

View File

@ -28,7 +28,6 @@ public class IndexFile {
public transient ModFile linkedFile; public transient ModFile linkedFile;
public transient SpaceSafeURI linkedFileURI; public transient SpaceSafeURI linkedFileURI;
public transient boolean optionValue = true;
public void downloadMeta(IndexFile parentIndexFile, SpaceSafeURI indexUri) throws Exception { public void downloadMeta(IndexFile parentIndexFile, SpaceSafeURI indexUri) throws Exception {
if (!metafile) { if (!metafile) {

View File

@ -12,7 +12,7 @@ public class CLIHandler implements IUserInterface {
} }
@Override @Override
public void show() {} public void show(InputStateHandler h) {}
@Override @Override
public void submitProgress(InstallProgress progress) { public void submitProgress(InstallProgress progress) {

View File

@ -1,11 +1,12 @@
package link.infra.packwiz.installer.ui; package link.infra.packwiz.installer.ui;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future; import java.util.concurrent.Future;
public interface IUserInterface { public interface IUserInterface {
void show(); void show(InputStateHandler handler);
void handleException(Exception e); void handleException(Exception e);
@ -25,4 +26,13 @@ public interface IUserInterface {
Future<IExceptionDetails.ExceptionListResult> showExceptions(List<IExceptionDetails> opts, int numTotal, boolean allowsIgnore); Future<IExceptionDetails.ExceptionListResult> showExceptions(List<IExceptionDetails> opts, int numTotal, boolean allowsIgnore);
default void disableOptionsButton() {}
// Should not return CONTINUE
default Future<IExceptionDetails.ExceptionListResult> showCancellationDialog() {
CompletableFuture<IExceptionDetails.ExceptionListResult> future = new CompletableFuture<>();
future.complete(IExceptionDetails.ExceptionListResult.CANCEL);
return future;
}
} }

View File

@ -0,0 +1,22 @@
package link.infra.packwiz.installer.ui;
public class InputStateHandler {
private boolean optionsButtonPressed = false;
private boolean cancelButtonPressed = false;
synchronized void pressCancelButton() {
this.cancelButtonPressed = true;
}
synchronized void pressOptionsButton() {
this.optionsButtonPressed = true;
}
public synchronized boolean getCancelButton() {
return cancelButtonPressed;
}
public synchronized boolean getOptionsButton() {
return optionsButtonPressed;
}
}

View File

@ -13,18 +13,21 @@ public class InstallWindow implements IUserInterface {
private JFrame frmPackwizlauncher; private JFrame frmPackwizlauncher;
private JLabel lblProgresslabel; private JLabel lblProgresslabel;
private JProgressBar progressBar; private JProgressBar progressBar;
private InputStateHandler inputStateHandler;
private String title = "Updating modpack..."; private String title = "Updating modpack...";
private SwingWorkerButWithPublicPublish<Void, InstallProgress> worker; private SwingWorkerButWithPublicPublish<Void, InstallProgress> worker;
private AtomicBoolean aboutToCrash = new AtomicBoolean(); private AtomicBoolean aboutToCrash = new AtomicBoolean();
private JButton btnOptions;
@Override @Override
public void show() { public void show(InputStateHandler handler) {
this.inputStateHandler = handler;
EventQueue.invokeLater(() -> { EventQueue.invokeLater(() -> {
try { try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
InstallWindow.this.initialize(); initialize();
InstallWindow.this.frmPackwizlauncher.setVisible(true); frmPackwizlauncher.setVisible(true);
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} }
@ -60,7 +63,12 @@ public class InstallWindow implements IUserInterface {
GridBagLayout gbl_panel_1 = new GridBagLayout(); GridBagLayout gbl_panel_1 = new GridBagLayout();
panel_1.setLayout(gbl_panel_1); panel_1.setLayout(gbl_panel_1);
JButton btnOptions = new JButton("Configure..."); btnOptions = new JButton("Optional mods...");
btnOptions.addActionListener(e -> {
btnOptions.setText("Loading...");
btnOptions.setEnabled(false);
inputStateHandler.pressOptionsButton();
});
btnOptions.setAlignmentX(Component.CENTER_ALIGNMENT); btnOptions.setAlignmentX(Component.CENTER_ALIGNMENT);
GridBagConstraints gbc_btnOptions = new GridBagConstraints(); GridBagConstraints gbc_btnOptions = new GridBagConstraints();
gbc_btnOptions.gridx = 0; gbc_btnOptions.gridx = 0;
@ -68,14 +76,9 @@ public class InstallWindow implements IUserInterface {
panel_1.add(btnOptions, gbc_btnOptions); panel_1.add(btnOptions, gbc_btnOptions);
JButton btnCancel = new JButton("Cancel"); JButton btnCancel = new JButton("Cancel");
btnCancel.addActionListener(event -> { btnCancel.addActionListener(e -> {
if (worker != null) { btnCancel.setEnabled(false);
worker.cancel(true); inputStateHandler.pressCancelButton();
}
frmPackwizlauncher.dispose();
// TODO: show window to ask user what to do
System.out.println("Update process cancelled by user!");
System.exit(1);
}); });
btnCancel.setAlignmentX(Component.CENTER_ALIGNMENT); btnCancel.setAlignmentX(Component.CENTER_ALIGNMENT);
GridBagConstraints gbc_btnCancel = new GridBagConstraints(); GridBagConstraints gbc_btnCancel = new GridBagConstraints();
@ -113,12 +116,23 @@ public class InstallWindow implements IUserInterface {
public void setTitle(String title) { public void setTitle(String title) {
this.title = title; this.title = title;
if (frmPackwizlauncher != null) { if (frmPackwizlauncher != null) {
EventQueue.invokeLater(() -> InstallWindow.this.frmPackwizlauncher.setTitle(title)); EventQueue.invokeLater(() -> frmPackwizlauncher.setTitle(title));
} }
} }
@Override @Override
public void submitProgress(InstallProgress progress) { public void submitProgress(InstallProgress progress) {
StringBuilder sb = new StringBuilder();
if (progress.hasProgress) {
sb.append('(');
sb.append(progress.progress);
sb.append('/');
sb.append(progress.progressTotal);
sb.append(") ");
}
sb.append(progress.message);
// TODO: better logging library?
System.out.println(sb.toString());
if (worker != null) { if (worker != null) {
worker.publishPublic(progress); worker.publishPublic(progress);
} }
@ -190,4 +204,25 @@ public class InstallWindow implements IUserInterface {
return future; return future;
} }
@Override
public void disableOptionsButton() {
if (btnOptions != null) {
btnOptions.setText("Optional mods...");
btnOptions.setEnabled(false);
}
}
@Override
public Future<IExceptionDetails.ExceptionListResult> showCancellationDialog() {
CompletableFuture<IExceptionDetails.ExceptionListResult> future = new CompletableFuture<>();
EventQueue.invokeLater(() -> {
Object[] buttons = {"Quit", "Ignore"};
int result = JOptionPane.showOptionDialog(frmPackwizlauncher,
"The installation was cancelled. Would you like to quit the game, or ignore the update and start the game?",
"Cancelled installation",
JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null, buttons, buttons[0]);
future.complete(result == 0 ? IExceptionDetails.ExceptionListResult.CANCEL : IExceptionDetails.ExceptionListResult.IGNORE);
});
return future;
}
} }

View File

@ -1,7 +1,7 @@
package link.infra.packwiz.installer.ui; package link.infra.packwiz.installer.ui;
// Serves as a proxy for IOptionDetails, so that setOptionValue isn't called until OK is clicked // Serves as a proxy for IOptionDetails, so that setOptionValue isn't called until OK is clicked
public class OptionTempHandler implements IOptionDetails { class OptionTempHandler implements IOptionDetails {
private final IOptionDetails opt; private final IOptionDetails opt;
private boolean tempValue; private boolean tempValue;