diff --git a/src/main/java/link/infra/packwiz/installer/Main.java b/src/main/java/link/infra/packwiz/installer/Main.java index e65b7d4..f1fcbdb 100644 --- a/src/main/java/link/infra/packwiz/installer/Main.java +++ b/src/main/java/link/infra/packwiz/installer/Main.java @@ -3,6 +3,7 @@ import link.infra.packwiz.installer.metadata.SpaceSafeURI; import link.infra.packwiz.installer.ui.CLIHandler; import link.infra.packwiz.installer.ui.IUserInterface; +import link.infra.packwiz.installer.ui.InputStateHandler; import link.infra.packwiz.installer.ui.InstallWindow; import org.apache.commons.cli.*; @@ -10,10 +11,11 @@ import java.awt.*; import java.net.URISyntaxException; +@SuppressWarnings("unused") public class Main { // Actual main() is in RequiresBootstrap! - + @SuppressWarnings("unused") public Main(String[] args) { // Big overarching try/catch just in case everything breaks try { @@ -79,7 +81,8 @@ private void startup(String[] args) { ui.setTitle(title); } - ui.show(); + InputStateHandler inputStateHandler = new InputStateHandler(); + ui.show(inputStateHandler); UpdateManager.Options uOptions = new UpdateManager.Options(); @@ -111,7 +114,7 @@ private void startup(String[] args) { try { ui.executeManager(() -> { try { - new UpdateManager(uOptions, ui); + new UpdateManager(uOptions, ui, inputStateHandler); } catch (Exception e) { // TODO: better error message? ui.handleExceptionAndExit(e); diff --git a/src/main/java/link/infra/packwiz/installer/UpdateManager.java b/src/main/java/link/infra/packwiz/installer/UpdateManager.java index 971e116..de48ade 100644 --- a/src/main/java/link/infra/packwiz/installer/UpdateManager.java +++ b/src/main/java/link/infra/packwiz/installer/UpdateManager.java @@ -16,6 +16,7 @@ import link.infra.packwiz.installer.request.HandlerManager; import link.infra.packwiz.installer.ui.IExceptionDetails; import link.infra.packwiz.installer.ui.IUserInterface; +import link.infra.packwiz.installer.ui.InputStateHandler; import link.infra.packwiz.installer.ui.InstallProgress; import okio.Okio; import okio.Source; @@ -33,6 +34,7 @@ public class UpdateManager { public final IUserInterface ui; private boolean cancelled; private boolean cancelledStartGame = false; + private InputStateHandler stateHandler; public static class Options { SpaceSafeURI downloadURI = null; @@ -92,9 +94,10 @@ public static Side from(String name) { } } - UpdateManager(Options opts, IUserInterface ui) { + UpdateManager(Options opts, IUserInterface ui, InputStateHandler inputStateHandler) { this.opts = opts; this.ui = ui; + this.stateHandler = inputStateHandler; this.start(); } @@ -114,6 +117,11 @@ private void start() { return; } + if (stateHandler.getCancelButton()) { + showCancellationDialog(); + handleCancellation(); + } + ui.submitProgress(new InstallProgress("Loading pack file...")); GeneralHashingSource packFileSource; try { @@ -133,6 +141,11 @@ private void start() { return; } + if (stateHandler.getCancelButton()) { + showCancellationDialog(); + handleCancellation(); + } + ui.submitProgress(new InstallProgress("Checking local files...")); // Invalidation checking must be done here, as it must happen before pack/index hashes are checked @@ -162,11 +175,18 @@ private void start() { if (manifest.packFileHash != null && packFileSource.hashIsEqual(manifest.packFileHash) && invalidatedUris.isEmpty()) { System.out.println("Modpack is already up to date!"); // todo: --force? - return; + if (!stateHandler.getOptionsButton()) { + return; + } } System.out.println("Modpack name: " + pf.name); + if (stateHandler.getCancelButton()) { + showCancellationDialog(); + handleCancellation(); + } + try { // This is badly written, I'll probably heavily refactor it at some point processIndex(HandlerManager.getNewLoc(opts.downloadURI, pf.index.file), @@ -175,15 +195,7 @@ private void start() { ui.handleExceptionAndExit(e1); } - if (cancelled) { - 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; - } + handleCancellation(); // TODO: update MMC params, java args etc @@ -205,7 +217,9 @@ private void checkOptions() { private void processIndex(SpaceSafeURI indexUri, Hash indexHash, String hashFormat, ManifestFile manifest, List invalidatedUris) { if (manifest.indexFileHash != null && manifest.indexFileHash.equals(indexHash) && invalidatedUris.isEmpty()) { System.out.println("Modpack files are already up to date!"); - return; + if (!stateHandler.getOptionsButton()) { + return; + } } manifest.indexFileHash = indexHash; @@ -233,6 +247,11 @@ private void processIndex(SpaceSafeURI indexUri, Hash indexHash, String hashForm return; } + if (stateHandler.getCancelButton()) { + showCancellationDialog(); + return; + } + if (manifest.cachedFiles == null) { manifest.cachedFiles = new HashMap<>(); } @@ -269,13 +288,21 @@ private void processIndex(SpaceSafeURI indexUri, Hash indexHash, String hashForm } } } + + if (stateHandler.getCancelButton()) { + showCancellationDialog(); + return; + } ui.submitProgress(new InstallProgress("Comparing new files...")); - // TODO: progress bar, parallelify + // TODO: progress bar? List tasks = DownloadTask.createTasksFromIndex(indexFile, indexFile.hashFormat, opts.side); // If the side changes, invalidate EVERYTHING just in case // Might not be needed, but done just to be safe boolean invalidateAll = !opts.side.equals(manifest.cachedSide); + if (invalidateAll) { + System.out.println("Side changed, invalidating all mods"); + } tasks.forEach(f -> { // TODO: should linkedfile be checked as well? should this be done in the download section? if (invalidateAll) { @@ -291,7 +318,14 @@ private void processIndex(SpaceSafeURI indexUri, Hash indexHash, String hashForm // If it is null, the DownloadTask will make a new empty cachedFile 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 failedTasks = tasks.stream().filter(t -> t.getException() != null).collect(Collectors.toList()); if (failedTasks.size() > 0) { @@ -315,10 +349,15 @@ private void processIndex(SpaceSafeURI indexUri, Hash indexHash, String hashForm } } + if (stateHandler.getCancelButton()) { + showCancellationDialog(); + return; + } + List nonFailedFirstTasks = tasks.stream().filter(t -> t.getException() == null).collect(Collectors.toList()); List optionTasks = nonFailedFirstTasks.stream().filter(DownloadTask::correctSide).filter(DownloadTask::isOptional).collect(Collectors.toList()); // 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 Future cancelledResult = ui.showOptions(new ArrayList<>(optionTasks)); try { @@ -332,6 +371,7 @@ private void processIndex(SpaceSafeURI indexUri, Hash indexHash, String hashForm ui.handleExceptionAndExit(e); } } + ui.disableOptionsButton(); // TODO: different thread pool type? ExecutorService threadPool = Executors.newFixedThreadPool(10); @@ -355,7 +395,7 @@ private void processIndex(SpaceSafeURI indexUri, Hash indexHash, String hashForm if (task.getException() != null) { ManifestFile.File file = task.cachedFile.getRevert(); if (file != null) { - manifest.cachedFiles.put(task.metadata.file, file); + manifest.cachedFiles.putIfAbsent(task.metadata.file, file); } } else { // idiot, if it wasn't there in the first place it won't magically appear there @@ -376,6 +416,13 @@ private void processIndex(SpaceSafeURI indexUri, Hash indexHash, String hashForm progress = "Failed to download, unknown reason"; } 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 failedTasks2ElectricBoogaloo = nonFailedFirstTasks.stream().filter(t -> t.getException() != null).collect(Collectors.toList()); @@ -399,4 +446,34 @@ private void processIndex(SpaceSafeURI indexUri, Hash indexHash, String hashForm } } } + + 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); + } + } } diff --git a/src/main/java/link/infra/packwiz/installer/metadata/IndexFile.java b/src/main/java/link/infra/packwiz/installer/metadata/IndexFile.java index 418bc3b..0a7e50d 100644 --- a/src/main/java/link/infra/packwiz/installer/metadata/IndexFile.java +++ b/src/main/java/link/infra/packwiz/installer/metadata/IndexFile.java @@ -28,7 +28,6 @@ public static class File { public transient ModFile linkedFile; public transient SpaceSafeURI linkedFileURI; - public transient boolean optionValue = true; public void downloadMeta(IndexFile parentIndexFile, SpaceSafeURI indexUri) throws Exception { if (!metafile) { diff --git a/src/main/java/link/infra/packwiz/installer/ui/CLIHandler.java b/src/main/java/link/infra/packwiz/installer/ui/CLIHandler.java index 72f4195..71bcbd0 100644 --- a/src/main/java/link/infra/packwiz/installer/ui/CLIHandler.java +++ b/src/main/java/link/infra/packwiz/installer/ui/CLIHandler.java @@ -12,7 +12,7 @@ public void handleException(Exception e) { } @Override - public void show() {} + public void show(InputStateHandler h) {} @Override public void submitProgress(InstallProgress progress) { diff --git a/src/main/java/link/infra/packwiz/installer/ui/IUserInterface.java b/src/main/java/link/infra/packwiz/installer/ui/IUserInterface.java index b933380..104ad80 100644 --- a/src/main/java/link/infra/packwiz/installer/ui/IUserInterface.java +++ b/src/main/java/link/infra/packwiz/installer/ui/IUserInterface.java @@ -1,11 +1,12 @@ package link.infra.packwiz.installer.ui; import java.util.List; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; public interface IUserInterface { - void show(); + void show(InputStateHandler handler); void handleException(Exception e); @@ -24,5 +25,14 @@ default void setTitle(String title) {} Future showOptions(List option); Future showExceptions(List opts, int numTotal, boolean allowsIgnore); + + default void disableOptionsButton() {} + + // Should not return CONTINUE + default Future showCancellationDialog() { + CompletableFuture future = new CompletableFuture<>(); + future.complete(IExceptionDetails.ExceptionListResult.CANCEL); + return future; + } } diff --git a/src/main/java/link/infra/packwiz/installer/ui/InputStateHandler.java b/src/main/java/link/infra/packwiz/installer/ui/InputStateHandler.java new file mode 100644 index 0000000..ee63d45 --- /dev/null +++ b/src/main/java/link/infra/packwiz/installer/ui/InputStateHandler.java @@ -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; + } +} diff --git a/src/main/java/link/infra/packwiz/installer/ui/InstallWindow.java b/src/main/java/link/infra/packwiz/installer/ui/InstallWindow.java index f7fcfb1..7c70a1f 100644 --- a/src/main/java/link/infra/packwiz/installer/ui/InstallWindow.java +++ b/src/main/java/link/infra/packwiz/installer/ui/InstallWindow.java @@ -13,18 +13,21 @@ public class InstallWindow implements IUserInterface { private JFrame frmPackwizlauncher; private JLabel lblProgresslabel; private JProgressBar progressBar; + private InputStateHandler inputStateHandler; private String title = "Updating modpack..."; private SwingWorkerButWithPublicPublish worker; private AtomicBoolean aboutToCrash = new AtomicBoolean(); + private JButton btnOptions; @Override - public void show() { + public void show(InputStateHandler handler) { + this.inputStateHandler = handler; EventQueue.invokeLater(() -> { try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); - InstallWindow.this.initialize(); - InstallWindow.this.frmPackwizlauncher.setVisible(true); + initialize(); + frmPackwizlauncher.setVisible(true); } catch (Exception e) { e.printStackTrace(); } @@ -60,7 +63,12 @@ private void initialize() { GridBagLayout gbl_panel_1 = new GridBagLayout(); 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); GridBagConstraints gbc_btnOptions = new GridBagConstraints(); gbc_btnOptions.gridx = 0; @@ -68,14 +76,9 @@ private void initialize() { panel_1.add(btnOptions, gbc_btnOptions); JButton btnCancel = new JButton("Cancel"); - btnCancel.addActionListener(event -> { - if (worker != null) { - worker.cancel(true); - } - frmPackwizlauncher.dispose(); - // TODO: show window to ask user what to do - System.out.println("Update process cancelled by user!"); - System.exit(1); + btnCancel.addActionListener(e -> { + btnCancel.setEnabled(false); + inputStateHandler.pressCancelButton(); }); btnCancel.setAlignmentX(Component.CENTER_ALIGNMENT); GridBagConstraints gbc_btnCancel = new GridBagConstraints(); @@ -113,12 +116,23 @@ public void handleExceptionAndExit(Exception e) { public void setTitle(String title) { this.title = title; if (frmPackwizlauncher != null) { - EventQueue.invokeLater(() -> InstallWindow.this.frmPackwizlauncher.setTitle(title)); + EventQueue.invokeLater(() -> frmPackwizlauncher.setTitle(title)); } } @Override 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) { worker.publishPublic(progress); } @@ -190,4 +204,25 @@ public Future showExceptions(List showCancellationDialog() { + CompletableFuture 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; + } } diff --git a/src/main/java/link/infra/packwiz/installer/ui/OptionTempHandler.java b/src/main/java/link/infra/packwiz/installer/ui/OptionTempHandler.java index e6c1449..67d92e0 100644 --- a/src/main/java/link/infra/packwiz/installer/ui/OptionTempHandler.java +++ b/src/main/java/link/infra/packwiz/installer/ui/OptionTempHandler.java @@ -1,7 +1,7 @@ package link.infra.packwiz.installer.ui; // 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 boolean tempValue;