Skip to content

Commit 77e9623

Browse files
authored
Add a Save As button to Forge mode (#98)
1 parent 9869a9a commit 77e9623

File tree

6 files changed

+165
-53
lines changed

6 files changed

+165
-53
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77
## [Unreleased]
88
### Added
99
- Added full support for MultiMC installation. The MultiMC mode now creates an instance just like the Vanilla mode creates a profile.
10+
- Added a "Save As" dialog to the Forge installation mode and a matching CLI argument `--destination`. This replaces the previous "Install" button, but only for Forge mode.
1011

1112
### Changed
1213
- Reworked the way OptiFine is supported. Instead of searching for an installed OptiFine instance, the user provides us with an OptiFine installer jar.
1314
- `--minecraft-directory`'s aliases changed; `--launcher-dir` and `--launcher-directory` were added and `--mc-path` was removed.
15+
- In "Show Vanilla JSON" mode the "Install" button now says "Show JSON" instead.
1416

1517
## Removed
1618
- Minecraft Directory setting from the GUI. It is still present as a CLI option.

src/main/java/io/github/ImpactDevelopment/installer/Args.java

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ public class Args {
6464
@Parameter(names = {"--all"}, description = "Run on all Impact releases")
6565
public boolean all = false;
6666

67+
@Parameter(names = {"--out", "--destination", "--dest"}, description = "Where to output \"Save As\" style modes like \"Forge\".")
68+
public String dest;
69+
6770
@Parameter(names = {"--mc-dir", "--minecraft-dir", "--minecraft-directory", "--launcher-dir", "--launcher-directory"}, description = "Path to the Minecraft Launcher directory")
6871
public String mcPath;
6972

@@ -116,12 +119,6 @@ public Args() {
116119
}
117120

118121
public void apply(InstallationConfig config) {
119-
if (mcPath != null) {
120-
setDirectory(config, MinecraftDirectorySetting.INSTANCE, mcPath);
121-
}
122-
if (multimcPath != null) {
123-
setDirectory(config, MultiMCDirectorySetting.INSTANCE, multimcPath);
124-
}
125122
if (mode != null) {
126123
config.setSettingValue(InstallationModeSetting.INSTANCE, InstallationModeOptions.valueOf(mode.toUpperCase()));
127124
}
@@ -152,11 +149,20 @@ public void apply(InstallationConfig config) {
152149
throw new IllegalArgumentException(optifine + " is not found");
153150
}
154151
}
152+
if (mcPath != null) {
153+
setPath(config, MinecraftDirectorySetting.INSTANCE, mcPath, true);
154+
}
155+
if (multimcPath != null) {
156+
setPath(config, MultiMCDirectorySetting.INSTANCE, multimcPath, true);
157+
}
158+
if (dest != null) {
159+
setPath(config, DestinationSetting.INSTANCE, dest, false);
160+
}
155161
}
156162

157-
private void setDirectory(InstallationConfig config, Setting<Path> setting, String value) {
163+
private void setPath(InstallationConfig config, Setting<Path> setting, String value, boolean mustBeDirectory) {
158164
Path path = Paths.get(value);
159-
if (!Files.isDirectory(path)) {
165+
if (mustBeDirectory && !Files.isDirectory(path)) {
160166
throw new IllegalStateException(path + " is not a directory");
161167
}
162168
config.setSettingValue(setting, path);

src/main/java/io/github/ImpactDevelopment/installer/gui/pages/MainPage.java

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,10 @@
3535
import java.awt.event.ActionListener;
3636
import java.awt.event.KeyEvent;
3737
import java.net.URI;
38+
import java.nio.file.Files;
3839
import java.nio.file.Path;
3940
import java.nio.file.Paths;
41+
import java.util.Optional;
4042

4143
import static io.github.ImpactDevelopment.installer.target.InstallationModeOptions.MULTIMC;
4244
import static io.github.ImpactDevelopment.installer.target.InstallationModeOptions.SHOWJSON;
@@ -57,9 +59,44 @@ public MainPage(AppWindow app) {
5759
default: addOptifineSetting(app);
5860
}
5961

60-
JButton install = new JButton("Install");
62+
JButton install = new JButton(mode.getButtonText());
6163
install.addActionListener((ActionEvent) -> {
6264
try {
65+
Optional<Path> destination = Optional.ofNullable(app.config.getSettingValue(DestinationSetting.INSTANCE));
66+
if (destination.isPresent()) {
67+
Path dest = destination.get();
68+
// JFileChooser won't open in a directory unless it exists;
69+
// If destination is inside .minecraft and .minecraft exists, create the dest dir
70+
Path defaultLauncher = app.config.getSettingValue(MinecraftDirectorySetting.INSTANCE);
71+
if (Files.isDirectory(defaultLauncher) && dest.toAbsolutePath().startsWith(defaultLauncher.toAbsolutePath())) {
72+
Files.createDirectories(dest.getParent());
73+
}
74+
// Otherwise, find the nearest existing parent.
75+
// (we don't want to create random directories just to open a Save dialog)
76+
Path dir = dest;
77+
while (dir != null && !Files.isDirectory(dir)) {
78+
dir = dir.getParent();
79+
}
80+
// If we couldn't find a directory, set a default
81+
if (dir == null) {
82+
dir = OperatingSystem.getHome();
83+
}
84+
85+
JFileChooser dialog = new JFileChooser(dir.toFile());
86+
dialog.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
87+
dialog.setSelectedFile(dest.toFile());
88+
switch (dialog.showSaveDialog(app)) {
89+
case JFileChooser.APPROVE_OPTION:
90+
// save to destination
91+
app.config.setSettingValue(DestinationSetting.INSTANCE, dialog.getSelectedFile().toPath());
92+
break;
93+
case JFileChooser.ERROR_OPTION:
94+
throw new RuntimeException("Unexpected error in save dialog");
95+
case JFileChooser.CANCEL_OPTION:
96+
return;
97+
}
98+
}
99+
63100
String msg = app.config.execute();
64101

65102
// Special case if installing optifine in showJson mode
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* This file is part of Impact Installer.
3+
*
4+
* Copyright (C) 2019 ImpactDevelopment and contributors
5+
*
6+
* See the CONTRIBUTORS.md file for a list of copyright holders
7+
*
8+
* This library is free software; you can redistribute it and/or
9+
* modify it under the terms of the GNU Lesser General Public
10+
* License as published by the Free Software Foundation; version 2.1
11+
* of the License.
12+
*
13+
* This library is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16+
* Lesser General Public License for more details.
17+
*
18+
* You should have received a copy of the GNU Lesser General Public
19+
* License along with this library; if not, write to the Free Software
20+
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21+
*/
22+
23+
package io.github.ImpactDevelopment.installer.setting.settings;
24+
25+
import io.github.ImpactDevelopment.installer.impact.ImpactVersion;
26+
import io.github.ImpactDevelopment.installer.setting.InstallationConfig;
27+
import io.github.ImpactDevelopment.installer.setting.Setting;
28+
import io.github.ImpactDevelopment.installer.target.InstallationModeOptions;
29+
30+
import java.nio.file.Path;
31+
32+
public enum DestinationSetting implements Setting<Path> {
33+
INSTANCE;
34+
35+
@Override
36+
public Path getDefaultValue(InstallationConfig config) {
37+
InstallationModeOptions mode = config.getSettingValue(InstallationModeSetting.INSTANCE);
38+
switch (mode) {
39+
case FORGE:
40+
case FORGE_PLUS_LITELOADER:
41+
Path minecraft = config.getSettingValue(MinecraftDirectorySetting.INSTANCE);
42+
ImpactVersion version = config.getSettingValue(ImpactVersionSetting.INSTANCE);
43+
return minecraft.resolve("mods").resolve(version.mcVersion).resolve("Impact" + "-" + version.getCombinedVersion() + ".jar");
44+
default:
45+
return null;
46+
}
47+
}
48+
49+
@Override
50+
public boolean validSetting(InstallationConfig config, Path value) {
51+
return true;
52+
}
53+
54+
@Override
55+
public String toString() {
56+
return getClass().getSimpleName();
57+
}
58+
}

src/main/java/io/github/ImpactDevelopment/installer/target/InstallationModeOptions.java

Lines changed: 16 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -29,19 +29,23 @@
2929
import java.util.function.Function;
3030

3131
public enum InstallationModeOptions {
32-
VANILLA(Vanilla::new, true),
33-
FORGE(opt -> new Forge(opt, false), true),
34-
FORGE_PLUS_LITELOADER(opt -> new Forge(opt, true), true),
35-
VALIDATE(Validate::new, false),
36-
MULTIMC(MultiMC::new, true),
37-
SHOWJSON(ShowJSON::new, true);
32+
VANILLA("Vanilla", "Install", true, Vanilla::new),
33+
FORGE("Forge", "Save As", true, opt -> new Forge(opt, false)),
34+
FORGE_PLUS_LITELOADER("Forge + Liteloader", "Save As", true, opt -> new Forge(opt, true)),
35+
VALIDATE("Validate Vanilla version", "Validate", false, Validate::new),
36+
MULTIMC("MultiMC", "Install", true, MultiMC::new),
37+
SHOWJSON("Show Vanilla JSON", "Show JSON", true, ShowJSON::new);
3838

39-
InstallationModeOptions(Function<InstallationConfig, InstallationMode> mode, boolean showInGUI) {
39+
InstallationModeOptions(String name, String buttonText, boolean showInGUI, Function<InstallationConfig, InstallationMode> mode) {
4040
this.mode = mode;
41+
this.name = name;
42+
this.buttonText = buttonText;
4143
this.showInGUI = showInGUI;
4244
}
4345

4446
public final Function<InstallationConfig, InstallationMode> mode;
47+
private final String name;
48+
private final String buttonText;
4549
public final boolean showInGUI;
4650

4751
public boolean supports(ImpactVersion impact) {
@@ -54,25 +58,12 @@ public boolean supports(ImpactVersion impact) {
5458
}
5559
}
5660

61+
public String getButtonText() {
62+
return buttonText;
63+
}
64+
5765
@Override
5866
public String toString() {
59-
// incredibly based code
60-
// this is oof NGL :eyes:
61-
switch (this) {
62-
case VANILLA:
63-
return "Vanilla";
64-
case SHOWJSON:
65-
return "Show Vanilla JSON";
66-
case MULTIMC:
67-
return "MultiMC";
68-
case FORGE:
69-
return "Forge";
70-
case FORGE_PLUS_LITELOADER:
71-
return "Forge + Liteloader";
72-
case VALIDATE:
73-
return "Validate Vanilla version";
74-
default:
75-
return "Unknown";
76-
}
67+
return name;
7768
}
7869
}

src/main/java/io/github/ImpactDevelopment/installer/target/targets/Forge.java

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import io.github.ImpactDevelopment.installer.impact.ImpactJsonVersion;
2626
import io.github.ImpactDevelopment.installer.libraries.ILibrary;
2727
import io.github.ImpactDevelopment.installer.setting.InstallationConfig;
28+
import io.github.ImpactDevelopment.installer.setting.settings.DestinationSetting;
2829
import io.github.ImpactDevelopment.installer.setting.settings.ImpactVersionSetting;
2930
import io.github.ImpactDevelopment.installer.setting.settings.MinecraftDirectorySetting;
3031
import io.github.ImpactDevelopment.installer.target.InstallationMode;
@@ -42,8 +43,10 @@
4243
import java.security.MessageDigest;
4344
import java.security.NoSuchAlgorithmException;
4445
import java.util.HashSet;
46+
import java.util.List;
4547
import java.util.jar.JarEntry;
4648
import java.util.jar.JarOutputStream;
49+
import java.util.stream.Collectors;
4750

4851
import static java.nio.charset.StandardCharsets.UTF_8;
4952

@@ -60,22 +63,26 @@ public Forge(InstallationConfig config, boolean liteloaderSupport) {
6063
}
6164

6265
@Override
63-
public String apply() {
64-
Path out = config.getSettingValue(MinecraftDirectorySetting.INSTANCE).resolve("mods").resolve(version.mcVersion).resolve(version.name + "-" + version.version + "-" + version.mcVersion + ".jar");
66+
public String apply() throws IOException {
67+
Path out = config.getSettingValue(DestinationSetting.INSTANCE);
68+
69+
if (Files.isDirectory(out)) {
70+
out = out.resolve(version.name + "-" + version.version + "-" + version.mcVersion + ".jar");
71+
}
6572

6673
if (!Files.exists(out.getParent())) {
67-
try {
68-
Files.createDirectories(out.getParent());
69-
} catch (IOException e) {
70-
e.printStackTrace();
71-
}
74+
Files.createDirectories(out.getParent());
7275
}
7376

7477
if (liteloaderSupport) {
7578
JOptionPane.showMessageDialog(null, "This Forge jar will ONLY work with Liteloader + Forge, not with either on their own.\nIf you don't have liteloader, use the Forge option instead!\nIf you change your mind and just want Forge (no liteloader), you will need to reinstall Impact with the correct option!", "IMPORTANT", JOptionPane.INFORMATION_MESSAGE);
7679
}
7780

78-
Tracky.persist(config.getSettingValue(MinecraftDirectorySetting.INSTANCE));
81+
Path defaultLauncher = config.getSettingValue(MinecraftDirectorySetting.INSTANCE);
82+
if (Files.isDirectory(defaultLauncher)) {
83+
Tracky.persist(defaultLauncher);
84+
}
85+
7986
HashSet<String> fileNames = new HashSet<>();
8087
try (JarOutputStream jarOut = new JarOutputStream(new FileOutputStream(out.toFile()))) {
8188
for (ILibrary library : version.resolveLibraries(config)) {
@@ -122,21 +129,32 @@ public String apply() {
122129
throw new RuntimeException(e);
123130
}
124131

125-
// Remove other Impact forge jars
132+
// Look for other Impact forge jars
126133
try {
127-
Files.list(out.getParent())
128-
.filter(f -> !f.equals(out))
134+
final Path finalOut = out; //Variable used in lambda expression should be final or effectively final
135+
List<Path> conflicts = Files.list(out.getParent())
136+
.filter(f -> !f.equals(finalOut))
129137
.filter(f -> f.getFileName().toString().startsWith("Impact-"))
130138
.filter(f -> f.getFileName().toString().endsWith(".jar"))
131-
.forEach(f -> {
132-
try {
133-
JOptionPane.showMessageDialog(null, "Replacing " + f, "\uD83D\uDE0E", JOptionPane.INFORMATION_MESSAGE);
134-
Files.delete(f);
135-
} catch (IOException e) {
136-
JOptionPane.showMessageDialog(null, "Failed to remove " + f, "\uD83D\uDE0E", JOptionPane.ERROR_MESSAGE);
137-
e.printStackTrace();
139+
.collect(Collectors.toList());
140+
141+
// If we find any Impact jars, warn the user and ask to delete them
142+
if (!conflicts.isEmpty()) {
143+
List<String> names = conflicts.stream().map(Path::getFileName).map(Path::toString).collect(Collectors.toList());
144+
JOptionPane.showMessageDialog(null, "Warning: Having multiple Impact mods installed will cause errors:\n" + String.join("\n", names), "\uD83D\uDE0E", JOptionPane.WARNING_MESSAGE);
145+
146+
conflicts.forEach(conflict -> {
147+
if (JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog(null, "Would you like to remove " + conflict.getFileName() + "?", "\uD83D\uDE0E", JOptionPane.YES_NO_OPTION)) {
148+
try {
149+
Files.delete(conflict);
150+
JOptionPane.showMessageDialog(null, "Removed " + conflict, "\uD83D\uDE0E", JOptionPane.INFORMATION_MESSAGE);
151+
} catch (IOException e) {
152+
JOptionPane.showMessageDialog(null, "Failed to remove " + conflict.getFileName() + ":\n" + e.getMessage(), "\uD83D\uDE0E", JOptionPane.ERROR_MESSAGE);
153+
e.printStackTrace();
154+
}
138155
}
139-
});
156+
});
157+
}
140158
} catch (IOException e) {
141159
JOptionPane.showMessageDialog(null, "Error while checking for older Impact Forge installations: " + e.getLocalizedMessage(), "\uD83D\uDE0E", JOptionPane.ERROR_MESSAGE);
142160
e.printStackTrace();

0 commit comments

Comments
 (0)