diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..77b124592 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +liberapay: offbeatwitch diff --git a/build.gradle b/build.gradle index e19c983ac..ef0c52bca 100644 --- a/build.gradle +++ b/build.gradle @@ -27,7 +27,7 @@ subprojects { apply plugin: 'maven' group = 'com.skcraft' - version = '4.5-SNAPSHOT' + version = '4.6-SNAPSHOT' sourceCompatibility = 1.8 targetCompatibility = 1.8 diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/controller/PackManagerController.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/controller/PackManagerController.java index f678c01d3..63f644098 100644 --- a/creator-tools/src/main/java/com/skcraft/launcher/creator/controller/PackManagerController.java +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/controller/PackManagerController.java @@ -6,14 +6,17 @@ package com.skcraft.launcher.creator.controller; -import com.google.common.base.Optional; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.skcraft.concurrency.Deferred; import com.skcraft.concurrency.Deferreds; import com.skcraft.concurrency.SettableProgress; +import com.skcraft.launcher.Instance; import com.skcraft.launcher.InstanceList; import com.skcraft.launcher.Launcher; import com.skcraft.launcher.auth.OfflineSession; @@ -22,6 +25,7 @@ import com.skcraft.launcher.builder.FnPatternList; import com.skcraft.launcher.creator.Creator; import com.skcraft.launcher.creator.controller.task.*; +import com.skcraft.launcher.creator.dialog.AboutDialog; import com.skcraft.launcher.creator.dialog.*; import com.skcraft.launcher.creator.dialog.BuildDialog.BuildOptions; import com.skcraft.launcher.creator.dialog.DeployServerDialog.DeployOptions; @@ -30,10 +34,7 @@ import com.skcraft.launcher.creator.server.TestServer; import com.skcraft.launcher.creator.server.TestServerBuilder; import com.skcraft.launcher.creator.swing.PackDirectoryFilter; -import com.skcraft.launcher.dialog.AccountSelectDialog; -import com.skcraft.launcher.dialog.ConfigurationDialog; -import com.skcraft.launcher.dialog.ConsoleFrame; -import com.skcraft.launcher.dialog.ProgressDialog; +import com.skcraft.launcher.dialog.*; import com.skcraft.launcher.model.modpack.LaunchModifier; import com.skcraft.launcher.persistence.Persistence; import com.skcraft.launcher.swing.PopupMouseAdapter; @@ -55,6 +56,7 @@ import java.util.Calendar; import java.util.Date; import java.util.List; +import java.util.Optional; import java.util.regex.Pattern; public class PackManagerController { @@ -177,10 +179,10 @@ public Optional getPackFromIndex(int selectedIndex, boolean requireLoaded) if (selectedIndex >= 0) { Pack pack = workspace.getPacks().get(selectedIndex); if (pack != null && (!requireLoaded || checkPackLoaded(pack))) { - return Optional.fromNullable(pack); + return Optional.of(pack); } } - return Optional.absent(); + return Optional.empty(); } public Optional getSelectedPack(boolean requireLoaded) { @@ -190,12 +192,12 @@ public Optional getSelectedPack(boolean requireLoaded) { selectedIndex = table.convertRowIndexToModel(selectedIndex); Pack pack = workspace.getPacks().get(selectedIndex); if (pack != null && (!requireLoaded || checkPackLoaded(pack))) { - return Optional.fromNullable(pack); + return Optional.of(pack); } } SwingHelper.showErrorDialog(frame, "Please select a modpack from the list.", "Error"); - return Optional.absent(); + return Optional.empty(); } public boolean writeWorkspace() { @@ -456,11 +458,9 @@ protected void showPopup(MouseEvent e) { }); frame.getOpenFolderMenuItem().addActionListener(e -> { - Optional optional = getSelectedPack(true); + Optional selectedPack = getSelectedPack(true); - if (optional.isPresent()) { - SwingHelper.browseDir(optional.get().getDirectory(), frame); - } + selectedPack.ifPresent(pack -> SwingHelper.browseDir(pack.getDirectory(), frame)); }); frame.getCheckProblemsMenuItem().addActionListener(e -> { @@ -498,6 +498,45 @@ protected void showPopup(MouseEvent e) { configDialog.setVisible(true); }); + frame.getInstanceOptionsMenuItem().addActionListener(e -> { + Optional selectedPack = getSelectedPack(true); + + selectedPack.ifPresent(pack -> { + InstanceList.Enumerator instanceList = launcher.getInstances().createEnumerator(); + + ListenableFuture future = executor.submit(instanceList); + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(InstanceList result) { + Instance found = null; + + for (Instance instance : result.getInstances()) { + if (instance.getName().equals(pack.getCachedConfig().getName())) { + found = instance; + break; + } + } + + if (found == null) { + SwingHelper.showErrorDialog(frame, "No instance found for that pack - you need " + + "to test the pack first.", "Not Found"); + return; + } + + InstanceSettingsDialog.open(frame, found); + } + + @Override + public void onFailure(Throwable ignored) { + } + }, SwingExecutor.INSTANCE); + + ProgressDialog.showProgress(frame, future, instanceList, "Enumerating instances...", + "Enumerating instances..."); + SwingHelper.addErrorDialogCallback(frame, future); + }); + }); + frame.getClearInstanceMenuItem().addActionListener(e -> { DirectoryDeleter deleter = new DirectoryDeleter(launcher.getInstancesDir()); Deferred deferred = Deferreds.makeDeferred(executor.submit(deleter), executor); @@ -629,6 +668,10 @@ private void popupPackMenu(Component component, int x, int y, Pack pack) { menuItem.addActionListener(e -> frame.getTestOnlineMenuItem().doClick()); popup.add(menuItem); + menuItem = new JMenuItem("Instance settings..."); + menuItem.addActionListener(e -> frame.getInstanceOptionsMenuItem().doClick()); + popup.add(menuItem); + menuItem = new JMenuItem("Build..."); menuItem.addActionListener(e -> frame.getBuildMenuItem().doClick()); popup.add(menuItem); diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/dialog/PackManagerFrame.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/dialog/PackManagerFrame.java index e8589c714..4ad17988b 100644 --- a/creator-tools/src/main/java/com/skcraft/launcher/creator/dialog/PackManagerFrame.java +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/dialog/PackManagerFrame.java @@ -43,6 +43,7 @@ public class PackManagerFrame extends JFrame { @Getter private final JMenuItem testMenuItem = new JMenuItem("Test"); @Getter private final JMenuItem testOnlineMenuItem = new JMenuItem("Test Online"); @Getter private final JMenuItem optionsMenuItem = new JMenuItem("Test Launcher Options..."); + @Getter private final JMenuItem instanceOptionsMenuItem = new JMenuItem("Test Instance Options..."); @Getter private final JMenuItem clearInstanceMenuItem = new JMenuItem("Delete Test Launcher Instances"); @Getter private final JMenuItem clearWebRootMenuItem = new JMenuItem("Empty Test Web Server"); @Getter private final JMenuItem buildMenuItem = new JMenuItem("Build Pack..."); @@ -163,6 +164,7 @@ private void initMenu() { menu.add(testOnlineMenuItem); menu.addSeparator(); menu.add(optionsMenuItem); + menu.add(instanceOptionsMenuItem); menu.addSeparator(); menu.add(clearInstanceMenuItem); menu.add(clearWebRootMenuItem); diff --git a/launcher-builder/src/main/java/com/skcraft/launcher/builder/loaders/ModernForgeLoaderProcessor.java b/launcher-builder/src/main/java/com/skcraft/launcher/builder/loaders/ModernForgeLoaderProcessor.java index cd8648b02..b21b63f61 100644 --- a/launcher-builder/src/main/java/com/skcraft/launcher/builder/loaders/ModernForgeLoaderProcessor.java +++ b/launcher-builder/src/main/java/com/skcraft/launcher/builder/loaders/ModernForgeLoaderProcessor.java @@ -18,10 +18,7 @@ import com.skcraft.launcher.util.FileUtils; import lombok.extern.java.Log; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; +import java.io.*; import java.net.URL; import java.util.List; import java.util.jar.JarFile; @@ -61,6 +58,12 @@ public LoaderResult process(File loaderJar, Manifest manifest, ObjectMapper mapp version.getArguments().getGameArguments().addAll(gameArguments); } + // Copy JVM arguments + List jvmArguments = info.getArguments().getJvmArguments(); + if (jvmArguments != null) { + version.getArguments().getJvmArguments().addAll(jvmArguments); + } + // Add libraries List libraries = info.getLibraries(); if (libraries != null) { @@ -101,11 +104,13 @@ public LoaderResult process(File loaderJar, Manifest manifest, ObjectMapper mapp // Extract the data files List extraFiles = Lists.newArrayList(); + File objectsDir = new File(baseDir, manifest.getObjectsLocation()); + ZipEntry clientBinpatch = BuilderUtils.getZipEntry(jarFile, "data/client.lzma"); if (clientBinpatch != null) { DownloadableFile entry = FileUtils.saveStreamToObjectsDir( closer.register(jarFile.getInputStream(clientBinpatch)), - new File(baseDir, manifest.getObjectsLocation())); + objectsDir); entry.setName("client.lzma"); entry.setSide(Side.CLIENT); @@ -117,7 +122,7 @@ public LoaderResult process(File loaderJar, Manifest manifest, ObjectMapper mapp if (serverBinpatch != null) { DownloadableFile entry = FileUtils.saveStreamToObjectsDir( closer.register(jarFile.getInputStream(serverBinpatch)), - new File(baseDir, manifest.getObjectsLocation())); + objectsDir); entry.setName("server.lzma"); entry.setSide(Side.SERVER); @@ -125,6 +130,20 @@ public LoaderResult process(File loaderJar, Manifest manifest, ObjectMapper mapp profile.getData().get("BINPATCH").setServer("&" + entry.getName() + "&"); } + // Forge install profile spec version 1 and above. + if (profile.getSpec() >= 1) { + // Add the installer itself to the extra files. + // This is for a server-only task like above, but hey. + DownloadableFile entry = FileUtils.saveStreamToObjectsDir( + closer.register(new FileInputStream(loaderJar)), objectsDir); + + entry.setName(loaderJar.getName()); + entry.setSide(Side.SERVER); + extraFiles.add(entry); + + profile.getData().put("INSTALLER", SidedData.of("&" + entry.getName() + "&")); + } + // Add extra sided data profile.getData().put("SIDE", SidedData.create("client", "server")); diff --git a/launcher-builder/src/main/java/com/skcraft/launcher/builder/loaders/OldForgeLoaderProcessor.java b/launcher-builder/src/main/java/com/skcraft/launcher/builder/loaders/OldForgeLoaderProcessor.java index 8daa555a7..c5e970d5e 100644 --- a/launcher-builder/src/main/java/com/skcraft/launcher/builder/loaders/OldForgeLoaderProcessor.java +++ b/launcher-builder/src/main/java/com/skcraft/launcher/builder/loaders/OldForgeLoaderProcessor.java @@ -86,7 +86,7 @@ public LoaderResult process(File loaderJar, Manifest manifest, ObjectMapper mapp ZipEntry libraryEntry = BuilderUtils.getZipEntry(jarFile, filePath); if (libraryEntry != null) { - File librariesDir = new File(baseDir, manifest.getLibrariesLocation()); + File librariesDir = new File(baseDir, "libraries"); File extractPath = new File(librariesDir, Library.mavenNameToPath(libraryPath)); Files.createParentDirs(extractPath); diff --git a/launcher/src/main/java/com/skcraft/launcher/Configuration.java b/launcher/src/main/java/com/skcraft/launcher/Configuration.java index 50d996f90..934a69be2 100644 --- a/launcher/src/main/java/com/skcraft/launcher/Configuration.java +++ b/launcher/src/main/java/com/skcraft/launcher/Configuration.java @@ -7,6 +7,8 @@ package com.skcraft.launcher; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.skcraft.launcher.launch.JavaRuntime; +import com.skcraft.launcher.launch.JavaRuntimeFinder; import lombok.Data; /** @@ -22,7 +24,7 @@ public class Configuration { private boolean offlineEnabled = false; - private String jvmPath; + private JavaRuntime javaRuntime; private String jvmArgs; private int minMemory = 1024; private int maxMemory = 0; // Updated in Launcher @@ -55,4 +57,13 @@ public int hashCode() { public void setWidowHeight(int height) { this.windowHeight = height; } + + /** + * Backwards compatibility for old configs with jvmPaths + */ + public void setJvmPath(String jvmPath) { + if (jvmPath != null) { + this.javaRuntime = JavaRuntimeFinder.getRuntimeFromPath(jvmPath); + } + } } diff --git a/launcher/src/main/java/com/skcraft/launcher/Instance.java b/launcher/src/main/java/com/skcraft/launcher/Instance.java index 3492e6208..dd63900fe 100644 --- a/launcher/src/main/java/com/skcraft/launcher/Instance.java +++ b/launcher/src/main/java/com/skcraft/launcher/Instance.java @@ -33,6 +33,7 @@ public class Instance implements Comparable { private Date lastAccessed; @JsonProperty("launch") private LaunchModifier launchModifier; + private InstanceSettings settings = new InstanceSettings(); @JsonIgnore private File dir; @JsonIgnore private URL manifestURL; diff --git a/launcher/src/main/java/com/skcraft/launcher/InstanceSettings.java b/launcher/src/main/java/com/skcraft/launcher/InstanceSettings.java new file mode 100644 index 000000000..f42c6c7ca --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/InstanceSettings.java @@ -0,0 +1,14 @@ +package com.skcraft.launcher; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.skcraft.launcher.launch.JavaRuntime; +import com.skcraft.launcher.launch.MemorySettings; +import lombok.Data; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class InstanceSettings { + private JavaRuntime runtime; + private MemorySettings memorySettings; + private String customJvmArgs; +} diff --git a/launcher/src/main/java/com/skcraft/launcher/dialog/ConfigurationDialog.java b/launcher/src/main/java/com/skcraft/launcher/dialog/ConfigurationDialog.java index 70aecd6d3..908192063 100644 --- a/launcher/src/main/java/com/skcraft/launcher/dialog/ConfigurationDialog.java +++ b/launcher/src/main/java/com/skcraft/launcher/dialog/ConfigurationDialog.java @@ -8,6 +8,9 @@ import com.skcraft.launcher.Configuration; import com.skcraft.launcher.Launcher; +import com.skcraft.launcher.dialog.component.BetterComboBox; +import com.skcraft.launcher.launch.JavaRuntime; +import com.skcraft.launcher.launch.JavaRuntimeFinder; import com.skcraft.launcher.persistence.Persistence; import com.skcraft.launcher.swing.*; import com.skcraft.launcher.util.SharedLocale; @@ -29,7 +32,7 @@ public class ConfigurationDialog extends JDialog { private final JPanel tabContainer = new JPanel(new BorderLayout()); private final JTabbedPane tabbedPane = new JTabbedPane(); private final FormPanel javaSettingsPanel = new FormPanel(); - private final JTextField jvmPathText = new JTextField(); + private final JComboBox jvmRuntime = new BetterComboBox<>(); private final JTextField jvmArgsText = new JTextField(); private final JSpinner minMemorySpinner = new JSpinner(); private final JSpinner maxMemorySpinner = new JSpinner(); @@ -70,7 +73,10 @@ public ConfigurationDialog(Window owner, @NonNull Launcher launcher) { setResizable(false); setLocationRelativeTo(owner); - mapper.map(jvmPathText, "jvmPath"); + JavaRuntime[] javaRuntimes = JavaRuntimeFinder.getAvailableRuntimes().toArray(new JavaRuntime[0]); + jvmRuntime.setModel(new DefaultComboBoxModel<>(javaRuntimes)); + jvmRuntime.setSelectedItem(config.getJavaRuntime()); + mapper.map(jvmArgsText, "jvmArgs"); mapper.map(minMemorySpinner, "minMemory"); mapper.map(maxMemorySpinner, "maxMemory"); @@ -88,7 +94,7 @@ public ConfigurationDialog(Window owner, @NonNull Launcher launcher) { } private void initComponents() { - javaSettingsPanel.addRow(new JLabel(SharedLocale.tr("options.jvmPath")), jvmPathText); + javaSettingsPanel.addRow(new JLabel(SharedLocale.tr("options.jvmPath")), jvmRuntime); javaSettingsPanel.addRow(new JLabel(SharedLocale.tr("options.jvmArguments")), jvmArgsText); javaSettingsPanel.addRow(Box.createVerticalStrut(15)); javaSettingsPanel.addRow(new JLabel(SharedLocale.tr("options.64BitJavaWarning"))); @@ -157,6 +163,8 @@ public void actionPerformed(ActionEvent e) { */ public void save() { mapper.copyFromSwing(); + config.setJavaRuntime((JavaRuntime) jvmRuntime.getSelectedItem()); + Persistence.commitAndForget(config); dispose(); } diff --git a/launcher/src/main/java/com/skcraft/launcher/dialog/InstanceSettingsDialog.java b/launcher/src/main/java/com/skcraft/launcher/dialog/InstanceSettingsDialog.java new file mode 100644 index 000000000..7ef41fe8e --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/dialog/InstanceSettingsDialog.java @@ -0,0 +1,153 @@ +package com.skcraft.launcher.dialog; + +import com.skcraft.launcher.Instance; +import com.skcraft.launcher.InstanceSettings; +import com.skcraft.launcher.dialog.component.BetterComboBox; +import com.skcraft.launcher.launch.JavaRuntime; +import com.skcraft.launcher.launch.JavaRuntimeFinder; +import com.skcraft.launcher.launch.MemorySettings; +import com.skcraft.launcher.persistence.Persistence; +import com.skcraft.launcher.swing.FormPanel; +import com.skcraft.launcher.swing.LinedBoxPanel; +import com.skcraft.launcher.util.SharedLocale; +import lombok.extern.java.Log; + +import javax.swing.*; +import java.awt.*; + +@Log +public class InstanceSettingsDialog extends JDialog { + private final InstanceSettings settings; + + private final LinedBoxPanel formsPanel = new LinedBoxPanel(false); + private final FormPanel memorySettingsPanel = new FormPanel(); + private final JCheckBox enableMemorySettings = new JCheckBox(SharedLocale.tr("instance.options.customMemory")); + private final JSpinner minMemorySpinner = new JSpinner(); + private final JSpinner maxMemorySpinner = new JSpinner(); + + private final JCheckBox enableCustomRuntime = new JCheckBox(SharedLocale.tr("instance.options.customJava")); + private final FormPanel runtimePanel = new FormPanel(); + private final JComboBox javaRuntimeBox = new BetterComboBox<>(); + private final JTextField javaArgsBox = new JTextField(); + + private final LinedBoxPanel buttonsPanel = new LinedBoxPanel(true); + private final JButton okButton = new JButton(SharedLocale.tr("button.save")); + private final JButton cancelButton = new JButton(SharedLocale.tr("button.cancel")); + + private boolean saved = false; + + public InstanceSettingsDialog(Window owner, InstanceSettings settings) { + super(owner); + this.settings = settings; + + setTitle(SharedLocale.tr("instance.options.title")); + setModalityType(DEFAULT_MODALITY_TYPE); + initComponents(); + setSize(new Dimension(400, 500)); + setLocationRelativeTo(owner); + } + + private void initComponents() { + memorySettingsPanel.addRow(enableMemorySettings); + memorySettingsPanel.addRow(new JLabel(SharedLocale.tr("options.minMemory")), minMemorySpinner); + memorySettingsPanel.addRow(new JLabel(SharedLocale.tr("options.maxMemory")), maxMemorySpinner); + + // TODO: Do we keep this list centrally somewhere? Or is actively refreshing good? + JavaRuntime[] javaRuntimes = JavaRuntimeFinder.getAvailableRuntimes().toArray(new JavaRuntime[0]); + javaRuntimeBox.setModel(new DefaultComboBoxModel<>(javaRuntimes)); + + runtimePanel.addRow(enableCustomRuntime); + runtimePanel.addRow(new JLabel(SharedLocale.tr("options.jvmPath")), javaRuntimeBox); + runtimePanel.addRow(new JLabel(SharedLocale.tr("options.jvmArguments")), javaArgsBox); + + okButton.setMargin(new Insets(0, 10, 0, 10)); + buttonsPanel.addGlue(); + buttonsPanel.addElement(okButton); + buttonsPanel.addElement(cancelButton); + + enableMemorySettings.addActionListener(e -> { + if (enableMemorySettings.isSelected()) { + settings.setMemorySettings(new MemorySettings()); + } else { + settings.setMemorySettings(null); + } + + updateComponents(); + }); + + enableCustomRuntime.addActionListener(e -> { + runtimePanel.setEnabled(enableCustomRuntime.isSelected()); + }); + + okButton.addActionListener(e -> { + save(); + dispose(); + }); + + cancelButton.addActionListener(e -> dispose()); + + formsPanel.addElement(memorySettingsPanel); + formsPanel.addElement(runtimePanel); + + add(formsPanel, BorderLayout.NORTH); + add(buttonsPanel, BorderLayout.SOUTH); + + updateComponents(); + } + + private void updateComponents() { + if (settings.getMemorySettings() != null) { + memorySettingsPanel.setEnabled(true); + enableMemorySettings.setSelected(true); + + minMemorySpinner.setValue(settings.getMemorySettings().getMinMemory()); + maxMemorySpinner.setValue(settings.getMemorySettings().getMaxMemory()); + } else { + memorySettingsPanel.setEnabled(false); + enableMemorySettings.setSelected(false); + } + + if (settings.getRuntime() != null) { + runtimePanel.setEnabled(true); + enableCustomRuntime.setSelected(true); + } else { + runtimePanel.setEnabled(false); + enableCustomRuntime.setSelected(false); + } + + javaRuntimeBox.setSelectedItem(settings.getRuntime()); + javaArgsBox.setText(settings.getCustomJvmArgs()); + } + + private void save() { + if (enableMemorySettings.isSelected()) { + MemorySettings memorySettings = settings.getMemorySettings(); + + memorySettings.setMinMemory((int) minMemorySpinner.getValue()); + memorySettings.setMaxMemory((int) maxMemorySpinner.getValue()); + } else { + settings.setMemorySettings(null); + } + + if (enableCustomRuntime.isSelected()) { + settings.setRuntime((JavaRuntime) javaRuntimeBox.getSelectedItem()); + settings.setCustomJvmArgs(javaArgsBox.getText()); + } else { + settings.setRuntime(null); + settings.setCustomJvmArgs(null); + } + + saved = true; + } + + public static boolean open(Window parent, Instance instance) { + InstanceSettingsDialog dialog = new InstanceSettingsDialog(parent, instance.getSettings()); + dialog.setVisible(true); + + if (dialog.saved) { + Persistence.commitAndForget(instance); + } + + return dialog.saved; + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/dialog/LauncherFrame.java b/launcher/src/main/java/com/skcraft/launcher/dialog/LauncherFrame.java index 6fedd8ca4..2083da75b 100644 --- a/launcher/src/main/java/com/skcraft/launcher/dialog/LauncherFrame.java +++ b/launcher/src/main/java/com/skcraft/launcher/dialog/LauncherFrame.java @@ -241,6 +241,12 @@ public void actionPerformed(ActionEvent e) { }); popup.add(menuItem); + menuItem = new JMenuItem(SharedLocale.tr("instance.openSettings")); + menuItem.addActionListener(e -> { + InstanceSettingsDialog.open(this, selected); + }); + popup.add(menuItem); + popup.addSeparator(); if (!selected.isUpdatePending()) { diff --git a/launcher/src/main/java/com/skcraft/launcher/dialog/component/BetterComboBox.java b/launcher/src/main/java/com/skcraft/launcher/dialog/component/BetterComboBox.java new file mode 100644 index 000000000..b9bb22d24 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/dialog/component/BetterComboBox.java @@ -0,0 +1,28 @@ +package com.skcraft.launcher.dialog.component; + +import javax.swing.*; +import javax.swing.plaf.basic.BasicComboBoxUI; +import javax.swing.plaf.basic.BasicComboPopup; +import javax.swing.plaf.basic.ComboPopup; +import java.awt.*; + +public class BetterComboBox extends JComboBox { + public BetterComboBox() { + setUI(new BetterComboBoxUI()); + } + + private static class BetterComboBoxUI extends BasicComboBoxUI { + @Override + protected ComboPopup createPopup() { + BasicComboPopup popup = new BasicComboPopup(comboBox) { + @Override + protected Rectangle computePopupBounds(int px, int py, int pw, int ph) { + return super.computePopupBounds(px, py, Math.max(comboBox.getPreferredSize().width, pw), ph); + } + }; + + popup.getAccessibleContext().setAccessibleParent(comboBox); + return popup; + } + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/install/ProcessorTask.java b/launcher/src/main/java/com/skcraft/launcher/install/ProcessorTask.java index 4c7cf653c..9b547edc7 100644 --- a/launcher/src/main/java/com/skcraft/launcher/install/ProcessorTask.java +++ b/launcher/src/main/java/com/skcraft/launcher/install/ProcessorTask.java @@ -42,10 +42,15 @@ public class ProcessorTask implements InstallTask { @Override public void execute(Launcher launcher) throws Exception { VersionManifest versionManifest = manifest.getVersionManifest(); - loaderManifest.getSidedData().put("MINECRAFT_JAR", SidedData.of(launcher.getJarPath(versionManifest).getAbsolutePath())); LoaderSubResolver resolver = new LoaderSubResolver(manifest, loaderManifest, - Environment.getInstance(), Side.CLIENT, launcher.getBaseDir(), localFiles); + Environment.getInstance(), Side.CLIENT, launcher.getLibrariesDir(), localFiles); + + Map> sidedData = loaderManifest.getSidedData(); + sidedData.put("ROOT", SidedData.of(launcher.getInstallerDir().getAbsolutePath())); + sidedData.put("MINECRAFT_JAR", SidedData.of(launcher.getJarPath(versionManifest).getAbsolutePath())); + sidedData.put("LIBRARY_DIR", SidedData.of(launcher.getLibrariesDir().getAbsolutePath())); + sidedData.put("MINECRAFT_VERSION", SidedData.of(versionManifest.getId())); message = "Resolving parameters"; List programArgs = processor.resolveArgs(resolver); @@ -83,6 +88,7 @@ public void execute(Launcher launcher) throws Exception { message = "Executing"; log.info(String.format("Running processor '%s' with %d args", processor.getJar(), programArgs.size())); + log.info("Arguments: [" + String.join(", ", programArgs) + "]"); ClassLoader parent; try { @@ -94,13 +100,19 @@ public void execute(Launcher launcher) throws Exception { parent = null; } + ClassLoader prev = Thread.currentThread().getContextClassLoader(); ClassLoader cl = new URLClassLoader(classpath.toArray(new URL[0]), parent); try { Class mainClazz = Class.forName(mainClass, true, cl); Method main = mainClazz.getDeclaredMethod("main", String[].class); + + // engage spicy mode + Thread.currentThread().setContextClassLoader(cl); main.invoke(null, (Object) programArgs.toArray(new String[0])); } catch (Throwable e) { throw new RuntimeException(e); + } finally { + Thread.currentThread().setContextClassLoader(prev); } message = "Verifying"; @@ -118,6 +130,7 @@ public void execute(Launcher launcher) throws Exception { } if (!FileUtils.getShaHash(artifact).equals(output.getValue())) { + log.warning("Invalid hash, expected " + output.getValue()); throw new RuntimeException(String.format("Artifact '%s' has invalid hash!", output.getKey())); } diff --git a/launcher/src/main/java/com/skcraft/launcher/launch/JavaRuntime.java b/launcher/src/main/java/com/skcraft/launcher/launch/JavaRuntime.java new file mode 100644 index 000000000..5dc7963b7 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/launch/JavaRuntime.java @@ -0,0 +1,117 @@ +package com.skcraft.launcher.launch; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonValue; +import com.google.common.base.Objects; +import lombok.Data; + +import java.io.File; + +@Data +public class JavaRuntime implements Comparable { + private final File dir; + private final String version; + private final boolean is64Bit; + private boolean isMinecraftBundled = false; // Used only in list sorting & not serialized. + + @JsonValue + public File getDir() { + return dir; + } + + @JsonCreator + public static JavaRuntime fromDir(String dir) { + return JavaRuntimeFinder.getRuntimeFromPath(dir); + } + + @JsonIgnore + public int getMajorVersion() { + if (version == null) { + return 0; // uhh make this an error? + } + + String[] parts = version.split("\\."); + + if (parts.length < 2) { + throw new IllegalArgumentException("Invalid Java runtime version: " + version); + } + + if (parts[0].equals("1")) { + return Integer.parseInt(parts[1]); + } else { + return Integer.parseInt(parts[0]); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + JavaRuntime that = (JavaRuntime) o; + return Objects.equal(dir, that.dir); + } + + @Override + public int hashCode() { + return Objects.hashCode(dir); + } + + @Override + public int compareTo(JavaRuntime o) { + if (isMinecraftBundled && !o.isMinecraftBundled) { + return -1; + } else if (!isMinecraftBundled && o.isMinecraftBundled) { + return 1; + } + + if (is64Bit && !o.is64Bit) { + return -1; + } else if (!is64Bit && o.is64Bit) { + return 1; + } + + if (version == null) { + return 1; + } else if (o.version == null) { + return -1; + } + + String[] a = version.split("[\\._]"); + String[] b = o.version.split("[\\._]"); + int min = Math.min(a.length, b.length); + + for (int i = 0; i < min; i++) { + int first, second; + + try { + first = Integer.parseInt(a[i]); + } catch (NumberFormatException e) { + return -1; + } + + try { + second = Integer.parseInt(b[i]); + } catch (NumberFormatException e) { + return 1; + } + + if (first > second) { + return -1; + } else if (first < second) { + return 1; + } + } + + if (a.length == b.length) { + return 0; // Same + } + + return a.length > b.length ? -1 : 1; + } + + @Override + public String toString() { + return String.format("Java %s (%s) (%s)", version, is64Bit ? "64-bit" : "32-bit", dir); + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/launch/JavaRuntimeFinder.java b/launcher/src/main/java/com/skcraft/launcher/launch/JavaRuntimeFinder.java index 5bf1cb32f..e23059588 100644 --- a/launcher/src/main/java/com/skcraft/launcher/launch/JavaRuntimeFinder.java +++ b/launcher/src/main/java/com/skcraft/launcher/launch/JavaRuntimeFinder.java @@ -6,16 +6,16 @@ package com.skcraft.launcher.launch; +import com.skcraft.launcher.model.minecraft.JavaVersion; import com.skcraft.launcher.util.Environment; +import com.skcraft.launcher.util.EnvironmentParser; import com.skcraft.launcher.util.Platform; import com.skcraft.launcher.util.WinRegistry; import java.io.File; import java.io.IOException; import java.lang.reflect.InvocationTargetException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; +import java.util.*; /** * Finds the best Java runtime to use. @@ -25,52 +25,145 @@ public final class JavaRuntimeFinder { private JavaRuntimeFinder() { } + public static List getAvailableRuntimes() { + Environment env = Environment.getInstance(); + List entries = new ArrayList<>(); + File launcherDir; + + if (env.getPlatform() == Platform.WINDOWS) { + try { + String launcherPath = WinRegistry.readString(WinRegistry.HKEY_CURRENT_USER, + "SOFTWARE\\Mojang\\InstalledProducts\\Minecraft Launcher", "InstallLocation"); + + launcherDir = new File(launcherPath); + } catch (Throwable ignored) { + launcherDir = new File(System.getenv("APPDATA"), ".minecraft"); + } + + try { + getEntriesFromRegistry(entries, "SOFTWARE\\JavaSoft\\Java Runtime Environment"); + getEntriesFromRegistry(entries, "SOFTWARE\\JavaSoft\\Java Development Kit"); + } catch (Throwable ignored) { + } + } else if (env.getPlatform() == Platform.LINUX) { + launcherDir = new File(System.getenv("HOME"), ".minecraft"); + + String javaHome = System.getenv("JAVA_HOME"); + if (javaHome != null) { + entries.add(getRuntimeFromPath(javaHome)); + } + + File[] runtimesList = new File("/usr/lib/jvm").listFiles(); + if (runtimesList != null) { + Arrays.stream(runtimesList).map(file -> { + try { + return file.getCanonicalFile(); + } catch (IOException exception) { + return file; + } + }).distinct().forEach(file -> entries.add(getRuntimeFromPath(file.getAbsolutePath()))); + } + } else { + return Collections.emptyList(); + } + + File runtimes = new File(launcherDir, "runtime"); + File[] runtimeList = runtimes.listFiles(); + if (runtimeList != null) { + for (File potential : runtimeList) { + if (potential.getName().startsWith("jre-x")) { + boolean is64Bit = potential.getName().equals("jre-x64"); + + JavaRuntime runtime = new JavaRuntime(potential.getAbsoluteFile(), readVersionFromRelease(potential), is64Bit); + runtime.setMinecraftBundled(true); + entries.add(runtime); + } else { + String runtimeName = potential.getName(); + + String[] children = potential.list(); + if (children == null || children.length == 0) continue; + String platformName = children[0]; + + String[] parts = platformName.split("-"); + if (parts.length < 2) continue; + + String arch = parts[1]; + boolean is64Bit = arch.equals("x64"); + + File javaDir = new File(potential, String.format("%s/%s", platformName, runtimeName)); + JavaRuntime runtime = new JavaRuntime(javaDir.getAbsoluteFile(), readVersionFromRelease(javaDir), is64Bit); + runtime.setMinecraftBundled(true); + + entries.add(runtime); + } + } + } + + Collections.sort(entries); + return entries; + } + /** * Return the path to the best found JVM location. * * @return the JVM location, or null */ public static File findBestJavaPath() { - if (Environment.getInstance().getPlatform() != Platform.WINDOWS) { - return null; - } - - List entries = new ArrayList(); - try { - getEntriesFromRegistry(entries, "SOFTWARE\\JavaSoft\\Java Runtime Environment"); - getEntriesFromRegistry(entries, "SOFTWARE\\JavaSoft\\Java Development Kit"); - } catch (Throwable ignored) { - } - Collections.sort(entries); - + List entries = getAvailableRuntimes(); if (entries.size() > 0) { - return new File(entries.get(0).dir, "bin"); + return new File(entries.get(0).getDir(), "bin"); } return null; } + + public static Optional findBestJavaRuntime(JavaVersion targetVersion) { + List entries = getAvailableRuntimes(); + + return entries.stream().sorted() + .filter(runtime -> runtime.getMajorVersion() == targetVersion.getMajorVersion()) + .findFirst(); + } + + public static Optional findAnyJavaRuntime() { + return getAvailableRuntimes().stream().sorted().findFirst(); + } + + public static JavaRuntime getRuntimeFromPath(String path) { + File target = new File(path); + + if (target.isFile()) { + // Probably referring directly to bin/java, back up two levels + target = target.getParentFile().getParentFile(); + } + + { + File jre = new File(target, "jre/release"); + if (jre.isFile()) { + target = jre.getParentFile(); + } + } + + return new JavaRuntime(target, readVersionFromRelease(target), guessIf64Bit(target)); + } - private static void getEntriesFromRegistry(List entries, String basePath) + private static void getEntriesFromRegistry(List entries, String basePath) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { List subKeys = WinRegistry.readStringSubKeys(WinRegistry.HKEY_LOCAL_MACHINE, basePath); for (String subKey : subKeys) { - JREEntry entry = getEntryFromRegistry(basePath, subKey); + JavaRuntime entry = getEntryFromRegistry(basePath, subKey); if (entry != null) { entries.add(entry); } } } - private static JREEntry getEntryFromRegistry(String basePath, String version) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { + private static JavaRuntime getEntryFromRegistry(String basePath, String version) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { String regPath = basePath + "\\" + version; String path = WinRegistry.readString(WinRegistry.HKEY_LOCAL_MACHINE, regPath, "JavaHome"); File dir = new File(path); if (dir.exists() && new File(dir, "bin/java.exe").exists()) { - JREEntry entry = new JREEntry(); - entry.dir = dir; - entry.version = version; - entry.is64Bit = guessIf64Bit(dir); - return entry; + return new JavaRuntime(dir, version, guessIf64Bit(dir)); } else { return null; } @@ -84,52 +177,19 @@ private static boolean guessIf64Bit(File path) { return false; } } - - private static class JREEntry implements Comparable { - private File dir; - private String version; - private boolean is64Bit; - - @Override - public int compareTo(JREEntry o) { - if (is64Bit && !o.is64Bit) { - return -1; - } else if (!is64Bit && o.is64Bit) { - return 1; - } - - String[] a = version.split("[\\._]"); - String[] b = o.version.split("[\\._]"); - int min = Math.min(a.length, b.length); - - for (int i = 0; i < min; i++) { - int first, second; - - try { - first = Integer.parseInt(a[i]); - } catch (NumberFormatException e) { - return -1; - } - - try { - second = Integer.parseInt(b[i]); - } catch (NumberFormatException e) { - return 1; - } - - if (first > second) { - return -1; - } else if (first < second) { - return 1; - } - } - - if (a.length == b.length) { - return 0; // Same + + private static String readVersionFromRelease(File javaPath) { + File releaseFile = new File(javaPath, "release"); + if (releaseFile.exists()) { + try { + Map releaseDetails = EnvironmentParser.parse(releaseFile); + + return releaseDetails.get("JAVA_VERSION"); + } catch (IOException e) { + throw new RuntimeException("Failed to read release file", e); } - - return a.length > b.length ? -1 : 1; } - } + return null; + } } diff --git a/launcher/src/main/java/com/skcraft/launcher/launch/MemorySettings.java b/launcher/src/main/java/com/skcraft/launcher/launch/MemorySettings.java new file mode 100644 index 000000000..eed8b64cf --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/launch/MemorySettings.java @@ -0,0 +1,19 @@ +package com.skcraft.launcher.launch; + +import lombok.Data; + +/** + * Settings for launched process memory allocation. + */ +@Data +public class MemorySettings { + /** + * Minimum memory in megabytes. + */ + private int minMemory; + + /** + * Maximum memory in megabytes. + */ + private int maxMemory; +} diff --git a/launcher/src/main/java/com/skcraft/launcher/launch/Runner.java b/launcher/src/main/java/com/skcraft/launcher/launch/Runner.java index b32314a76..96602f506 100644 --- a/launcher/src/main/java/com/skcraft/launcher/launch/Runner.java +++ b/launcher/src/main/java/com/skcraft/launcher/launch/Runner.java @@ -32,6 +32,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.Callable; import static com.skcraft.launcher.LauncherUtils.checkInterrupted; @@ -211,9 +212,17 @@ private void addLibraries() throws LauncherException { * * @throws IOException on I/O error */ - private void addJvmArgs() throws IOException { - int minMemory = config.getMinMemory(); - int maxMemory = config.getMaxMemory(); + private void addJvmArgs() throws IOException, LauncherException { + Optional memorySettings = Optional.ofNullable(instance.getSettings().getMemorySettings()); + + int minMemory = memorySettings + .map(MemorySettings::getMinMemory) + .orElse(config.getMinMemory()); + + int maxMemory = memorySettings + .map(MemorySettings::getMaxMemory) + .orElse(config.getMaxMemory()); + int permGen = config.getPermGen(); if (minMemory <= 0) { @@ -240,16 +249,29 @@ private void addJvmArgs() throws IOException { builder.setMaxMemory(maxMemory); builder.setPermGen(permGen); - String rawJvmPath = config.getJvmPath(); - if (!Strings.isNullOrEmpty(rawJvmPath)) { - builder.tryJvmPath(new File(rawJvmPath)); + JavaRuntime selectedRuntime = Optional.ofNullable(instance.getSettings().getRuntime()) + .orElseGet(() -> Optional.ofNullable(versionManifest.getJavaVersion()) + .flatMap(JavaRuntimeFinder::findBestJavaRuntime) + .orElse(config.getJavaRuntime()) + ); + + // Builder defaults to a found runtime or just the PATH `java` otherwise + if (selectedRuntime != null) { + String rawJvmPath = selectedRuntime.getDir().getAbsolutePath(); + if (!Strings.isNullOrEmpty(rawJvmPath)) { + builder.tryJvmPath(new File(rawJvmPath)); + } } List flags = builder.getFlags(); - String rawJvmArgs = config.getJvmArgs(); - if (!Strings.isNullOrEmpty(rawJvmArgs)) { - for (String arg : JavaProcessBuilder.splitArgs(rawJvmArgs)) { - flags.add(arg); + String[] rawJvmArgsList = new String[] { + config.getJvmArgs(), + instance.getSettings().getCustomJvmArgs() + }; + + for (String rawJvmArgs : rawJvmArgsList) { + if (!Strings.isNullOrEmpty(rawJvmArgs)) { + flags.addAll(JavaProcessBuilder.splitArgs(rawJvmArgs)); } } @@ -407,6 +429,10 @@ private Map getCommandSubstitutions() throws JsonProcessingExcep map.put("classpath", builder.buildClassPath()); map.put("natives_directory", extractDir.getAbsolutePath()); + // Forge additions + map.put("library_directory", launcher.getLibrariesDir().getAbsolutePath()); + map.put("classpath_separator", System.getProperty("path.separator")); + return map; } diff --git a/launcher/src/main/java/com/skcraft/launcher/model/loader/InstallProcessor.java b/launcher/src/main/java/com/skcraft/launcher/model/loader/InstallProcessor.java index 505a67bf1..45e18f73e 100644 --- a/launcher/src/main/java/com/skcraft/launcher/model/loader/InstallProcessor.java +++ b/launcher/src/main/java/com/skcraft/launcher/model/loader/InstallProcessor.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.google.common.collect.Lists; +import com.skcraft.launcher.model.minecraft.Side; import lombok.Data; import java.util.Collections; @@ -16,6 +17,7 @@ public class InstallProcessor { private List classpath; private List args; private Map outputs; + private List sides; public List resolveArgs(LoaderSubResolver resolver) { return Lists.transform(getArgs(), resolver); @@ -32,4 +34,19 @@ public Map resolveOutputs(final LoaderSubResolver resolver) { return result; } + + public boolean shouldRunOn(Side side) { + if (sides == null) { + return true; + } + + switch (side) { + case CLIENT: + return sides.contains("client"); + case SERVER: + return sides.contains("server"); + } + + return false; + } } diff --git a/launcher/src/main/java/com/skcraft/launcher/model/loader/LoaderSubResolver.java b/launcher/src/main/java/com/skcraft/launcher/model/loader/LoaderSubResolver.java index 299aea712..9042daab0 100644 --- a/launcher/src/main/java/com/skcraft/launcher/model/loader/LoaderSubResolver.java +++ b/launcher/src/main/java/com/skcraft/launcher/model/loader/LoaderSubResolver.java @@ -17,11 +17,11 @@ public class LoaderSubResolver implements Function { private final LoaderManifest loader; private final Environment env; private final Side side; - private final File baseDir; + private final File libraryDir; private final HashMap localFiles; public String getPathOf(String... rest) { - File file = baseDir; + File file = libraryDir; for (String part : rest) { file = new File(file, part); } @@ -33,6 +33,8 @@ public String getPathOf(String... rest) { public String apply(String arg) { if (arg == null) return null; + arg = replaceTokens(arg); + while (true) { char start = arg.charAt(0); int bound = arg.length() - 1; @@ -47,9 +49,9 @@ public String apply(String arg) { String libraryName = arg.substring(1, bound); Library library = loader.findLibrary(libraryName); if (library != null) { - arg = getPathOf(manifest.getLibrariesLocation(), library.getPath(env)); + arg = getPathOf(library.getPath(env)); } else { - arg = getPathOf(manifest.getLibrariesLocation(), Library.mavenNameToPath(libraryName)); + arg = getPathOf(Library.mavenNameToPath(libraryName)); } } else if (start == '&' && end == '&') { String localFileName = arg.substring(1, bound); @@ -66,4 +68,55 @@ public String apply(String arg) { } } } + + private String replaceTokens(String arg) { + StringBuilder buf = new StringBuilder(); + + int length = arg.length(); + for (int i = 0; i < length; i++) { + char c = arg.charAt(i); + + if (c == '\\') { + buf.append(arg.charAt(i + 1)); + i++; + } else if (c == '{' || c == '\'') { + StringBuilder keyBuf = new StringBuilder(); + + for (int j = i + 1; j <= length; j++) { + if (j == length) { + throw new IllegalArgumentException("Illegal pattern: unclosed " + c); + } + + char d = arg.charAt(j); + + if (d == '\\') { + keyBuf.append(arg.charAt(j + 1)); + j++; + } else if (c == '{' && d == '}') { + String key = keyBuf.toString(); + SidedData sidedData = loader.getSidedData().get(key); + + if (sidedData != null) { + buf.append(sidedData.resolveFor(side)); + } else { + throw new IllegalArgumentException("Missing key: " + key); + } + + i = j; + break; + } else if (c == '\'' && d == '\'') { + buf.append(keyBuf.toString()); + i = j; + break; + } else { + keyBuf.append(d); + } + } + } else { + buf.append(c); + } + } + + return buf.toString(); + } } diff --git a/launcher/src/main/java/com/skcraft/launcher/model/loader/ProcessorEntry.java b/launcher/src/main/java/com/skcraft/launcher/model/loader/ProcessorEntry.java index dbceece71..33e7319c2 100644 --- a/launcher/src/main/java/com/skcraft/launcher/model/loader/ProcessorEntry.java +++ b/launcher/src/main/java/com/skcraft/launcher/model/loader/ProcessorEntry.java @@ -1,6 +1,7 @@ package com.skcraft.launcher.model.loader; import com.skcraft.launcher.install.*; +import com.skcraft.launcher.model.minecraft.Side; import com.skcraft.launcher.model.modpack.ManifestEntry; import lombok.AllArgsConstructor; import lombok.Data; @@ -19,6 +20,8 @@ public class ProcessorEntry extends ManifestEntry { public void install(Installer installer, InstallLog log, UpdateCache cache, InstallExtras extras) throws Exception { LocalLoader loader = extras.getLoader(loaderName); - installer.queueLate(new ProcessorTask(processor, loader.getManifest(), getManifest(), loader.getLocalFiles())); + if (processor.shouldRunOn(Side.CLIENT)) { + installer.queueLate(new ProcessorTask(processor, loader.getManifest(), getManifest(), loader.getLocalFiles())); + } } } diff --git a/launcher/src/main/java/com/skcraft/launcher/model/loader/profiles/ModernForgeInstallProfile.java b/launcher/src/main/java/com/skcraft/launcher/model/loader/profiles/ModernForgeInstallProfile.java index da79acaa1..b6b9478c7 100644 --- a/launcher/src/main/java/com/skcraft/launcher/model/loader/profiles/ModernForgeInstallProfile.java +++ b/launcher/src/main/java/com/skcraft/launcher/model/loader/profiles/ModernForgeInstallProfile.java @@ -21,6 +21,7 @@ @Data @JsonIgnoreProperties(ignoreUnknown = true) public class ModernForgeInstallProfile { + private int spec; private List libraries; private List processors; private Map> data; diff --git a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/JavaVersion.java b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/JavaVersion.java new file mode 100644 index 000000000..9cd26749a --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/JavaVersion.java @@ -0,0 +1,11 @@ +package com.skcraft.launcher.model.minecraft; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class JavaVersion { + private String component; + private int majorVersion; +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/VersionManifest.java b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/VersionManifest.java index d0fad1afd..f270ca42e 100644 --- a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/VersionManifest.java +++ b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/VersionManifest.java @@ -27,6 +27,7 @@ public class VersionManifest { private String mainClass; private int minimumLauncherVersion; private LinkedHashSet libraries; + private JavaVersion javaVersion; private Map downloads = new HashMap(); public String getAssetId() { diff --git a/launcher/src/main/java/com/skcraft/launcher/util/EnvironmentParser.java b/launcher/src/main/java/com/skcraft/launcher/util/EnvironmentParser.java new file mode 100644 index 000000000..227afea8f --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/util/EnvironmentParser.java @@ -0,0 +1,139 @@ +package com.skcraft.launcher.util; + +import com.google.common.io.CharSource; +import com.google.common.io.Files; +import lombok.Data; +import lombok.RequiredArgsConstructor; + +import java.io.BufferedReader; +import java.io.EOFException; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +/** + * Parses dotenv-style files. + */ +@RequiredArgsConstructor +public class EnvironmentParser { + private final BufferedReader reader; + + private char read() throws IOException { + int c = reader.read(); + + if (c == -1) { + throw new EOFException("End of stream reached unexpectedly!"); + } + + return (char) c; + } + + public Map parse() throws IOException { + HashMap result = new HashMap<>(); + + while (reader.ready()) { + KeyValue entry = parseLine(); + + result.put(entry.getKey(), entry.getValue()); + } + + return result; + } + + public KeyValue parseLine() throws IOException { + String key = parseKey(); + String value = parseValue(); + + try { + reader.mark(1); + char newline = read(); + if (newline == '\r') { + reader.mark(1); + if (read() != '\n') { + throw new IOException("Expected CRLF but only got CR"); + } + reader.reset(); + } else if (newline != '\n') { + reader.reset(); + } + } catch (EOFException ignored) { + } + + return new KeyValue(key, value); + } + + private String parseKey() throws IOException { + StringBuilder buffer = new StringBuilder(); + + // Very lenient key parsing. + while (true) { + char c = read(); + + switch (c) { + case '=': + case '\r': + case '\n': + return buffer.toString(); + default: + buffer.append(c); + } + } + } + + private String parseValue() throws IOException { + StringBuffer buffer = new StringBuffer(); + + while (true) { + char c = read(); + + switch (c) { + case '\r': + case '\n': + return buffer.toString(); + case '"': + buffer.append(parseQuotedPhrase()); + break; + case '\\': + char next = read(); + buffer.append(next); + break; + default: + buffer.append(c); + } + } + } + + private String parseQuotedPhrase() throws IOException { + StringBuilder buffer = new StringBuilder(); + + while (true) { + char c = read(); + + switch (c) { + case '"': + return buffer.toString(); + case '\\': + char next = read(); + buffer.append(next); + break; + default: + buffer.append(c); + } + } + } + + public static Map parse(File target) throws IOException { + CharSource charSource = Files.asCharSource(target, StandardCharsets.UTF_8); + + EnvironmentParser parser = new EnvironmentParser(charSource.openBufferedStream()); + return parser.parse(); + } + + @Data + private static class KeyValue { + private final String key; + private final String value; + } +} diff --git a/launcher/src/main/resources/com/skcraft/launcher/lang/Launcher.properties b/launcher/src/main/resources/com/skcraft/launcher/lang/Launcher.properties index f4d52a705..d8699d433 100644 --- a/launcher/src/main/resources/com/skcraft/launcher/lang/Launcher.properties +++ b/launcher/src/main/resources/com/skcraft/launcher/lang/Launcher.properties @@ -22,10 +22,11 @@ errors.selfUpdateCheckError=Checking for an update to the launcher has failed. button.cancel=Cancel button.ok=OK +button.save=Save options.title = Options options.useProxyCheck = Use following proxy in Minecraft -options.jvmPath=JVM path\: +options.jvmPath=Java Runtime\: options.jvmArguments=JVM arguments\: options.64BitJavaWarning=Make sure to have 64-bit Java installed if you are planning to set the memory limits higher. options.minMemory=Minimum memory (MB)\: @@ -52,6 +53,7 @@ instance.openSaves=View saves instance.openResourcePacks=View resource packs instance.openScreenshots=View screenshots instance.copyAsPath=Copy as path +instance.openSettings=Settings... instance.forceUpdate=Force update instance.hardForceUpdate=Hard force update... instance.deleteFiles=Delete files... @@ -62,6 +64,10 @@ instance.confirmHardUpdate=A hard force update will delete the contents of confi instance.resettingTitle=Resetting instance... instance.resettingStatus=Resetting ''{0}''... +instance.options.title=Instance Settings +instance.options.customJava=Use a custom Java runtime +instance.options.customMemory=Use custom memory settings + launcher.launch=Launch... launcher.checkForUpdates=Check for updates launcher.options=Options...