diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..2a53e1b --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,198 @@ + + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. + +------------------------------------------------------------------------------- + +The following license also applies to contributions by third parties: + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +* The names of its contributors may not be used to endorse or promote + products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/README.md b/README.md index 76ed922..3d24760 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,91 @@ -# StardustLauncher-Files +SKCraft Launcher +================ -Files for the StardustLauncher \ No newline at end of file +![Now with 1.13+ support!](readme/now_with_support.png) + +[![Liberapay](https://liberapay.com/assets/widgets/donate.svg)](https://liberapay.com/offbeatwitch/donate) + +*If you rely on this software, please consider [donating](https://liberapay.com/offbeatwitch/donate) to help keep it maintained!* + +Need to make it easy for people to install and play your modpack? The SKCraft Launcher platform may be for you if: + +:heavy_check_mark: You want your own logo and branding on the launcher, with your own news feed +:heavy_check_mark: You want to be able to update the modpack whenever you want +:heavy_check_mark: You don't want to depend on anyone else +:heavy_check_mark: There's several modpacks you want to provide as a set +:heavy_check_mark: You want to provide 'hidden' modpacks that are only accessible by a secret code + +It may not be for you if: + +:small_orange_diamond: You made a cool modpack and you just want it out there +:small_orange_diamond: You don't have a website or place for people to download files from +:small_orange_diamond: You don't want anything to do with distributing the launcher + +## What We Do Right + +Creating modpacks with this launcher is super easy: :one: only drag & drop of configs and mods into a folder, :two: ZERO! scary XML/JSON files to edit, and :three: there's a nice interface for creating modpacks (command-line is also available). + +The launcher has all standard features that you'd expect like :one: resume of incomplete downloads, :two: incremental updates, and :three: file deduplication (saves disk space when you have files duplicated between updates or modpacks). + +LiteLoader, Forge, Fabric, and custom .jar mods are supported. You can put resource packs in, or really even random mod files that don't go in the configs or mods folder. + +You do need some sort of website, but it does **not** need anything complicated like PHP. + +For those technically inclined, you can generate modpacks from Git (or any VCS) using your favorite continuous integration server, and then dynamically create modpacks even down to one per branch or tag. + +Best of all, our launcher is Minecraft's oldest surviving modpack launcher platform. :thumbsup: + +## Getting Started + +You'll probably need a few hours to get everything working. Fortunately, we have a [detailed wiki](https://github.com/SKCraft/Launcher/wiki) with screenshots for every step, intended for someone with minimal technical experience. + +For your evaluation, there are sample files provided that you will be able to upload to your site to see if it even works for you. + +## Pretty Pictures + +If you are going to create modpacks with our GUI tool, you'll be seeing this beaut: + +![Modpack Creator](readme/pack_manager.png) + +![Modpack Creator](readme/modpack_creator.png) + +![Build Modpacks](readme/packages_generator.png) + +Making a modpack is just drag and drop into a folder like this: + +* src/**config**/ +* src/**mods**/ +* src/**resourcepacks**/ +* loaders/ + +LiteLoader and Forge installers, or Fabric Loader, go into the *loaders* folder. :ok_hand: + +### More Pictures + +There's a dark version of the launcher (personally not the creator's cup of tea) that you can use: + +![Skinned](readme/launcher_skinned.png) + +Users can select optional mods/features: + +![Optional Features](readme/features.png) + +The launcher can update itself and you can use it in portable mode too. + +### The Light Theme + +![Main Launcher](readme/launcher.png) + +![Options](readme/options.png) + +![Console](readme/log.png) + +## Contributing + +Pull requests can be submitted on GitHub, but we will accept them at our discretion. Please note that your code must follow Oracle's Java Code Conventions. + +Contributions by third parties must be dual licensed under the two licenses described within LICENSE.txt (GNU Lesser General Public License, version 3, and the 3-clause BSD license). + +## License + +The launcher is licensed under the GNU Lesser General Public License, version 3. diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..8a44f50 --- /dev/null +++ b/build.bat @@ -0,0 +1,3 @@ +@echo off +call gradlew clean build +pause \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..49eea45 --- /dev/null +++ b/build.gradle @@ -0,0 +1,75 @@ +plugins { + id "com.github.johnrengelman.shadow" version "7.1.2" + id 'io.freefair.lombok' version '5.3.0' +} + +println """ +******************************************* + You are building SKCraft Launcher! + + Output files will be in [subproject]/build/libs +******************************************* +""" + +subprojects { + apply plugin: 'java' + + group = 'com.skcraft' + version = '4.6-SNAPSHOT' + + java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(8)) + } + } + + repositories { + mavenCentral() + } + + if (JavaVersion.current().isJava8Compatible()) { + // Java 8 turns on doclint which we fail + tasks.withType(Javadoc) { + options.addStringOption('Xdoclint:none', '-quiet') + } + } + + tasks.withType(JavaExec) { + workingDir = new File(rootDir, "run/") + workingDir.mkdirs() + } + + // Work around gradle shadow bug + // see https://github.com/johnrengelman/shadow/issues/713 + afterEvaluate { + startScripts { + dependsOn(shadowJar) + } + + distTar { + dependsOn(shadowJar) + } + + distZip { + dependsOn(shadowJar) + } + + startShadowScripts { + dependsOn(jar) + } + + shadowDistTar { + dependsOn(jar) + } + + shadowDistZip { + dependsOn(jar) + } + } +} + +task clean { + subprojects { + rootProject.clean.dependsOn tasks.matching { it.name == "clean" } + } +} diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..249c540 --- /dev/null +++ b/build.sh @@ -0,0 +1,3 @@ +#!/bin/sh +./gradlew clean build +read -p "Press any key to continue..." \ No newline at end of file diff --git a/creator-tools/build.gradle b/creator-tools/build.gradle new file mode 100644 index 0000000..0905e6d --- /dev/null +++ b/creator-tools/build.gradle @@ -0,0 +1,33 @@ +plugins { + id 'application' + id "com.github.johnrengelman.shadow" + id 'io.freefair.lombok' +} + +version = "2.1.0-SNAPSHOT" + +application { + mainClassName = "com.skcraft.launcher.creator.Creator" +} + +dependencies { + implementation project(path: ':launcher-builder') + implementation 'org.eclipse.jetty:jetty-server:9.3.1.v20150714' + implementation 'com.jidesoft:jide-oss:3.6.18' +} + +processResources { + filesMatching('**/*.properties') { + filter { + it.replace('${project.version}', project.version) + } + } +} + +shadowJar { + archiveClassifier.set("") +} + +build { + dependsOn(shadowJar) +} diff --git a/creator-tools/lombok.config b/creator-tools/lombok.config new file mode 100644 index 0000000..6aa51d7 --- /dev/null +++ b/creator-tools/lombok.config @@ -0,0 +1,2 @@ +# This file is generated by the 'io.freefair.lombok' Gradle plugin +config.stopBubbling = true diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/Creator.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/Creator.java new file mode 100644 index 0000000..6f15707 --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/Creator.java @@ -0,0 +1,88 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator; + +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import com.skcraft.launcher.Launcher; +import com.skcraft.launcher.creator.controller.WelcomeController; +import com.skcraft.launcher.creator.dialog.WelcomeDialog; +import com.skcraft.launcher.creator.model.creator.CreatorConfig; +import com.skcraft.launcher.creator.model.creator.RecentEntry; +import com.skcraft.launcher.creator.model.creator.Workspace; +import com.skcraft.launcher.persistence.Persistence; +import com.skcraft.launcher.swing.SwingHelper; +import lombok.Getter; + +import javax.swing.*; +import javax.swing.filechooser.FileSystemView; +import java.io.File; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.Executors; + +public class Creator { + + @Getter private final File dataDir; + @Getter private final CreatorConfig config; + @Getter private final ListeningExecutorService executor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool()); + + public Creator() { + this.dataDir = getAppDataDir(); + this.config = Persistence.load(new File(dataDir, "config.json"), CreatorConfig.class); + + // Remove deleted workspaces + List recentEntries = config.getRecentEntries(); + Iterator it = recentEntries.iterator(); + while (it.hasNext()) { + RecentEntry workspace = it.next(); + if (!Workspace.getWorkspaceFile(workspace.getPath()).exists()) { + it.remove(); + } + } + } + + public void showWelcome() { + WelcomeDialog dialog = new WelcomeDialog(); + WelcomeController controller = new WelcomeController(dialog, this); + controller.show(); + } + + private static File getFileChooseDefaultDir() { + JFileChooser chooser = new JFileChooser(); + FileSystemView fsv = chooser.getFileSystemView(); + return fsv.getDefaultDirectory(); + } + + private static File getAppDataDir() { + String osName = System.getProperty("os.name").toLowerCase(); + if (osName.contains("win")) { + return new File(getFileChooseDefaultDir(), "SKCraft Modpack Creator"); + } else { + return new File(System.getProperty("user.home"), ".skcraftcreator"); + } + } + + public static void main(String[] args) throws Exception { + Launcher.setupLogger(); + System.setProperty("skcraftLauncher.killWithoutConfirm", "true"); + + final Creator creator = new Creator(); + + SwingUtilities.invokeAndWait(() -> { + SwingHelper.setSwingProperties("Modpack Creator"); + SwingHelper.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + + try { + creator.showWelcome(); + } catch (Exception e) { + SwingHelper.showErrorDialog(null, "Failed to start the modpack creator program.", "Start Error", e); + } + }); + } + +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/controller/GenerateListingController.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/controller/GenerateListingController.java new file mode 100644 index 0000000..688cad8 --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/controller/GenerateListingController.java @@ -0,0 +1,218 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.controller; + +import com.google.common.base.Optional; +import com.google.common.collect.Lists; +import com.google.common.io.Files; +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.creator.dialog.GenerateListingDialog; +import com.skcraft.launcher.creator.dialog.ManifestEntryDialog; +import com.skcraft.launcher.creator.model.creator.ManifestEntry; +import com.skcraft.launcher.creator.model.creator.Workspace; +import com.skcraft.launcher.creator.model.swing.ListingType; +import com.skcraft.launcher.creator.model.swing.ManifestEntryTableModel; +import com.skcraft.launcher.dialog.ProgressDialog; +import com.skcraft.launcher.persistence.Persistence; +import com.skcraft.launcher.swing.SwingHelper; +import com.skcraft.launcher.util.SwingExecutor; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.List; +import java.util.stream.Collectors; + +public class GenerateListingController { + + private final GenerateListingDialog dialog; + private final Workspace workspace; + private final List manifestEntries; + private final ListeningExecutorService executor; + private final ManifestEntryTableModel manifestTableModel; + + public GenerateListingController(GenerateListingDialog dialog, Workspace workspace, List manifestEntries, ListeningExecutorService executor) { + this.dialog = dialog; + this.workspace = workspace; + this.manifestEntries = manifestEntries; + this.executor = executor; + + this.manifestTableModel = new ManifestEntryTableModel(manifestEntries); + dialog.getManifestsTable().setModel(manifestTableModel); + dialog.getManifestsTableAdjuster().adjustColumns(); + + initListeners(); + + setListingType(workspace.getPackageListingType()); + } + + public void setOutputDir(File dir) { + dialog.getDestDirField().setPath(dir.getAbsolutePath()); + } + + public void setListingType(ListingType type) { + dialog.getListingTypeCombo().setSelectedItem(type); + } + + public void show() { + dialog.setVisible(true); + } + + public Optional getManifestFromIndex(int selectedIndex) { + if (selectedIndex >= 0) { + ManifestEntry manifest = manifestEntries.get(selectedIndex); + if (manifest != null) { + return Optional.fromNullable(manifest); + } + } + return Optional.absent(); + } + + public Optional getSelectedManifest() { + JTable table = dialog.getManifestsTable(); + int selectedIndex = table.getSelectedRow(); + if (selectedIndex >= 0) { + selectedIndex = table.convertRowIndexToModel(selectedIndex); + ManifestEntry manifest = manifestEntries.get(selectedIndex); + if (manifest != null) { + return Optional.fromNullable(manifest); + } + } + + SwingHelper.showErrorDialog(dialog, "Please select a modpack from the list.", "Error"); + return Optional.absent(); + } + + private void updateManifestEntryInTable(ManifestEntry manifestEntry) { + int index = manifestEntries.indexOf(manifestEntry); + if (index >= 0) { + manifestTableModel.fireTableRowsUpdated(index, index); + } + } + + private void initListeners() { + dialog.getManifestsTable().addMouseListener(new MouseAdapter() { + public void mousePressed(MouseEvent e) { + if (e.getClickCount() == 2) { + JTable table = (JTable) e.getSource(); + Point point = e.getPoint(); + int selectedIndex = table.rowAtPoint(point); + if (selectedIndex >= 0) { + selectedIndex = table.convertRowIndexToModel(selectedIndex); + Optional optional = getManifestFromIndex(selectedIndex); + if (optional.isPresent()) { + if (showModifyDialog(optional.get())) { + updateManifestEntryInTable(optional.get()); + } + } + } + } + } + }); + + dialog.getListingTypeCombo().addItemListener(e -> { + ListingType type = (ListingType) e.getItem(); + dialog.getGameKeyWarning().setVisible(!type.isGameKeyCompatible()); + }); + + dialog.getEditManifestButton().addActionListener(e -> { + Optional optional = getSelectedManifest(); + if (optional.isPresent()) { + if (showModifyDialog(optional.get())) { + updateManifestEntryInTable(optional.get()); + } + } + }); + + dialog.getGenerateButton().addActionListener(e -> tryGenerate()); + + dialog.getCancelButton().addActionListener(e -> dialog.dispose()); + } + + private boolean showModifyDialog(ManifestEntry manifestEntry) { + ManifestEntryDialog modifyDialog = new ManifestEntryDialog(dialog); + modifyDialog.setTitle("Modify " + manifestEntry.getManifestInfo().getLocation()); + ManifestEntryController controller = new ManifestEntryController(modifyDialog, manifestEntry); + return controller.show(); + } + + private boolean tryGenerate() { + String path = dialog.getDestDirField().getPath().trim(); + + if (path.isEmpty()) { + SwingHelper.showErrorDialog(dialog, "A directory must be entered.", "Error"); + return false; + } + + List selected = manifestEntries.stream() + .filter(ManifestEntry::isSelected) + .sorted() + .collect(Collectors.toCollection(Lists::newArrayList)); + + if (selected.isEmpty()) { + SwingHelper.showErrorDialog(dialog, "At least one modpack must be selected to appear in the package list.", "Error"); + return false; + } + + ListingType listingType = (ListingType) dialog.getListingTypeCombo().getSelectedItem(); + File destDir = new File(path); + destDir.mkdirs(); + File file = new File(destDir, listingType.getFilename()); + + workspace.setPackageListingEntries(selected); + workspace.setPackageListingType(listingType); + Persistence.commitAndForget(workspace); + + SettableProgress progress = new SettableProgress("Generating package listing...", -1); + + Deferred deferred = Deferreds.makeDeferred(executor.submit(() -> listingType.generate(selected))) + .thenTap(() -> progress.set("Deleting older package listing files...", -1)) + .thenApply(input -> { + for (ListingType otherListingType : ListingType.values()) { + File f = new File(destDir, otherListingType.getFilename()); + if (f.exists()) { + f.delete(); + } + } + + return input; + }) + .thenTap(() -> progress.set("Writing package listing to disk...", -1)) + .thenApply(input -> { + try { + Files.write(input, file, Charset.forName("UTF-8")); + return file; + } catch (IOException e) { + throw new RuntimeException("Failed to write package listing file to disk", e); + } + }) + .handleAsync(v -> { + if (listingType.isGameKeyCompatible()) { + SwingHelper.showMessageDialog(dialog, "Successfully generated package listing.", "Success", null, JOptionPane.INFORMATION_MESSAGE); + } else { + SwingHelper.showMessageDialog(dialog, "Successfully generated package listing.\n\n" + + "Note that any modpacks with game keys set were not added.", + "Success", null, JOptionPane.INFORMATION_MESSAGE); + } + dialog.dispose(); + SwingHelper.browseDir(destDir, dialog); + }, ex -> {}, SwingExecutor.INSTANCE); + + ProgressDialog.showProgress(dialog, deferred, progress, "Writing package listing...", "Writing package listing..."); + SwingHelper.addErrorDialogCallback(dialog, deferred); + + return true; + } + +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/controller/ManifestEntryController.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/controller/ManifestEntryController.java new file mode 100644 index 0000000..c5cc42e --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/controller/ManifestEntryController.java @@ -0,0 +1,55 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.controller; + +import com.skcraft.launcher.creator.dialog.ManifestEntryDialog; +import com.skcraft.launcher.creator.model.creator.ManifestEntry; +import com.skcraft.launcher.swing.SwingHelper; + +public class ManifestEntryController { + + private final ManifestEntryDialog dialog; + private final ManifestEntry manifestEntry; + + private boolean save; + + public ManifestEntryController(ManifestEntryDialog dialog, ManifestEntry manifestEntry) { + this.dialog = dialog; + this.manifestEntry = manifestEntry; + + initListeners(); + copyFrom(); + } + + private void copyFrom() { + dialog.getIncludeCheck().setSelected(manifestEntry.isSelected()); + dialog.getPrioritySpinner().setValue(manifestEntry.getManifestInfo().getPriority()); + SwingHelper.setTextAndResetCaret(dialog.getGameKeysText(), SwingHelper.listToLines(manifestEntry.getGameKeys())); + } + + private void copyTo() { + manifestEntry.setSelected(dialog.getIncludeCheck().isSelected()); + manifestEntry.getManifestInfo().setPriority((Integer) dialog.getPrioritySpinner().getValue()); + manifestEntry.setGameKeys(SwingHelper.linesToList(dialog.getGameKeysText().getText())); + } + + public boolean show() { + dialog.setVisible(true); + return save; + } + + private void initListeners() { + dialog.getOkButton().addActionListener(e -> { + copyTo(); + save = true; + dialog.dispose(); + }); + + dialog.getCancelButton().addActionListener(e -> dialog.dispose()); + } + +} 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 new file mode 100644 index 0000000..63f6440 --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/controller/PackManagerController.java @@ -0,0 +1,862 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.controller; + +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; +import com.skcraft.launcher.auth.Session; +import com.skcraft.launcher.builder.BuilderConfig; +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; +import com.skcraft.launcher.creator.model.creator.*; +import com.skcraft.launcher.creator.model.swing.PackTableModel; +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.*; +import com.skcraft.launcher.model.modpack.LaunchModifier; +import com.skcraft.launcher.persistence.Persistence; +import com.skcraft.launcher.swing.PopupMouseAdapter; +import com.skcraft.launcher.swing.SwingHelper; +import com.skcraft.launcher.util.MorePaths; +import com.skcraft.launcher.util.SwingExecutor; +import lombok.Getter; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.io.File; +import java.io.IOException; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.regex.Pattern; + +public class PackManagerController { + + private static final DateFormat VERSION_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss"); + private static final Pattern FILENAME_SANITIZE = Pattern.compile("[^a-z0-9_\\-\\.]+"); + + @Getter private final File workspaceDir; + @Getter private final Creator creator; + @Getter private final File workspaceFile; + @Getter private final File dataDir; + @Getter private final File distDir; + @Getter private final File launcherDir; + @Getter private File webRoot; + @Getter private Workspace workspace; + @Getter private final Launcher launcher; + @Getter private final ListeningExecutorService executor; + @Getter private final TestServer testServer; + + private File lastServerDestDir; + + private final PackManagerFrame frame; + private PackTableModel packTableModel; + + public PackManagerController(PackManagerFrame frame, File workspaceDir, Creator creator) throws IOException { + this.workspaceDir = workspaceDir; + this.creator = creator; + this.dataDir = Workspace.getDataDir(workspaceDir); + workspaceFile = Workspace.getWorkspaceFile(workspaceDir); + + this.distDir = new File(workspaceDir, "_upload"); + launcherDir = new File(dataDir, "staging/launcher"); + File launcherConfigDir = new File(creator.getDataDir(), "launcher"); + this.webRoot = new File(dataDir, "staging/www"); + + launcherDir.mkdirs(); + launcherConfigDir.mkdirs(); + webRoot.mkdirs(); + + this.launcher = new Launcher(launcherDir, launcherConfigDir); + this.executor = launcher.getExecutor(); + this.frame = frame; + + TestServerBuilder builder = new TestServerBuilder(); + builder.setBaseDir(webRoot); + builder.setPort(0); + testServer = builder.build(); + } + + public void show() { + frame.setVisible(true); + frame.setTitle("Modpack Creator - [" + workspaceDir.getAbsolutePath() + "]"); + + initListeners(); + loadWorkspace(); + + Deferreds.makeDeferred(executor.submit(() -> { + startServer(); + return null; + })) + .handle( + result -> { + }, + (ex) -> SwingHelper.showErrorDialog(frame, "Failed to start a local web server. You will be unable to test modpacks.", "Error", ex) + ); + } + + private void startServer() throws Exception { + testServer.start(); + + launcher.getProperties().setProperty("newsUrl", "http://localhost:" + testServer.getLocalPort() + "/news.html"); + launcher.getProperties().setProperty("packageListUrl", "http://localhost:" + testServer.getLocalPort() + "/packages.json"); + launcher.getProperties().setProperty("selfUpdateUrl", "http://localhost:" + testServer.getLocalPort() + "/latest.json"); + } + + private void loadWorkspace() { + PackLoader loader = new PackLoader(); + + SettableProgress progress = new SettableProgress("Loading workspace...", -1); + + Deferred deferred = Deferreds.makeDeferred(executor.submit(() -> { + Workspace workspace = Persistence.load(workspaceFile, Workspace.class); + workspace.setDirectory(workspaceDir); + workspace.load(); + if (!workspaceFile.exists()) { + Persistence.commitAndForget(workspace); + } + this.workspace = workspace; + return workspace; + })) + .thenTap(() -> progress.observe(loader)) + .thenApply(loader) + .thenApplyAsync(packs -> { + JTable table = frame.getPackTable(); + packTableModel = new PackTableModel(packs); + table.setModel(packTableModel); + packTableModel.fireTableDataChanged(); + table.getRowSorter().toggleSortOrder(1); + if (packTableModel.getRowCount() > 0) { + table.addRowSelectionInterval(0, 0); + } + + return packs; + }, SwingExecutor.INSTANCE); + + ProgressDialog.showProgress(frame, deferred, progress, "Loading workspace...", "Loading workspace..."); + SwingHelper.addErrorDialogCallback(frame, deferred); + } + + private boolean checkPackLoaded(Pack pack) { + if (pack.isLoaded()) { + return true; + } else { + SwingHelper.showErrorDialog(frame, "The selected pack could not be loaded. You will have to remove it from the workspace or change its location.", "Error"); + return false; + } + } + + public Optional getPackFromIndex(int selectedIndex, boolean requireLoaded) { + if (selectedIndex >= 0) { + Pack pack = workspace.getPacks().get(selectedIndex); + if (pack != null && (!requireLoaded || checkPackLoaded(pack))) { + return Optional.of(pack); + } + } + return Optional.empty(); + } + + public Optional getSelectedPack(boolean requireLoaded) { + JTable table = frame.getPackTable(); + int selectedIndex = table.getSelectedRow(); + if (selectedIndex >= 0) { + selectedIndex = table.convertRowIndexToModel(selectedIndex); + Pack pack = workspace.getPacks().get(selectedIndex); + if (pack != null && (!requireLoaded || checkPackLoaded(pack))) { + return Optional.of(pack); + } + } + + SwingHelper.showErrorDialog(frame, "Please select a modpack from the list.", "Error"); + return Optional.empty(); + } + + public boolean writeWorkspace() { + try { + Persistence.commit(workspace); + return true; + } catch (IOException e) { + SwingHelper.showErrorDialog(frame, "Failed to write the current state of the workspace to disk. " + + "Any recent changes to the list of modpacks may not appear in the workspace on the next load.", "Error", e); + return false; + } + } + + public boolean writeBuilderConfig(Pack pack, BuilderConfig config) { + try { + Persistence.write(pack.getConfigFile(), config, Persistence.L2F_LIST_PRETTY_PRINTER); + return true; + } catch (IOException e) { + SwingHelper.showErrorDialog(frame, "Failed to write modpack.json to disk. Aborting.", "Error", e); + return false; + } + } + + private boolean isOfflineEnabled() { + CreatorConfig config = creator.getConfig(); + + if (config.isOfflineEnabled()) { + return true; + } else { + Session session = AccountSelectDialog.showAccountRequest(frame, launcher); + if (session != null) { + config.setOfflineEnabled(true); + Persistence.commitAndForget(config); + return true; + } else { + return false; + } + } + } + + public boolean canAddPackDir(File dir) { + try { + if (dir.exists() && !dir.isDirectory()) { + SwingHelper.showErrorDialog(frame, "The selected path is a file that already exists. It must be a directory.", "Error"); + return false; + } else if (dir.getCanonicalPath().equals(workspaceDir.getCanonicalPath())) { + SwingHelper.showErrorDialog(frame, "You cannot choose the workspace directory.", "Error"); + return false; + } else if (workspace.hasPack(dir)) { + SwingHelper.showErrorDialog(frame, "There is already a modpack in this workspace that uses that directory.", "Error"); + return false; + } else { + return true; + } + } catch (IOException e) { + SwingHelper.showErrorDialog(frame, "An unexpected error occurred while checking if the modpack being added can be added.", "Error", e); + return false; + } + } + + public boolean addPackToWorkspace(Pack pack) { + pack.load(); + + try { + File base = workspaceDir; + File child = pack.getDirectory(); + if (MorePaths.isSubDirectory(base, child)) { + pack.setLocation(MorePaths.relativize(base, child)); + } + } catch (IOException e) { + SwingHelper.showErrorDialog(frame, "An unexpected error occurred that could have been caused by the removal " + + "of the workspace or modpack directory.", "Error", e); + return false; + } + + List packs = workspace.getPacks(); + pack.setWorkspace(workspace); + packs.add(pack); + packTableModel.fireTableRowsInserted(packs.size() - 1, packs.size() - 1); + writeWorkspace(); + + return true; + } + + public void updatePackInWorkspace(Pack pack) { + List packs = workspace.getPacks(); + pack.load(); + int index = packs.indexOf(pack); + if (index >= 0) { + packTableModel.fireTableRowsUpdated(index, index); + } + writeWorkspace(); + } + + public boolean removePackFromWorkspace(Pack pack) { + if (workspace.getPacks().remove(pack)) { + packTableModel.fireTableDataChanged(); + writeWorkspace(); + return true; + } else { + return false; + } + } + + private void addDefaultConfig(BuilderConfig config) { + LaunchModifier launchModifier = new LaunchModifier(); + launchModifier.setFlags(ImmutableList.of("-Dfml.ignoreInvalidMinecraftCertificates=true")); + config.setLaunchModifier(launchModifier); + + FnPatternList userFiles = new FnPatternList(); + userFiles.setInclude(Lists.newArrayList("options.txt", "optionsshaders.txt")); + userFiles.setExclude(Lists.newArrayList()); + config.setUserFiles(userFiles); + } + + private void initListeners() { + frame.addWindowListener(new WindowAdapter() { + @Override + public void windowClosed(WindowEvent e) { + try { + testServer.stop(); + } catch (Exception ignored) { + } + System.exit(0); // TODO: Proper shutdown + } + }); + + frame.getPackTable().addMouseListener(new MouseAdapter() { + public void mousePressed(MouseEvent e) { + if (e.getClickCount() == 2) { + JTable table = (JTable) e.getSource(); + Point point = e.getPoint(); + int selectedIndex = table.rowAtPoint(point); + if (selectedIndex >= 0) { + selectedIndex = table.convertRowIndexToModel(selectedIndex); + Optional optional = getPackFromIndex(selectedIndex, true); + if (optional.isPresent()) { + if (e.isControlDown()) { + SwingHelper.browseDir(optional.get().getDirectory(), frame); + } else { + startTest(optional.get(), false); + } + } + } + } + } + }); + + frame.getPackTable().addMouseListener(new PopupMouseAdapter() { + @Override + protected void showPopup(MouseEvent e) { + JTable table = (JTable) e.getSource(); + Point point = e.getPoint(); + int selectedIndex = table.rowAtPoint(point); + if (selectedIndex >= 0) { + table.setRowSelectionInterval(selectedIndex, selectedIndex); + Optional optional = getSelectedPack(false); + if (optional.isPresent()) { + popupPackMenu(e.getComponent(), e.getX(), e.getY(), optional.get()); + } + } + } + }); + + frame.getNewPackMenuItem().addActionListener(event -> tryAddPackViaDialog()); + + frame.getNewPackAtLocationMenuItem().addActionListener(e -> tryAddPackViaDirectory(true)); + + frame.getImportPackMenuItem().addActionListener(event -> tryAddPackViaDirectory(false)); + + frame.getRemovePackItem().addActionListener(e -> { + Optional optional = getSelectedPack(false); + + if (optional.isPresent()) { + Pack pack = optional.get(); + + if (!pack.isLoaded() || SwingHelper.confirmDialog(frame, "Are you sure that you want to remove this modpack? No files will be deleted " + + "and you can later re-import the modpack using 'Add Existing'.", "Confirm")) { + removePackFromWorkspace(pack); + } + } + }); + + frame.getDeletePackItem().addActionListener(e -> { + Optional optional = getSelectedPack(false); + + if (optional.isPresent()) { + Pack pack = optional.get(); + + String input = JOptionPane.showInputDialog( + frame, + "Are you sure that you want to delete '" + pack.getDirectory().getAbsolutePath() + "'? " + + "If yes, type 'delete' below.", + "Confirm", + JOptionPane.WARNING_MESSAGE); + + if (input != null && input.replaceAll("'", "").equalsIgnoreCase("delete")) { + removePackFromWorkspace(pack); + + DirectoryDeleter deleter = new DirectoryDeleter(pack.getDirectory()); + Deferred deferred = Deferreds.makeDeferred(executor.submit(deleter), executor); + ProgressDialog.showProgress(frame, deferred, deleter, "Deleting modpack...", "Deleting modpack..."); + SwingHelper.addErrorDialogCallback(frame, deferred); + } else if (input != null) { + SwingHelper.showMessageDialog(frame, "You did not enter the correct word. Nothing was deleted.", "Failure", null, JOptionPane.INFORMATION_MESSAGE); + } + } + }); + + frame.getChangePackLocationMenuItem().addActionListener(e -> { + Optional optional = getSelectedPack(false); + + if (optional.isPresent()) { + Pack pack = optional.get(); + + JFileChooser chooser = new JFileChooser(); + chooser.setDialogTitle("Choose New Folder for Pack"); + chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + chooser.setFileFilter(new PackDirectoryFilter()); + + File dir = workspaceDir; + + do { + chooser.setCurrentDirectory(dir); + int returnVal = chooser.showOpenDialog(frame); + + if (returnVal == JFileChooser.APPROVE_OPTION) { + dir = chooser.getSelectedFile(); + } else { + return; + } + } while (!canAddPackDir(dir)); + + pack.setLocation(dir.getAbsolutePath()); + updatePackInWorkspace(pack); + } + }); + + frame.getRefreshMenuItem().addActionListener(e -> loadWorkspace()); + + frame.getQuitMenuItem().addActionListener(e -> { + frame.dispose(); + }); + + frame.getEditConfigMenuItem().addActionListener(e -> { + Optional optional = getSelectedPack(true); + + if (optional.isPresent()) { + Pack pack = optional.get(); + File file = pack.getConfigFile(); + BuilderConfig config = Persistence.read(file, BuilderConfig.class); + + if (BuilderConfigDialog.showEditor(frame, config)) { + writeBuilderConfig(pack, config); + updatePackInWorkspace(pack); + } + } + }); + + frame.getOpenFolderMenuItem().addActionListener(e -> { + Optional selectedPack = getSelectedPack(true); + + selectedPack.ifPresent(pack -> SwingHelper.browseDir(pack.getDirectory(), frame)); + }); + + frame.getCheckProblemsMenuItem().addActionListener(e -> { + Optional optional = getSelectedPack(true); + + if (optional.isPresent()) { + ProblemChecker checker = new ProblemChecker(optional.get()); + Deferred deferred = Deferreds.makeDeferred(executor.submit(checker), executor) + .handleAsync(this::showProblems, (ex) -> { + }, SwingExecutor.INSTANCE); + SwingHelper.addErrorDialogCallback(frame, deferred); + } + }); + + frame.getTestMenuItem().addActionListener(e -> { + Optional optional = getSelectedPack(true); + + if (optional.isPresent()) { + Pack pack = optional.get(); + startTest(pack, false); + } + }); + + frame.getTestOnlineMenuItem().addActionListener(e -> { + Optional optional = getSelectedPack(true); + + if (optional.isPresent()) { + Pack pack = optional.get(); + startTest(pack, true); + } + }); + + frame.getOptionsMenuItem().addActionListener(e -> { + ConfigurationDialog configDialog = new ConfigurationDialog(frame, launcher); + 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); + ProgressDialog.showProgress(frame, deferred, deleter, "Deleting test instances...", "Deleting test instances..."); + SwingHelper.addErrorDialogCallback(frame, deferred); + }); + + frame.getClearWebRootMenuItem().addActionListener(e -> { + DirectoryDeleter deleter = new DirectoryDeleter(webRoot); + Deferred deferred = Deferreds.makeDeferred(executor.submit(deleter), executor); + ProgressDialog.showProgress(frame, deferred, deleter, "Deleting web server files...", "Deleting web server files..."); + SwingHelper.addErrorDialogCallback(frame, deferred); + }); + + frame.getBuildMenuItem().addActionListener(e -> { + Optional optional = getSelectedPack(true); + + if (optional.isPresent()) { + Pack pack = optional.get(); + buildPack(pack); + } + }); + + frame.getDeployServerMenuItem().addActionListener(e -> { + Optional optional = getSelectedPack(true); + + if (optional.isPresent()) { + Pack pack = optional.get(); + DeployOptions options = DeployServerDialog.showDeployDialog(frame, lastServerDestDir); + + if (options != null) { + ConsoleFrame.showMessages(); + + File destDir = options.getDestDir(); + destDir.mkdirs(); + lastServerDestDir = destDir; + + ServerDeploy deploy = new ServerDeploy(pack.getSourceDir(), options); + Deferred deferred = Deferreds.makeDeferred(executor.submit(deploy), executor) + .handleAsync(r -> SwingHelper.showMessageDialog(frame, "Server deployment complete!", "Success", null, JOptionPane.INFORMATION_MESSAGE), + ex -> { + }, + SwingExecutor.INSTANCE); + ProgressDialog.showProgress(frame, deferred, deploy, "Deploying files...", "Deploying server files..."); + SwingHelper.addErrorDialogCallback(frame, deferred); + } + } + }); + + frame.getGeneratePackagesMenuItem().addActionListener(e -> { + List entries = workspace.getPackageListingEntries(); + ManifestInfoEnumerator enumerator = new ManifestInfoEnumerator(distDir); + Deferred deferred = Deferreds.makeDeferred(executor.submit(() -> enumerator.apply(entries))) + .handleAsync(loaded -> { + GenerateListingDialog dialog = new GenerateListingDialog(frame); + GenerateListingController controller = new GenerateListingController(dialog, workspace, loaded, executor); + controller.setOutputDir(distDir); + controller.show(); + }, ex -> { + }, SwingExecutor.INSTANCE); + ProgressDialog.showProgress(frame, deferred, new SettableProgress("Searching...", -1), "Searching for manifests...", "Searching for manifests..."); + SwingHelper.addErrorDialogCallback(frame, deferred); + }); + + frame.getVersionCheckMenuItem().addActionListener(e -> { + Optional optional = getSelectedPack(true); + + if (optional.isPresent()) { + Pack pack = optional.get(); + + VersionCheckDialog dialog = new VersionCheckDialog(frame); + VersionCheckController controller = new VersionCheckController(dialog, executor); + controller.showUpdates(pack.getModsDir(), pack.getCachedConfig().getGameVersion(), frame); + } + }); + + frame.getOpenOutputFolderMenuItem().addActionListener(e -> SwingHelper.browseDir(distDir, frame)); + + frame.getOpenWorkspaceFolderMenuItem().addActionListener(e1 -> SwingHelper.browseDir(workspaceDir, frame)); + + frame.getOpenLauncherFolderMenuItem().addActionListener(e1 -> SwingHelper.browseDir(launcherDir, frame)); + + frame.getOpenWebRootMenuItem().addActionListener(e1 -> SwingHelper.browseDir(webRoot, frame)); + + frame.getOpenConsoleMenuItem().addActionListener(e -> { + ConsoleFrame.showMessages(); + }); + + frame.getDocsMenuItem().addActionListener(e -> { + SwingHelper.openURL("https://github.com/SKCraft/Launcher/wiki", frame); + }); + + frame.getAboutMenuItem().addActionListener(e -> { + AboutDialog.showAboutDialog(frame); + }); + + SwingHelper.addActionListeners(frame.getNewPackButton(), frame.getNewPackMenuItem().getActionListeners()); + SwingHelper.addActionListeners(frame.getImportButton(), frame.getImportPackMenuItem().getActionListeners()); + SwingHelper.addActionListeners(frame.getEditConfigButton(), frame.getEditConfigMenuItem().getActionListeners()); + SwingHelper.addActionListeners(frame.getOpenFolderButton(), frame.getOpenFolderMenuItem().getActionListeners()); + SwingHelper.addActionListeners(frame.getCheckProblemsButton(), frame.getCheckProblemsMenuItem().getActionListeners()); + SwingHelper.addActionListeners(frame.getTestButton(), frame.getTestMenuItem().getActionListeners()); + SwingHelper.addActionListeners(frame.getBuildButton(), frame.getBuildMenuItem().getActionListeners()); + } + + private void popupPackMenu(Component component, int x, int y, Pack pack) { + JPopupMenu popup = new JPopupMenu(); + JMenuItem menuItem; + + menuItem = new JMenuItem("Edit modpack.json..."); + menuItem.addActionListener(e -> frame.getEditConfigMenuItem().doClick()); + popup.add(menuItem); + + menuItem = new JMenuItem("Open Directory"); + menuItem.addActionListener(e -> frame.getOpenFolderMenuItem().doClick()); + popup.add(menuItem); + + menuItem = new JMenuItem("Check for Problems"); + menuItem.addActionListener(e -> frame.getCheckProblemsMenuItem().doClick()); + popup.add(menuItem); + + popup.addSeparator(); + + menuItem = new JMenuItem("Test"); + menuItem.addActionListener(e -> frame.getTestMenuItem().doClick()); + popup.add(menuItem); + + menuItem = new JMenuItem("Test Online"); + 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); + + menuItem = new JMenuItem("Deploy Server..."); + menuItem.addActionListener(e -> frame.getDeployServerMenuItem().doClick()); + popup.add(menuItem); + + popup.addSeparator(); + + menuItem = new JMenuItem("Change Location..."); + menuItem.addActionListener(e -> frame.getChangePackLocationMenuItem().doClick()); + popup.add(menuItem); + + menuItem = new JMenuItem("Remove..."); + menuItem.addActionListener(e -> frame.getRemovePackItem().doClick()); + popup.add(menuItem); + + menuItem = new JMenuItem("Delete Forever..."); + menuItem.addActionListener(e -> frame.getDeletePackItem().doClick()); + popup.add(menuItem); + + popup.show(component, x, y); + } + + private void tryAddPackViaDialog() { + BuilderConfig config = new BuilderConfig(); + addDefaultConfig(config); + Pack pack = new Pack(); + File dir; + + do { + if (BuilderConfigDialog.showEditor(frame, config)) { + dir = new File(workspaceDir, config.getName()); + } else { + return; + } + } while (!canAddPackDir(dir)); + + pack.setLocation(dir.getAbsolutePath()); + + if (pack.getConfigFile().exists()) { + if (SwingHelper.confirmDialog(frame, "There's already an existing modpack with that name, though " + + "it is not imported into this workspace. Would you like to import it and ignore " + + "the new modpack that you just entered?", "Conflict")) { + addPackToWorkspace(pack); + } + } else { + if (writeBuilderConfig(pack, config)) { + pack.createGuideFolders(); + addPackToWorkspace(pack); + } + } + } + + private void tryAddPackViaDirectory(boolean createNew) { + JFileChooser chooser = new JFileChooser(); + chooser.setDialogTitle("Choose Folder for Pack"); + chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + chooser.setFileFilter(new PackDirectoryFilter()); + + File dir = workspaceDir; + + do { + chooser.setCurrentDirectory(dir); + int returnVal = chooser.showOpenDialog(frame); + + if (returnVal == JFileChooser.APPROVE_OPTION) { + dir = chooser.getSelectedFile(); + } else { + return; + } + } while (!canAddPackDir(dir)); + + Pack pack = new Pack(); + pack.setLocation(dir.getAbsolutePath()); + + if (pack.getConfigFile().exists()) { + if (createNew) { + if (SwingHelper.confirmDialog(frame, "There's already a modpack in that directory. Do you want to import it instead?", "Exists Already")) { + addPackToWorkspace(pack); + } + } else { + addPackToWorkspace(pack); + } + } else if (createNew || SwingHelper.confirmDialog(frame, "You've selected a directory that doesn't seem to be a modpack " + + "(at least with the directory structure this program expects). Would you like to create the files necessary to turn " + + "that folder into a modpack?", "Import Error")) { + + BuilderConfig config = new BuilderConfig(); + addDefaultConfig(config); + + if (BuilderConfigDialog.showEditor(frame, config)) { + if (writeBuilderConfig(pack, config)) { + pack.createGuideFolders(); + addPackToWorkspace(pack); + } + } + } + } + + private void startTest(Pack pack, boolean online) { + Session session; + + if (online) { + session = AccountSelectDialog.showAccountRequest(frame, launcher); + if (session == null) { + return; + } + } else { + if (!isOfflineEnabled()) { + return; + } + + session = new OfflineSession("Player"); + } + + String version = generateVersionFromDate(); + + PackBuilder builder = new PackBuilder(pack, webRoot, version, "staging.json", false, false); + InstanceList.Enumerator enumerator = launcher.getInstances().createEnumerator(); + TestLauncher instanceLauncher = new TestLauncher(launcher, frame, pack.getCachedConfig().getName(), session); + + SettableProgress progress = new SettableProgress(builder); + + ConsoleFrame.showMessages(); + + Deferred deferred = Deferreds.makeDeferred(executor.submit(builder), executor) + .thenTap(() -> progress.set("Loading instance in test launcher...", -1)) + .thenRun(enumerator) + .thenTap(() -> progress.set("Launching", -1)) + .thenApply(instanceLauncher) + .handleAsync(result -> ConsoleFrame.hideMessages(), ex -> {}, SwingExecutor.INSTANCE); + + ProgressDialog.showProgress(frame, deferred, progress, "Setting up test instance...", "Preparing files for launch..."); + SwingHelper.addErrorDialogCallback(frame, deferred); + } + + private void buildPack(Pack pack) { + String initialVersion = generateVersionFromDate(); + BuildOptions options = BuildDialog.showBuildDialog(frame, initialVersion, generateManifestName(pack), distDir); + + if (options != null) { + ConsoleFrame.showMessages(); + PackBuilder builder = new PackBuilder(pack, options.getDestDir(), options.getVersion(), options.getManifestFilename(), false, true); + Deferred deferred = Deferreds.makeDeferred(executor.submit(builder), executor) + .handleAsync(result -> { + ConsoleFrame.hideMessages(); + SwingHelper.showMessageDialog(frame, "Successfully generated the package files.", "Success", null, JOptionPane.INFORMATION_MESSAGE); + }, ex -> {}, SwingExecutor.INSTANCE); + ProgressDialog.showProgress(frame, deferred, builder, "Building modpack...", "Building modpack..."); + SwingHelper.addErrorDialogCallback(frame, deferred); + } + } + + private void showProblems(List problems) { + if (problems.isEmpty()) { + SwingHelper.showMessageDialog(frame, "No potential problems found!", "Success", null, JOptionPane.INFORMATION_MESSAGE); + } else { + ProblemViewer viewer = new ProblemViewer(frame, problems); + viewer.setVisible(true); + } + } + + public String generateManifestName(Pack pack) { + File file = pack.getConfigFile(); + if (file.exists()) { + BuilderConfig config = Persistence.read(file, BuilderConfig.class, true); + if (config != null) { + String name = Strings.nullToEmpty(config.getName()); + name = name.toLowerCase(); + name = FILENAME_SANITIZE.matcher(name).replaceAll("-"); + name = name.trim(); + if (!name.isEmpty()) { + return name + ".json"; + } + } + } + + return "my_modpack.json"; + } + + public static String generateVersionFromDate() { + Date today = Calendar.getInstance().getTime(); + return VERSION_DATE_FORMAT.format(today); + } + +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/controller/VersionCheckController.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/controller/VersionCheckController.java new file mode 100644 index 0000000..cfcaddb --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/controller/VersionCheckController.java @@ -0,0 +1,171 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.controller; + +import com.google.common.collect.Lists; +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.creator.controller.task.DirectoryWalker; +import com.skcraft.launcher.creator.dialog.VersionCheckDialog; +import com.skcraft.launcher.creator.model.creator.ModFile; +import com.skcraft.launcher.creator.model.swing.ModFileTableModel; +import com.skcraft.launcher.creator.util.ModInfoReader; +import com.skcraft.launcher.creator.util.ModInfoReader.ModInfo; +import com.skcraft.launcher.creator.util.NemModList; +import com.skcraft.launcher.creator.util.NemModList.ModEntry; +import com.skcraft.launcher.dialog.ProgressDialog; +import com.skcraft.launcher.swing.SwingHelper; +import com.skcraft.launcher.util.SwingExecutor; + +import javax.annotation.Nullable; +import javax.swing.*; +import java.awt.*; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.List; + +public class VersionCheckController { + + private final VersionCheckDialog dialog; + private final ListeningExecutorService executor; + + public VersionCheckController(VersionCheckDialog dialog, ListeningExecutorService executor) { + this.dialog = dialog; + this.executor = executor; + } + + public void showUpdates(File dir, String gameVersion, Window parentWindow) { + initListeners(); + + DirectoryWalker walker = new DirectoryWalker(dir); + walker.setRecursive(false); + walker.setFileFilter(pathname -> pathname.getName().endsWith(".jar")); + + ModInfoReader binaryInspector = new ModInfoReader(); + NemModList nemModList = new NemModList(); + + SettableProgress progress = new SettableProgress("Retrieving mod information...", -1); + + Deferred deferred = Deferreds.makeDeferred(executor.submit(walker), executor) + .thenTap(() -> progress.set("Querying NotEnoughMods for version data...", -1)) + .thenTap(() -> { + try { + nemModList.load(gameVersion); + } catch (IOException | InterruptedException e) { + throw new RuntimeException("Failed to retrieve mod information from NotEnoughMods. Perhaps NEM doesn't support your pack's MC version.", e); + } + }) + .thenTap(() -> progress.set("Scanning mod files for manifests...", -1)) + .thenApply(files -> { + List mods = Lists.newArrayList(); + + for (File file : files) { + ModFile mod = new ModFile(); + mod.setGameVersion(gameVersion); + mod.setFile(file); + + List infoList = binaryInspector.detectMods(file); + if (!infoList.isEmpty()) { + ModInfo info = infoList.get(0); + mod.setModId(info.getModId()); + mod.setName(info.getName()); + mod.setVersion(info.getVersion()); + + if (info.getUrl() != null) { + mod.setUrl(getFirstUrl(info.getUrl(), "http://" + info.getUrl())); + } + } + + if (mod.getModId() != null) { + ModEntry entry = nemModList.get(mod.getModId()); + + if (entry != null) { + mod.setLatestVersion(entry.getLatestVersion()); + mod.setLatestDevVersion(entry.getLatestDevVersion()); + if (entry.getUrl() != null) { + mod.setUrl(entry.getUrl()); + } + } + } + + mods.add(mod); + } + + return mods; + }) + .handleAsync(mods -> { + List known = Lists.newArrayList(); + List unknown = Lists.newArrayList(); + + for (ModFile mod : mods) { + if (mod.getVersion() != null && mod.getLatestVersion() != null) { + if (!mod.getCleanVersion().equals(mod.getLatestVersion()) && !mod.getCleanVersion().equals(mod.getLatestDevVersion())) { + known.add(mod); + } + } else { + unknown.add(mod); + } + } + + dialog.getKnownModsTable().setModel(new ModFileTableModel(known)); + dialog.getUnknownModsTable().setModel(new ModFileTableModel(unknown)); + dialog.getKnownModsTable().getRowSorter().toggleSortOrder(1); + dialog.getUnknownModsTable().getRowSorter().toggleSortOrder(1); + dialog.setVisible(true); + }, ex -> { + }, SwingExecutor.INSTANCE); + + ProgressDialog.showProgress(parentWindow, deferred, progress, "Checking for mod updates...", "Checking for mod updates..."); + SwingHelper.addErrorDialogCallback(parentWindow, deferred); + } + + private void initListeners() { + dialog.getCloseButton().addActionListener(e -> dialog.dispose()); + + ModTableMouseListener mouseListener = new ModTableMouseListener(); + dialog.getKnownModsTable().addMouseListener(mouseListener); + dialog.getUnknownModsTable().addMouseListener(mouseListener); + } + + @Nullable + private static URL getFirstUrl(String... options) { + for (String option : options) { + try { + return new URL(option); + } catch (MalformedURLException ignored) { + } + } + + return null; + } + + private class ModTableMouseListener extends MouseAdapter { + + public void mousePressed(MouseEvent e) { + if (e.getClickCount() == 2) { + JTable table = (JTable) e.getSource(); + Point point = e.getPoint(); + int selectedIndex = table.rowAtPoint(point); + if (selectedIndex >= 0) { + selectedIndex = table.convertRowIndexToModel(selectedIndex); + ModFile mod = ((ModFileTableModel) table.getModel()).getMod(selectedIndex); + if (mod != null && mod.getUrl() != null) { + SwingHelper.openURL(mod.getUrl(), dialog); + } + } + } + } + + } + +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/controller/WelcomeController.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/controller/WelcomeController.java new file mode 100644 index 0000000..6a08779 --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/controller/WelcomeController.java @@ -0,0 +1,212 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.controller; + +import com.google.common.base.Optional; +import com.google.common.collect.Lists; +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.creator.Creator; +import com.skcraft.launcher.creator.dialog.AboutDialog; +import com.skcraft.launcher.creator.dialog.PackManagerFrame; +import com.skcraft.launcher.creator.dialog.WelcomeDialog; +import com.skcraft.launcher.creator.model.creator.RecentEntry; +import com.skcraft.launcher.creator.model.creator.Workspace; +import com.skcraft.launcher.creator.model.swing.RecentListModel; +import com.skcraft.launcher.creator.swing.WorkspaceDirectoryFilter; +import com.skcraft.launcher.dialog.ProgressDialog; +import com.skcraft.launcher.persistence.Persistence; +import com.skcraft.launcher.swing.PopupMouseAdapter; +import com.skcraft.launcher.swing.SwingHelper; +import com.skcraft.launcher.util.MorePaths; +import com.skcraft.launcher.util.SwingExecutor; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.stream.Collectors; + +public class WelcomeController { + + private final WelcomeDialog dialog; + private final Creator creator; + private final RecentListModel recentListModel; + + public WelcomeController(WelcomeDialog dialog, Creator creator) { + this.dialog = dialog; + this.creator = creator; + + initListeners(); + + recentListModel = new RecentListModel(creator.getConfig().getRecentEntries()); + dialog.getRecentList().setModel(recentListModel); + } + + public void show() { + dialog.setVisible(true); + } + + private boolean openWorkspace(File dir) { + ListeningExecutorService executor = creator.getExecutor(); + PackManagerFrame frame = new PackManagerFrame(); + Deferred deferred = Deferreds.makeDeferred(executor.submit(() -> { + PackManagerController controller = new PackManagerController(frame, dir, creator); + addRecentEntry(dir); + return controller; + }), executor) + .handleAsync(PackManagerController::show, ex -> {}, SwingExecutor.INSTANCE); + ProgressDialog.showProgress(frame, deferred, new SettableProgress("Loading...", -1), "Loading workspace...", "Loading workspace..."); + SwingHelper.addErrorDialogCallback(frame, deferred); + + return true; + } + + private void addRecentEntry(File dir) { + List newEntries = creator.getConfig().getRecentEntries() + .stream() + .filter(entry -> { + try { + return !MorePaths.isSamePath(entry.getPath(), dir); + } catch (IOException ignored) { + return false; + } + }) + .collect(Collectors.toCollection(Lists::newArrayList)); + + RecentEntry recent = new RecentEntry(); + recent.setPath(dir); + newEntries.add(0, recent); + + creator.getConfig().setRecentEntries(newEntries); + + Persistence.commitAndForget(creator.getConfig()); + + recentListModel.fireUpdate(); + } + + private void removeRecentEntry(RecentEntry entry) { + creator.getConfig().getRecentEntries().remove(entry); + Persistence.commitAndForget(creator.getConfig()); + + recentListModel.fireUpdate(); + } + + private Optional getSelectedRecentEntry() { + int selectedIndex = dialog.getRecentList().getSelectedIndex(); + if (selectedIndex >= 0) { + return Optional.fromNullable(creator.getConfig().getRecentEntries().get(selectedIndex)); + } else { + return Optional.absent(); + } + } + + private Optional getWorkspaceDir() { + JFileChooser chooser = new JFileChooser(); + chooser.setDialogTitle("Select Workspace Directory"); + chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + chooser.setFileFilter(new WorkspaceDirectoryFilter()); + + int returnVal = chooser.showOpenDialog(dialog); + + if (returnVal == JFileChooser.APPROVE_OPTION) { + return Optional.fromNullable(chooser.getSelectedFile()); + } else { + return Optional.absent(); + } + } + + private void initListeners() { + dialog.getRecentList().addMouseListener(new MouseAdapter() { + public void mousePressed(MouseEvent e) { + if (e.getClickCount() == 2) { + JList table = (JList) e.getSource(); + Point point = e.getPoint(); + int selectedIndex = table.locationToIndex(point); + if (selectedIndex >= 0) { + table.setSelectedIndex(selectedIndex); + Optional optional = getSelectedRecentEntry(); + if (optional.isPresent()) { + if (openWorkspace(optional.get().getPath())) { + dialog.dispose(); + } + } + } + } + } + }); + + dialog.getRecentList().addMouseListener(new PopupMouseAdapter() { + @Override + protected void showPopup(MouseEvent e) { + JList table = (JList) e.getSource(); + Point point = e.getPoint(); + int selectedIndex = table.locationToIndex(point); + if (selectedIndex >= 0) { + table.setSelectedIndex(selectedIndex); + Optional optional = getSelectedRecentEntry(); + if (optional.isPresent()) { + popupRecentWorkspaceMenu(e.getComponent(), e.getX(), e.getY(), optional.get()); + } + } + } + }); + + dialog.getNewButton().addActionListener(e -> { + Optional optional = getWorkspaceDir(); + if (optional.isPresent()) { + File workspaceFile = Workspace.getWorkspaceFile(optional.get()); + if (!workspaceFile.exists() || SwingHelper.confirmDialog(dialog, "There is already a workspace there. Do you want to load it?", "Existing")) { + if (openWorkspace(optional.get())) { + dialog.dispose(); + } + } + } + }); + + dialog.getOpenButton().addActionListener(e -> { + Optional optional = getWorkspaceDir(); + if (optional.isPresent()) { + File workspaceFile = Workspace.getWorkspaceFile(optional.get()); + if (workspaceFile.exists() || SwingHelper.confirmDialog(dialog, "Do you want to create a new workspace there?", "Create New")) { + if (openWorkspace(optional.get())) { + dialog.dispose(); + } + } + } + }); + + dialog.getHelpButton().addActionListener(e -> { + SwingHelper.openURL("https://github.com/SKCraft/Launcher/wiki", dialog); + }); + + dialog.getAboutButton().addActionListener(e -> { + AboutDialog.showAboutDialog(dialog); + }); + + dialog.getQuitButton().addActionListener(e -> { + dialog.dispose(); + }); + } + + private void popupRecentWorkspaceMenu(Component component, int x, int y, RecentEntry entry) { + JPopupMenu popup = new JPopupMenu(); + JMenuItem menuItem; + + menuItem = new JMenuItem("Remove"); + menuItem.addActionListener(e -> removeRecentEntry(entry)); + popup.add(menuItem); + + popup.show(component, x, y); + } + +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/controller/task/DirectoryDeleter.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/controller/task/DirectoryDeleter.java new file mode 100644 index 0000000..99141dd --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/controller/task/DirectoryDeleter.java @@ -0,0 +1,57 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.controller.task; + +import com.skcraft.concurrency.ProgressObservable; +import com.skcraft.launcher.LauncherException; +import com.skcraft.launcher.LauncherUtils; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; + +public class DirectoryDeleter implements Callable, ProgressObservable { + + private final File dir; + + public DirectoryDeleter(File dir) { + this.dir = dir; + } + + @Override + public File call() throws Exception { + Thread.sleep(2000); + + List failures = new ArrayList(); + + try { + LauncherUtils.interruptibleDelete(dir, failures); + } catch (IOException e) { + Thread.sleep(1000); + LauncherUtils.interruptibleDelete(dir, failures); + } + + if (failures.size() > 0) { + throw new LauncherException(failures.size() + " failed to delete", failures.size() + " file(s) could not be deleted"); + } + + return dir; + } + + @Override + public double getProgress() { + return -1; + } + + @Override + public String getStatus() { + return "Deleting files..."; + } + +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/controller/task/DirectoryWalker.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/controller/task/DirectoryWalker.java new file mode 100644 index 0000000..89eaeae --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/controller/task/DirectoryWalker.java @@ -0,0 +1,73 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.controller.task; + +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import com.skcraft.launcher.util.MorePaths; +import lombok.Getter; +import lombok.Setter; + +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.Callable; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class DirectoryWalker implements Callable> { + + @Getter private final File dir; + @Getter @Setter private FileFilter fileFilter = pathname -> true; + @Getter @Setter private boolean recursive; + + public DirectoryWalker(File dir) { + checkNotNull(dir, "dir"); + this.dir = dir; + } + + @Override + public List call() throws IOException { + if (!dir.isDirectory()) { + throw new IllegalArgumentException(dir.getAbsolutePath() + " is not a directory"); + } + + List matched = Lists.newArrayList(); + Set seen = Sets.newHashSet(); + + Queue queue = new LinkedList<>(); + queue.add(dir); + + File cur; + while ((cur = queue.poll()) != null) { + String canonical = cur.getCanonicalPath(); + if (!seen.contains(canonical) && MorePaths.isSubDirectory(dir, cur)) { + File[] files = cur.listFiles(); + + if (files != null) { + for (File file : files) { + if (recursive && file.isDirectory()) { + queue.add(file); + } + + if (fileFilter.accept(file)) { + matched.add(file); + } + } + } + } + + } + + return matched; + } + +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/controller/task/ManifestInfoEnumerator.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/controller/task/ManifestInfoEnumerator.java new file mode 100644 index 0000000..86f6c0f --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/controller/task/ManifestInfoEnumerator.java @@ -0,0 +1,66 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.controller.task; + +import com.skcraft.launcher.creator.model.creator.ManifestEntry; +import com.skcraft.launcher.model.modpack.Manifest; +import com.skcraft.launcher.model.modpack.ManifestInfo; +import com.skcraft.launcher.persistence.Persistence; + +import java.io.File; +import java.util.List; +import java.util.function.Function; + +public class ManifestInfoEnumerator implements Function, List> { + + private final File searchDir; + + public ManifestInfoEnumerator(File searchDir) { + this.searchDir = searchDir; + } + + @Override + public List apply(List entries) { + File[] files = searchDir.listFiles(f -> f.isFile() && f.getName().toLowerCase().endsWith(".json") && !f.getName().startsWith("packages.")); + + if (files != null) { + for (File file : files) { + String location = file.getName(); + Manifest manifest = Persistence.read(file, Manifest.class, true); + + if (manifest != null) { + ManifestInfo info = new ManifestInfo(); + info.setName(manifest.getName()); + info.setTitle(manifest.getTitle()); + info.setVersion(manifest.getVersion()); + info.setPriority(0); + info.setLocation(location); + + boolean found = false; + + for (ManifestEntry entry : entries) { + if (entry.getManifestInfo().getLocation().equals(location)) { + info.setPriority(entry.getManifestInfo().getPriority()); + entry.setManifestInfo(info); + found = true; + break; + } + } + + if (!found) { + ManifestEntry entry = new ManifestEntry(); + entry.setManifestInfo(info); + entries.add(entry); + } + } + } + } + + return entries; + } + +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/controller/task/PackBuilder.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/controller/task/PackBuilder.java new file mode 100644 index 0000000..365bf0d --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/controller/task/PackBuilder.java @@ -0,0 +1,80 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.controller.task; + +import com.skcraft.concurrency.ProgressObservable; +import com.skcraft.launcher.LauncherException; +import com.skcraft.launcher.LauncherUtils; +import com.skcraft.launcher.builder.PackageBuilder; +import com.skcraft.launcher.creator.model.creator.Pack; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; + +public class PackBuilder implements Callable, ProgressObservable { + + private final Pack pack; + private final File outputDir; + private final String version; + private final String manifestFilename; + private final boolean clean; + private final boolean downloadUrls; + + public PackBuilder(Pack pack, File outputDir, String version, String manifestFilename, boolean clean, boolean downloadUrls) { + this.pack = pack; + this.outputDir = outputDir; + this.version = version; + this.manifestFilename = manifestFilename; + this.clean = clean; + this.downloadUrls = downloadUrls; + } + + @Override + public PackBuilder call() throws Exception { + if (clean) { + List failures = new ArrayList(); + + try { + LauncherUtils.interruptibleDelete(outputDir, failures); + } catch (IOException e) { + Thread.sleep(1000); + LauncherUtils.interruptibleDelete(outputDir, failures); + } + + if (failures.size() > 0) { + throw new LauncherException(failures.size() + " failed to delete", "There were " + failures.size() + " failures during cleaning."); + } + } + + //noinspection ResultOfMethodCallIgnored + outputDir.mkdirs(); + + System.setProperty("com.skcraft.builder.ignoreURLOverrides", downloadUrls ? "false" : "true"); + String[] args = { + "--version", version, + "--manifest-dest", new File(outputDir, manifestFilename).getAbsolutePath(), + "-i", pack.getDirectory().getAbsolutePath(), + "-o", outputDir.getAbsolutePath() + }; + PackageBuilder.main(args); + + return this; + } + + @Override + public double getProgress() { + return -1; + } + + @Override + public String getStatus() { + return "Building modpack..."; + } +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/controller/task/PackLoader.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/controller/task/PackLoader.java new file mode 100644 index 0000000..df09771 --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/controller/task/PackLoader.java @@ -0,0 +1,57 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.controller.task; + +import com.google.common.base.Function; +import com.skcraft.concurrency.ProgressObservable; +import com.skcraft.launcher.creator.model.creator.Pack; +import com.skcraft.launcher.creator.model.creator.Workspace; + +import java.util.List; + +public class PackLoader implements ProgressObservable, Function> { + + private int index; + private int size = 0; + private Pack lastPack; + + @Override + public List apply(Workspace workspace) { + List packs = workspace.getPacks(); + size = packs.size(); + + for (Pack pack : packs) { + lastPack = pack; + pack.load(); + index++; + } + + lastPack = null; + + return packs; + } + + @Override + public double getProgress() { + if (size == 0) { + return -1; + } else { + return index / (double) size; + } + } + + @Override + public String getStatus() { + Pack pack = lastPack; + if (pack != null) { + return "Loading " + pack.getDirectory().getName() + "..."; + } else { + return "Enumerating packs..."; + } + } + +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/controller/task/ProblemChecker.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/controller/task/ProblemChecker.java new file mode 100644 index 0000000..4f14835 --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/controller/task/ProblemChecker.java @@ -0,0 +1,95 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.controller.task; + +import com.google.common.collect.Lists; +import com.skcraft.concurrency.ProgressObservable; +import com.skcraft.launcher.builder.BuilderOptions; +import com.skcraft.launcher.creator.model.creator.Pack; +import com.skcraft.launcher.creator.model.creator.Problem; + +import java.io.File; +import java.util.List; +import java.util.concurrent.Callable; + +public class ProblemChecker implements Callable>, ProgressObservable { + + private final Pack pack; + + public ProblemChecker(Pack pack) { + this.pack = pack; + } + + @Override + public List call() throws Exception { + List problems = Lists.newArrayList(); + + File packDir = pack.getDirectory(); + File srcDir = pack.getSourceDir(); + + File loadersDir = new File(packDir, BuilderOptions.DEFAULT_LOADERS_DIRNAME); + File modsDir = new File(srcDir, "mods"); + boolean hasLoaders = hasFiles(loadersDir); + boolean hasMods = hasFiles(modsDir); + + String[] files; + + if (new File(packDir, "_CLIENT").exists()) { + problems.add(new Problem("Root _CLIENT", "There's a _CLIENT folder that's not in " + + "the src/ folder. Only files that are in src/ will actually appear in the " + + "modpack, so you probably intended to put _CLIENT in src/.")); + } + + if (new File(packDir, "_SERVER").exists()) { + problems.add(new Problem("Root _SERVER", "There's a _SERVER folder that's not in " + + "the src/ folder. Only files that are in src/ will actually appear in the " + + "modpack, so you probably intended to put _SERVER in src/.")); + } + + if (new File(packDir, "mods").exists()) { + problems.add(new Problem("Root mods", "There's a mods folder that's not in " + + "the src/ folder. Only files that are in src/ will actually appear in the " + + "modpack.")); + } + + if (new File(packDir, "config").exists()) { + problems.add(new Problem("Root mods", "There's a config folder that's not in " + + "the src/ folder. Only files that are in src/ will actually appear in the " + + "modpack.")); + } + + if (new File(packDir, "version.json").exists()) { + problems.add(new Problem("Legacy version.json", "There's a version.json file in the " + + "project directory. If you are upgrading your modpack from an old version " + + "of the launcher, then you should be able to delete version.json as it is " + + "no longer needed to create a modpack. If you are intentionally overriding the " + + "Minecraft version manifest, then ignore this warning.")); + } + + if (hasMods && !hasLoaders) { + problems.add(new Problem("No Loaders", "There appears to be a mods/ folder but there's no mod loaders in loaders/.")); + } + + return problems; + } + + @Override + public double getProgress() { + return -1; + } + + @Override + public String getStatus() { + return "Checking for problems..."; + } + + private static boolean hasFiles(File dir) { + String[] contents = dir.list(); + return contents != null && contents.length > 0; + } + +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/controller/task/ServerDeploy.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/controller/task/ServerDeploy.java new file mode 100644 index 0000000..d392833 --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/controller/task/ServerDeploy.java @@ -0,0 +1,69 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.controller.task; + +import com.skcraft.concurrency.ProgressObservable; +import com.skcraft.launcher.LauncherException; +import com.skcraft.launcher.LauncherUtils; +import com.skcraft.launcher.builder.ServerCopyExport; +import com.skcraft.launcher.creator.dialog.DeployServerDialog.DeployOptions; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; + +public class ServerDeploy implements Callable, ProgressObservable { + + private final File srcDir; + private final DeployOptions options; + + public ServerDeploy(File srcDir, DeployOptions options) { + this.srcDir = srcDir; + this.options = options; + } + + @Override + public ServerDeploy call() throws Exception { + File modsDir = new File(options.getDestDir(), "mods"); + + if (options.isCleanMods() && modsDir.isDirectory()) { + List failures = new ArrayList(); + + try { + LauncherUtils.interruptibleDelete(modsDir, failures); + } catch (IOException e) { + Thread.sleep(1000); + LauncherUtils.interruptibleDelete(modsDir, failures); + } + + if (failures.size() > 0) { + throw new LauncherException(failures.size() + " failed to delete", "There were " + failures.size() + " failures during cleaning."); + } + } + + String[] args = { + "--source", srcDir.getAbsolutePath(), + "--dest", options.getDestDir().getAbsolutePath() + }; + ServerCopyExport.main(args); + + return this; + } + + @Override + public double getProgress() { + return -1; + } + + @Override + public String getStatus() { + return "Deploying server files..."; + } + +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/controller/task/TestLauncher.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/controller/task/TestLauncher.java new file mode 100644 index 0000000..f96e983 --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/controller/task/TestLauncher.java @@ -0,0 +1,81 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.controller.task; + +import com.google.common.base.Function; +import com.google.common.base.Optional; +import com.skcraft.concurrency.ProgressObservable; +import com.skcraft.launcher.Instance; +import com.skcraft.launcher.InstanceList; +import com.skcraft.launcher.Launcher; +import com.skcraft.launcher.auth.Session; +import com.skcraft.launcher.launch.LaunchOptions; +import com.skcraft.launcher.launch.LaunchOptions.UpdatePolicy; +import com.skcraft.launcher.swing.SwingHelper; + +import java.awt.*; +import java.util.List; + +public class TestLauncher implements Function, ProgressObservable { + + private final Launcher launcher; + private final Window window; + private final String id; + private final Session session; + + public TestLauncher(Launcher launcher, Window window, String id, Session session) { + this.launcher = launcher; + this.window = window; + this.id = id; + this.session = session; + } + + private Optional findInstance(List instances) { + for (Instance instance : instances) { + if (instance.getName().equals(id)) { + return Optional.fromNullable(instance); + } + } + + return Optional.absent(); + } + + @Override + public Instance apply(InstanceList instanceList) { + Optional optional = findInstance(instanceList.getInstances()); + + if (optional.isPresent()) { + LaunchOptions options = new LaunchOptions.Builder() + .setInstance(optional.get()) + .setUpdatePolicy(UpdatePolicy.ALWAYS_UPDATE) + .setWindow(window) + .setSession(session) + .build(); + + launcher.getLaunchSupervisor().launch(options); + + return optional.get(); + } else { + SwingHelper.showErrorDialog(window, + "After generating the necessary files, it appears the modpack can't be found in the " + + "launcher. Did you change modpack.json while the launcher was launching?", "Launch Error"); + + return null; + } + } + + @Override + public double getProgress() { + return -1; + } + + @Override + public String getStatus() { + return "Launching the game..."; + } + +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/dialog/AboutDialog.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/dialog/AboutDialog.java new file mode 100644 index 0000000..2885959 --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/dialog/AboutDialog.java @@ -0,0 +1,77 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.dialog; + +import com.skcraft.launcher.LauncherUtils; +import com.skcraft.launcher.creator.Creator; +import com.skcraft.launcher.swing.SwingHelper; +import lombok.extern.java.Log; +import net.miginfocom.swing.MigLayout; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.KeyEvent; +import java.io.IOException; +import java.util.Properties; +import java.util.logging.Level; + +@Log +public class AboutDialog extends JDialog { + + private String version; + + public AboutDialog(Window parent) { + super(parent, "About", ModalityType.DOCUMENT_MODAL); + + try { + Properties properties = LauncherUtils.loadProperties(Creator.class, "creator.properties", "com.skcraft.creator.propertiesFile"); + version = properties.getProperty("version", "????"); + } catch (IOException e) { + log.log(Level.WARNING, "Failed to get version", e); + version = "????"; + } + + setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + initComponents(); + setResizable(false); + pack(); + setLocationRelativeTo(parent); + } + + private void initComponents() { + JPanel container = new JPanel(); + container.setLayout(new MigLayout("insets dialog")); + + container.add(new JLabel(SwingHelper.createIcon(Creator.class, "about_header.png")), "dock north"); + container.add(new JLabel("Version " + version), "wrap"); + container.add(new JLabel("Licensed under GNU Lesser General Public License, version 3."), "wrap, gapbottom unrel"); + container.add(new JLabel("Created by the SKCraft team. Visit our website!"), "wrap, gapbottom unrel"); + + JButton okButton = new JButton("OK"); + JButton sourceCodeButton = new JButton("Source Code"); + JButton skCraftButton = new JButton("Website"); + + container.add(sourceCodeButton, "span, split 3, sizegroup bttn"); + container.add(skCraftButton, "sizegroup bttn"); + container.add(okButton, "tag ok, sizegroup bttn"); + + add(container, BorderLayout.CENTER); + + getRootPane().setDefaultButton(okButton); + getRootPane().registerKeyboardAction(e -> okButton.doClick(), KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW); + + okButton.addActionListener(e -> dispose()); + sourceCodeButton.addActionListener(e -> SwingHelper.openURL("https://github.com/SKCraft/Launcher", this)); + skCraftButton.addActionListener(e -> SwingHelper.openURL("http://www.skcraft.com", this)); + } + + public static void showAboutDialog(Window parent) { + AboutDialog dialog = new AboutDialog(parent); + dialog.setVisible(true); + } +} + diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/dialog/BuildDialog.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/dialog/BuildDialog.java new file mode 100644 index 0000000..3a8a036 --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/dialog/BuildDialog.java @@ -0,0 +1,109 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.dialog; + +import com.skcraft.launcher.swing.DirectoryField; +import com.skcraft.launcher.swing.SwingHelper; +import com.skcraft.launcher.swing.TextFieldPopupMenu; +import lombok.Data; +import lombok.Getter; +import net.miginfocom.swing.MigLayout; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.KeyEvent; +import java.io.File; + +public class BuildDialog extends JDialog { + + private final DirectoryField destDirField = new DirectoryField(); + private final JTextField versionText = new JTextField(20); + private final JTextField manifestFilenameText = new JTextField(30); + @Getter + private BuildOptions options; + + public BuildDialog(Window parent) { + super(parent, "Build Release", ModalityType.DOCUMENT_MODAL); + + setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + initComponents(); + setResizable(false); + pack(); + setLocationRelativeTo(parent); + } + + private void initComponents() { + versionText.setComponentPopupMenu(TextFieldPopupMenu.INSTANCE); + manifestFilenameText.setComponentPopupMenu(TextFieldPopupMenu.INSTANCE); + + JPanel container = new JPanel(); + container.setLayout(new MigLayout("insets dialog")); + + container.add(new JLabel("Version:")); + container.add(versionText, "span"); + + container.add(new JLabel("Manifest Filename:")); + container.add(manifestFilenameText, "span"); + + container.add(new JLabel("Output Directory:")); + container.add(destDirField, "span"); + + JButton buildButton = new JButton("Build"); + JButton cancelButton = new JButton("Cancel"); + + container.add(buildButton, "tag ok, span, split 2, sizegroup bttn"); + container.add(cancelButton, "tag cancel, sizegroup bttn"); + + add(container, BorderLayout.CENTER); + + getRootPane().setDefaultButton(buildButton); + getRootPane().registerKeyboardAction(e -> cancelButton.doClick(), KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW); + + buildButton.addActionListener(e -> returnValue()); + cancelButton.addActionListener(e -> dispose()); + } + + private void returnValue() { + String version = versionText.getText().trim(); + String manifestFilename = manifestFilenameText.getText().trim(); + + if (version.isEmpty()) { + SwingHelper.showErrorDialog(this, "A version string must be entered.", "Error"); + return; + } + + if (manifestFilename.isEmpty()) { + SwingHelper.showErrorDialog(this, "A manifest filename must be entered.", "Error"); + return; + } + + if (destDirField.getPath().isEmpty()) { + SwingHelper.showErrorDialog(this, "A destination directory must be entered.", "Error"); + return; + } + + options = new BuildOptions(version, manifestFilename, new File(destDirField.getPath())); + dispose(); + } + + public static BuildOptions showBuildDialog(Window parent, String version, String manifestName, File destDir) { + BuildDialog dialog = new BuildDialog(parent); + dialog.versionText.setText(version); + dialog.manifestFilenameText.setText(manifestName); + dialog.destDirField.setPath(destDir.getAbsolutePath()); + dialog.setVisible(true); + return dialog.getOptions(); + } + + @Data + public static class BuildOptions { + private final String version; + private final String manifestFilename; + private final File destDir; + } + +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/dialog/BuilderConfigDialog.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/dialog/BuilderConfigDialog.java new file mode 100644 index 0000000..c5c1847 --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/dialog/BuilderConfigDialog.java @@ -0,0 +1,231 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.dialog; + +import com.google.common.base.Strings; +import com.jidesoft.swing.SearchableUtils; +import com.jidesoft.swing.TableSearchable; +import com.skcraft.launcher.builder.BuilderConfig; +import com.skcraft.launcher.builder.FeaturePattern; +import com.skcraft.launcher.builder.FnPatternList; +import com.skcraft.launcher.creator.model.swing.FeaturePatternTableModel; +import com.skcraft.launcher.model.modpack.LaunchModifier; +import com.skcraft.launcher.swing.SwingHelper; +import com.skcraft.launcher.swing.TextFieldPopupMenu; +import net.miginfocom.swing.MigLayout; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.KeyEvent; + +public class BuilderConfigDialog extends JDialog { + + private final JTextField nameText = new JTextField(20); + private final JTextField titleText = new JTextField(30); + private final JTextField gameVersionText = new JTextField(10); + private final JTextArea launchFlagsArea = new JTextArea(10, 40); + private final JTextArea userFilesIncludeArea = new JTextArea(15, 40); + private final JTextArea userFilesExcludeArea = new JTextArea(8, 40); + private final FeaturePatternTable featuresTable = new FeaturePatternTable(); + private FeaturePatternTableModel featuresModel; + + private final BuilderConfig config; + private boolean saved = false; + + public BuilderConfigDialog(Window parent, BuilderConfig config) { + super(parent, "Modpack Properties", ModalityType.DOCUMENT_MODAL); + + this.config = config; + + setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + initComponents(); + setResizable(false); + pack(); + setLocationRelativeTo(parent); + + copyFrom(); + + nameText.requestFocus(); + } + + private void initComponents() { + nameText.setComponentPopupMenu(TextFieldPopupMenu.INSTANCE); + titleText.setComponentPopupMenu(TextFieldPopupMenu.INSTANCE); + gameVersionText.setComponentPopupMenu(TextFieldPopupMenu.INSTANCE); + launchFlagsArea.setComponentPopupMenu(TextFieldPopupMenu.INSTANCE); + userFilesIncludeArea.setComponentPopupMenu(TextFieldPopupMenu.INSTANCE); + + launchFlagsArea.setFont(nameText.getFont()); + userFilesIncludeArea.setFont(nameText.getFont()); + userFilesExcludeArea.setFont(nameText.getFont()); + + JTabbedPane tabbedPane = new JTabbedPane(); + + JPanel container = new JPanel(); + container.setLayout(new MigLayout("fill, insets dialog")); + + tabbedPane.addTab("Modpack", null, createMainPanel()); + tabbedPane.addTab("Launch", null, createLaunchPanel()); + tabbedPane.addTab("User Files", null, createUserFilesPanel()); + tabbedPane.addTab("Optional Features", null, createFeaturesPanel()); + + container.add(tabbedPane, "span, grow, gapbottom unrel"); + + JButton saveButton = new JButton("Save"); + JButton cancelButton = new JButton("Cancel"); + + container.add(saveButton, "tag ok, span, split 2, sizegroup bttn"); + container.add(cancelButton, "tag cancel, sizegroup bttn"); + + getRootPane().setDefaultButton(saveButton); + getRootPane().registerKeyboardAction(event -> cancelButton.doClick(), KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW); + + add(container, BorderLayout.CENTER); + + saveButton.addActionListener(e -> { + if (nameText.getText().trim().isEmpty()) { + SwingHelper.showErrorDialog(BuilderConfigDialog.this, "The 'Name' field cannot be empty.", "Input Error"); + return; + } + + if (gameVersionText.getText().trim().isEmpty()) { + SwingHelper.showErrorDialog(BuilderConfigDialog.this, "The 'Game Version' field must be a Minecraft version.", "Input Error"); + return; + } + + copyTo(); + saved = true; + dispose(); + }); + + cancelButton.addActionListener(e -> dispose()); + + TableSearchable tableSearchable = SearchableUtils.installSearchable(featuresTable); + tableSearchable.setMainIndex(-1); + } + + private JPanel createMainPanel() { + JPanel container = new JPanel(); + SwingHelper.removeOpaqueness(container); + container.setLayout(new MigLayout("insets dialog")); + + container.add(new JLabel("Name:")); + container.add(nameText, "span"); + + container.add(new JLabel("Title:")); + container.add(titleText, "span"); + + container.add(new JLabel("Game Version:")); + container.add(gameVersionText, "span"); + + return container; + } + + private JPanel createLaunchPanel() { + JPanel container = new JPanel(); + SwingHelper.removeOpaqueness(container); + container.setLayout(new MigLayout("insets dialog")); + + container.add(new JLabel("Launch Flags:"), "wrap"); + container.add(SwingHelper.wrapScrollPane(launchFlagsArea), "span"); + + return container; + } + + private JPanel createUserFilesPanel() { + JPanel container = new JPanel(); + SwingHelper.removeOpaqueness(container); + container.setLayout(new MigLayout("insets dialog")); + + container.add(new JLabel("Include Patterns:"), "wrap"); + container.add(SwingHelper.wrapScrollPane(userFilesIncludeArea), "span, gapbottom unrel"); + + container.add(new JLabel("Exclude Patterns:"), "wrap"); + container.add(SwingHelper.wrapScrollPane(userFilesExcludeArea), "span"); + + return container; + } + + private JPanel createFeaturesPanel() { + JPanel container = new JPanel(); + SwingHelper.removeOpaqueness(container); + container.setLayout(new MigLayout("fill, insets dialog")); + + JButton newButton = new JButton("New..."); + JButton editButton = new JButton("Edit..."); + JButton deleteButton = new JButton("Delete..."); + + container.add(newButton, "span, split 3, sizegroup bttn"); + container.add(editButton, "sizegroup bttn"); + container.add(deleteButton, "sizegroup bttn"); + + container.add(SwingHelper.wrapScrollPane(featuresTable), "grow, w 10:100:null, gaptop 10"); + + newButton.addActionListener(e -> { + FeaturePattern pattern = new FeaturePattern(); + if (FeaturePatternDialog.showEditor(BuilderConfigDialog.this, pattern)) { + featuresModel.addFeature(pattern); + } + }); + + editButton.addActionListener(e -> { + int index = featuresTable.getSelectedRow(); + if (index > -1) { + FeaturePattern pattern = featuresModel.getFeature(index); + FeaturePatternDialog.showEditor(BuilderConfigDialog.this, pattern); + featuresModel.fireTableDataChanged(); + } else { + SwingHelper.showErrorDialog(BuilderConfigDialog.this, "Select a feature first.", "No Selection"); + } + }); + + deleteButton.addActionListener(e -> { + int index = featuresTable.getSelectedRow(); + if (index > -1) { + FeaturePattern pattern = featuresModel.getFeature(index); + if (SwingHelper.confirmDialog(BuilderConfigDialog.this, "Are you sure that you want to delete '" + pattern.getFeature().getName() + "'?", "Delete")) { + featuresModel.removeFeature(index); + } + } else { + SwingHelper.showErrorDialog(BuilderConfigDialog.this, "Select a feature first.", "No Selection"); + } + }); + + return container; + } + + private void copyFrom() { + SwingHelper.setTextAndResetCaret(nameText, config.getName()); + SwingHelper.setTextAndResetCaret(titleText, config.getTitle()); + SwingHelper.setTextAndResetCaret(gameVersionText, config.getGameVersion()); + SwingHelper.setTextAndResetCaret(launchFlagsArea, SwingHelper.listToLines(config.getLaunchModifier().getFlags())); + SwingHelper.setTextAndResetCaret(userFilesIncludeArea, SwingHelper.listToLines(config.getUserFiles().getInclude())); + SwingHelper.setTextAndResetCaret(userFilesExcludeArea, SwingHelper.listToLines(config.getUserFiles().getExclude())); + featuresModel = new FeaturePatternTableModel(config.getFeatures()); + featuresTable.setModel(featuresModel); + } + + private void copyTo() { + config.setName(nameText.getText().trim()); + config.setTitle(Strings.emptyToNull(titleText.getText().trim())); + config.setGameVersion(gameVersionText.getText().trim()); + + LaunchModifier launchModifier = config.getLaunchModifier(); + FnPatternList userFiles = config.getUserFiles(); + + launchModifier.setFlags(SwingHelper.linesToList(launchFlagsArea.getText())); + userFiles.setInclude(SwingHelper.linesToList(userFilesIncludeArea.getText())); + userFiles.setExclude(SwingHelper.linesToList(userFilesExcludeArea.getText())); + } + + public static boolean showEditor(Window window, BuilderConfig config) { + BuilderConfigDialog dialog = new BuilderConfigDialog(window, config); + dialog.setVisible(true); + return dialog.saved; + } + +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/dialog/DeployServerDialog.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/dialog/DeployServerDialog.java new file mode 100644 index 0000000..b2d2e82 --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/dialog/DeployServerDialog.java @@ -0,0 +1,97 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.dialog; + +import com.skcraft.launcher.swing.DirectoryField; +import com.skcraft.launcher.swing.SwingHelper; +import lombok.Data; +import lombok.Getter; +import net.miginfocom.swing.MigLayout; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.KeyEvent; +import java.io.File; + +public class DeployServerDialog extends JDialog { + + private final DirectoryField destDirField = new DirectoryField(); + private final JCheckBox cleanModsCheck = new JCheckBox("Delete \"mods/\" folder before deploying"); + @Getter + private DeployOptions options; + + public DeployServerDialog(Window parent) { + super(parent, "Deploy Server Files", ModalityType.DOCUMENT_MODAL); + + setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + initComponents(); + setResizable(false); + pack(); + setLocationRelativeTo(parent); + + cleanModsCheck.setSelected(true); + } + + private void initComponents() { + JPanel container = new JPanel(); + container.setLayout(new MigLayout("insets dialog")); + + container.add(new JLabel("Output Directory:")); + container.add(destDirField, "span"); + + container.add(cleanModsCheck, "span, gapbottom unrel"); + + JButton buildButton = new JButton("Deploy"); + JButton cancelButton = new JButton("Cancel"); + + container.add(buildButton, "tag ok, span, split 2, sizegroup bttn"); + container.add(cancelButton, "tag cancel, sizegroup bttn"); + + add(container, BorderLayout.CENTER); + + getRootPane().setDefaultButton(buildButton); + getRootPane().registerKeyboardAction(e -> cancelButton.doClick(), KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW); + + buildButton.addActionListener(e -> returnValue()); + cancelButton.addActionListener(e -> dispose()); + } + + private void returnValue() { + String dir = destDirField.getPath(); + + if (dir.isEmpty()) { + SwingHelper.showErrorDialog(this, "A directory must be entered.", "Error"); + return; + } + + File dirFile = new File(dir); + + if (!dirFile.isDirectory()) { + SwingHelper.showErrorDialog(this, "The selected path is not a directory that exists.", "Error"); + return; + } + + options = new DeployOptions(dirFile,cleanModsCheck.isSelected()); + dispose(); + } + + public static DeployOptions showDeployDialog(Window parent, File destDir) { + DeployServerDialog dialog = new DeployServerDialog(parent); + if (destDir != null) { + dialog.destDirField.setPath(destDir.getAbsolutePath()); + } + dialog.setVisible(true); + return dialog.getOptions(); + } + + @Data + public static class DeployOptions { + private final File destDir; + private final boolean cleanMods; + } + +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/dialog/FeaturePatternDialog.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/dialog/FeaturePatternDialog.java new file mode 100644 index 0000000..f57a783 --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/dialog/FeaturePatternDialog.java @@ -0,0 +1,148 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.dialog; + +import com.google.common.base.Joiner; +import com.skcraft.launcher.builder.FeaturePattern; +import com.skcraft.launcher.builder.FnPatternList; +import com.skcraft.launcher.creator.model.swing.RecommendationComboBoxModel; +import com.skcraft.launcher.model.modpack.Feature; +import com.skcraft.launcher.model.modpack.Feature.Recommendation; +import com.skcraft.launcher.swing.SwingHelper; +import com.skcraft.launcher.swing.TextFieldPopupMenu; +import net.miginfocom.swing.MigLayout; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.KeyEvent; + +public class FeaturePatternDialog extends JDialog { + + private static final Joiner NEW_LINE_JOINER = Joiner.on("\n"); + + private final JTextField nameText = new JTextField(20); + private final JTextArea descArea = new JTextArea(3, 40); + private final JComboBox recommendationCombo = new JComboBox(new RecommendationComboBoxModel()); + private final JCheckBox selectedCheck = new JCheckBox("Selected by default"); + private final JTextArea includeArea = new JTextArea(8, 40); + private final JTextArea excludeArea = new JTextArea(3, 40); + + private final FeaturePattern pattern; + private boolean saved = false; + + public FeaturePatternDialog(Window parent, FeaturePattern pattern) { + super(parent, "Configure Feature", ModalityType.DOCUMENT_MODAL); + + this.pattern = pattern; + + setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + initComponents(); + setResizable(false); + pack(); + setLocationRelativeTo(parent); + + copyFrom(); + } + + private void initComponents() { + nameText.setComponentPopupMenu(TextFieldPopupMenu.INSTANCE); + descArea.setComponentPopupMenu(TextFieldPopupMenu.INSTANCE); + includeArea.setComponentPopupMenu(TextFieldPopupMenu.INSTANCE); + excludeArea.setComponentPopupMenu(TextFieldPopupMenu.INSTANCE); + + descArea.setFont(nameText.getFont()); + includeArea.setFont(nameText.getFont()); + excludeArea.setFont(nameText.getFont()); + + JPanel container = new JPanel(); + container.setLayout(new MigLayout("insets dialog")); + + container.add(new JLabel("Feature Name:")); + container.add(nameText, "span"); + + container.add(new JLabel("Recommendation:")); + container.add(recommendationCombo, "span"); + + container.add(selectedCheck, "span"); + + container.add(new JLabel("Description:"), "wrap"); + container.add(SwingHelper.wrapScrollPane(descArea), "span"); + + container.add(new JLabel("Include Patterns:"), "wrap"); + container.add(SwingHelper.wrapScrollPane(includeArea), "span"); + + container.add(new JLabel("Exclude Patterns:"), "wrap"); + container.add(SwingHelper.wrapScrollPane(excludeArea), "span, gapbottom unrel"); + + JButton okButton = new JButton("OK"); + JButton cancelButton = new JButton("Cancel"); + + container.add(okButton, "tag ok, span, split 2, sizegroup bttn"); + container.add(cancelButton, "tag cancel, sizegroup bttn"); + + getRootPane().setDefaultButton(okButton); + getRootPane().registerKeyboardAction(e -> cancelButton.doClick(), KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW); + + add(container, BorderLayout.CENTER); + + okButton.addActionListener(e -> { + if (nameText.getText().trim().isEmpty()) { + SwingHelper.showErrorDialog(FeaturePatternDialog.this, "The 'Feature Name' field cannot be empty.", "Input Error"); + return; + } + + if (descArea.getText().trim().isEmpty()) { + SwingHelper.showErrorDialog(FeaturePatternDialog.this, "The 'Description' field cannot be empty.", "Input Error"); + return; + } + + if (includeArea.getText().trim().isEmpty()) { + SwingHelper.showErrorDialog(FeaturePatternDialog.this, "The 'Include Patterns' field cannot be empty.", "Input Error"); + return; + } + + copyTo(); + saved = true; + dispose(); + }); + + cancelButton.addActionListener(e -> dispose()); + } + + private void copyFrom() { + if (pattern.getFeature() == null) { + pattern.setFeature(new Feature()); + } + + if (pattern.getFilePatterns() == null) { + pattern.setFilePatterns(new FnPatternList()); + } + + SwingHelper.setTextAndResetCaret(nameText, pattern.getFeature().getName()); + SwingHelper.setTextAndResetCaret(descArea, pattern.getFeature().getDescription()); + recommendationCombo.setSelectedItem(pattern.getFeature().getRecommendation()); + selectedCheck.setSelected(pattern.getFeature().isSelected()); + SwingHelper.setTextAndResetCaret(includeArea, NEW_LINE_JOINER.join(pattern.getFilePatterns().getInclude())); + SwingHelper.setTextAndResetCaret(excludeArea, NEW_LINE_JOINER.join(pattern.getFilePatterns().getExclude())); + } + + private void copyTo() { + pattern.getFeature().setName(nameText.getText().trim()); + pattern.getFeature().setDescription(descArea.getText().trim()); + pattern.getFeature().setRecommendation((Recommendation) recommendationCombo.getSelectedItem()); + pattern.getFeature().setSelected(selectedCheck.isSelected()); + pattern.getFilePatterns().setInclude(SwingHelper.linesToList(includeArea.getText())); + pattern.getFilePatterns().setExclude(SwingHelper.linesToList(excludeArea.getText())); + } + + public static boolean showEditor(Window window, FeaturePattern pattern) { + FeaturePatternDialog dialog = new FeaturePatternDialog(window, pattern); + dialog.setVisible(true); + return dialog.saved; + } + +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/dialog/FeaturePatternTable.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/dialog/FeaturePatternTable.java new file mode 100644 index 0000000..333699a --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/dialog/FeaturePatternTable.java @@ -0,0 +1,24 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.dialog; + +import com.skcraft.launcher.swing.DefaultTable; + +import javax.swing.table.TableModel; + +class FeaturePatternTable extends DefaultTable { + + @Override + public void setModel(TableModel dataModel) { + super.setModel(dataModel); + try { + getColumnModel().getColumn(1).setMaxWidth(80); + getColumnModel().getColumn(2).setMaxWidth(80); + } catch (ArrayIndexOutOfBoundsException ignored) { + } + } +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/dialog/GenerateListingDialog.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/dialog/GenerateListingDialog.java new file mode 100644 index 0000000..6a4a42c --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/dialog/GenerateListingDialog.java @@ -0,0 +1,82 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.dialog; + +import com.jidesoft.swing.SearchableUtils; +import com.jidesoft.swing.TableSearchable; +import com.skcraft.launcher.creator.Creator; +import com.skcraft.launcher.creator.model.swing.ListingType; +import com.skcraft.launcher.creator.model.swing.ListingTypeComboBoxModel; +import com.skcraft.launcher.swing.DefaultTable; +import com.skcraft.launcher.swing.DirectoryField; +import com.skcraft.launcher.swing.SwingHelper; +import com.skcraft.launcher.swing.TableColumnAdjuster; +import lombok.Getter; +import net.miginfocom.swing.MigLayout; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.KeyEvent; + +public class GenerateListingDialog extends JDialog { + + @Getter private final DirectoryField destDirField = new DirectoryField(); + @Getter private final JComboBox listingTypeCombo = new JComboBox<>(new ListingTypeComboBoxModel()); + @Getter private final JTable manifestsTable = new DefaultTable(); + @Getter private final JLabel gameKeyWarning = new JLabel("Selected listing type won't support adding modpacks using 'game keys'.", SwingHelper.createIcon(Creator.class, "warning_icon.png"), SwingConstants.LEFT); + + @Getter private final JButton editManifestButton = new JButton("Modify..."); + + @Getter private final JButton generateButton = new JButton("Generate"); + @Getter private final JButton cancelButton = new JButton("Cancel"); + + @Getter private final TableColumnAdjuster manifestsTableAdjuster = new TableColumnAdjuster(manifestsTable); + + public GenerateListingDialog(Window parent) { + super(parent, "Generate Package Listing", ModalityType.DOCUMENT_MODAL); + + setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + initComponents(); + pack(); + setLocationRelativeTo(parent); + } + + private void initComponents() { + manifestsTableAdjuster.adjustColumns(); + manifestsTableAdjuster.setDynamicAdjustment(true); + + manifestsTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN); + manifestsTable.setAutoCreateRowSorter(true); + + JPanel container = new JPanel(); + container.setLayout(new MigLayout("insets dialog, fill", "[grow 0][grow 100]")); + + container.add(new JLabel("Output Directory:")); + container.add(destDirField, "span"); + + container.add(new JLabel("Package Listing Type:")); + container.add(listingTypeCombo, "span"); + container.add(gameKeyWarning, "span, skip 1, hidemode 3"); + + container.add(new JLabel("Modpacks to Include:"), "span, gaptop unrel"); + container.add(SwingHelper.wrapScrollPane(manifestsTable), "grow, pushy, span, w 500:650, h 170"); + container.add(editManifestButton, "gapbottom unrel, span, split 2"); + container.add(new JLabel("Previously-selected modpacks and those in the _upload directory are the available options."), "gapbottom unrel"); + + container.add(generateButton, "tag ok, span, split 2, sizegroup bttn"); + container.add(cancelButton, "tag cancel, sizegroup bttn"); + + add(container, BorderLayout.CENTER); + + getRootPane().setDefaultButton(generateButton); + getRootPane().registerKeyboardAction(e -> cancelButton.doClick(), KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW); + + TableSearchable tableSearchable = SearchableUtils.installSearchable(manifestsTable); + tableSearchable.setMainIndex(-1); + } + +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/dialog/ManifestEntryDialog.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/dialog/ManifestEntryDialog.java new file mode 100644 index 0000000..60f3cab --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/dialog/ManifestEntryDialog.java @@ -0,0 +1,64 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.dialog; + +import com.skcraft.launcher.swing.SwingHelper; +import com.skcraft.launcher.swing.TextFieldPopupMenu; +import lombok.Getter; +import net.miginfocom.swing.MigLayout; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.KeyEvent; + +public class ManifestEntryDialog extends JDialog { + + @Getter private final JSpinner prioritySpinner = new JSpinner(); + @Getter private final JTextArea gameKeysText = new JTextArea(5, 30); + @Getter private final JCheckBox includeCheck = new JCheckBox("Include in package listing"); + + @Getter private final JButton okButton = new JButton("OK"); + @Getter private final JButton cancelButton = new JButton("Cancel"); + + public ManifestEntryDialog(Window parent) { + super(parent, "Modpack Entry", ModalityType.DOCUMENT_MODAL); + + setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + initComponents(); + setResizable(false); + pack(); + setLocationRelativeTo(parent); + } + + private void initComponents() { + gameKeysText.setFont(prioritySpinner.getFont()); + + prioritySpinner.setComponentPopupMenu(TextFieldPopupMenu.INSTANCE); + gameKeysText.setComponentPopupMenu(TextFieldPopupMenu.INSTANCE); + + JPanel container = new JPanel(); + container.setLayout(new MigLayout("insets dialog")); + + container.add(includeCheck, "span, gapbottom unrel"); + + container.add(new JLabel("Priority:")); + container.add(prioritySpinner, "span, split 2, w 50"); + container.add(new JLabel("(Greater is higher)")); + + container.add(new JLabel("Game Keys:")); + container.add(SwingHelper.wrapScrollPane(gameKeysText), "span"); + + container.add(okButton, "tag ok, span, split 2, sizegroup bttn, gaptop unrel"); + container.add(cancelButton, "tag cancel, sizegroup bttn"); + + add(container, BorderLayout.CENTER); + + getRootPane().setDefaultButton(okButton); + getRootPane().registerKeyboardAction(e -> cancelButton.doClick(), KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW); + } + +} 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 new file mode 100644 index 0000000..4ad1798 --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/dialog/PackManagerFrame.java @@ -0,0 +1,206 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.dialog; + +import com.jidesoft.swing.SearchableUtils; +import com.jidesoft.swing.TableSearchable; +import com.skcraft.launcher.creator.Creator; +import com.skcraft.launcher.swing.DefaultTable; +import com.skcraft.launcher.swing.SwingHelper; +import com.skcraft.launcher.swing.TableColumnAdjuster; +import lombok.Getter; +import net.miginfocom.swing.MigLayout; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.KeyEvent; + +public class PackManagerFrame extends JFrame { + + @Getter private final JButton newPackButton = new JButton("New Pack", SwingHelper.createIcon(Creator.class, "new.png")); + @Getter private final JButton importButton = new JButton("Add Existing", SwingHelper.createIcon(Creator.class, "import.png")); + @Getter private final JButton editConfigButton = new JButton("Modify", SwingHelper.createIcon(Creator.class, "edit.png")); + @Getter private final JButton openFolderButton = new JButton("Open", SwingHelper.createIcon(Creator.class, "open_folder.png")); + @Getter private final JButton checkProblemsButton = new JButton("Check", SwingHelper.createIcon(Creator.class, "check.png")); + @Getter private final JButton testButton = new JButton("Test", SwingHelper.createIcon(Creator.class, "test.png")); + @Getter private final JButton buildButton = new JButton("Build", SwingHelper.createIcon(Creator.class, "build.png")); + + @Getter private final JMenuItem newPackMenuItem = new JMenuItem("New Pack..."); + @Getter private final JMenuItem newPackAtLocationMenuItem = new JMenuItem("New Pack at Location..."); + @Getter private final JMenuItem importPackMenuItem = new JMenuItem("Add Existing Pack..."); + @Getter private final JMenuItem changePackLocationMenuItem = new JMenuItem("Change Pack Location..."); + @Getter private final JMenuItem refreshMenuItem = new JMenuItem("Reload Workspace"); + @Getter private final JMenuItem removePackItem = new JMenuItem("Remove Pack..."); + @Getter private final JMenuItem deletePackItem = new JMenuItem("Delete Pack Forever..."); + @Getter private final JMenuItem quitMenuItem = new JMenuItem("Exit"); + @Getter private final JMenuItem editConfigMenuItem = new JMenuItem("Edit modpack.json..."); + @Getter private final JMenuItem openFolderMenuItem = new JMenuItem("Open Directory"); + @Getter private final JMenuItem checkProblemsMenuItem = new JMenuItem("Scan for Problems..."); + @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..."); + @Getter private final JMenuItem deployServerMenuItem = new JMenuItem("Deploy Server..."); + @Getter private final JMenuItem generatePackagesMenuItem = new JMenuItem("Generate packages.json..."); + @Getter private final JMenuItem openOutputFolderMenuItem = new JMenuItem("Open Upload Folder"); + @Getter private final JMenuItem versionCheckMenuItem = new JMenuItem("Check for Mod Updates"); + @Getter private final JMenuItem openWorkspaceFolderMenuItem = new JMenuItem("Open Workspace Folder"); + @Getter private final JMenuItem openLauncherFolderMenuItem = new JMenuItem("Open Test Launcher Folder"); + @Getter private final JMenuItem openWebRootMenuItem = new JMenuItem("Open Test Web Server Folder"); + @Getter private final JMenuItem openConsoleMenuItem = new JMenuItem("Open Console"); + @Getter private final JMenuItem docsMenuItem = new JMenuItem("Documentation"); + @Getter private final JMenuItem aboutMenuItem = new JMenuItem("About"); + + @Getter private final JTable packTable = new DefaultTable(); + + public PackManagerFrame() { + super("Modpack Creator"); + + setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + initComponents(); + initMenu(); + pack(); + setLocationRelativeTo(null); + + SwingHelper.setFrameIcon(this, Creator.class, "icon.png"); + } + + private void initComponents() { + TableColumnAdjuster adjuster = new TableColumnAdjuster(packTable); + adjuster.adjustColumns(); + adjuster.setDynamicAdjustment(true); + + packTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN); + packTable.setAutoCreateRowSorter(true); + + JPanel container = new JPanel(); + container.setLayout(new MigLayout("fill, wrap 1")); + + container.add(createToolbar(), "dock north"); + container.add(SwingHelper.wrapScrollPane(packTable), "grow, span, w null:800:null"); + + TableSearchable tableSearchable = SearchableUtils.installSearchable(packTable); + tableSearchable.setMainIndex(-1); + + add(container, BorderLayout.CENTER); + } + + private JToolBar createToolbar() { + JToolBar toolBar = new JToolBar("Toolbar"); + + toolBar.setFloatable(false); + + toolBar.add(newPackButton); + toolBar.add(importButton); + toolBar.addSeparator(); + toolBar.add(editConfigButton); + toolBar.add(openFolderButton); + toolBar.add(checkProblemsButton); + toolBar.addSeparator(); + toolBar.add(testButton); + toolBar.add(buildButton); + + return toolBar; + } + + private void initMenu() { + int ctrlKeyMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(); + + newPackMenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, ctrlKeyMask)); + newPackAtLocationMenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, ctrlKeyMask | Event.SHIFT_MASK)); + editConfigMenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_E, ctrlKeyMask)); + openFolderMenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, ctrlKeyMask | Event.SHIFT_MASK)); + testMenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F5, 0)); + testOnlineMenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F6, 0)); + buildMenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F10, Event.SHIFT_MASK)); + deployServerMenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F9, Event.SHIFT_MASK)); + docsMenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F1, 0)); + + JMenuBar menuBar; + JMenu menu; + + menuBar = new JMenuBar(); + menuBar.setBorder(BorderFactory.createEmptyBorder()); + + Insets menuInset = new Insets(2, 2, 2, 2); + + menu = new JMenu("File"); + menu.setMargin(menuInset); + menu.setMnemonic('f'); + menuBar.add(menu); + menu.add(newPackMenuItem); + menu.add(newPackAtLocationMenuItem); + menu.add(importPackMenuItem); + menu.addSeparator(); + menu.add(changePackLocationMenuItem); + menu.add(removePackItem); + menu.add(deletePackItem); + menu.addSeparator(); + menu.add(refreshMenuItem); + menu.addSeparator(); + menu.add(quitMenuItem); + + menu = new JMenu("Edit"); + menu.setMargin(menuInset); + menu.setMnemonic('e'); + menuBar.add(menu); + menu.add(editConfigMenuItem); + menu.add(openFolderMenuItem); + menu.addSeparator(); + menu.add(checkProblemsMenuItem); + + menu = new JMenu("Test"); + menu.setMargin(menuInset); + menu.setMnemonic('t'); + menuBar.add(menu); + menu.add(testMenuItem); + menu.add(testOnlineMenuItem); + menu.addSeparator(); + menu.add(optionsMenuItem); + menu.add(instanceOptionsMenuItem); + menu.addSeparator(); + menu.add(clearInstanceMenuItem); + menu.add(clearWebRootMenuItem); + + menu = new JMenu("Build"); + menu.setMargin(menuInset); + menu.setMnemonic('b'); + menuBar.add(menu); + menu.add(buildMenuItem); + menu.add(deployServerMenuItem); + menu.addSeparator(); + menu.add(generatePackagesMenuItem); + menu.addSeparator(); + menu.add(openOutputFolderMenuItem); + + menu = new JMenu("Tools"); + menu.setMargin(menuInset); + menu.setMnemonic('t'); + menuBar.add(menu); + menu.add(versionCheckMenuItem); + menu.addSeparator(); + menu.add(openWorkspaceFolderMenuItem); + menu.add(openLauncherFolderMenuItem); + menu.add(openWebRootMenuItem); + menu.addSeparator(); + menu.add(openConsoleMenuItem); + + menu = new JMenu("Help"); + menu.setMargin(menuInset); + menu.setMnemonic('h'); + menuBar.add(menu); + menu.add(docsMenuItem); + menu.addSeparator(); + menu.add(aboutMenuItem); + + setJMenuBar(menuBar); + } + +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/dialog/ProblemTable.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/dialog/ProblemTable.java new file mode 100644 index 0000000..0cdf8bd --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/dialog/ProblemTable.java @@ -0,0 +1,30 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.dialog; + +import com.skcraft.launcher.swing.DefaultTable; + +import javax.swing.table.TableModel; + +class ProblemTable extends DefaultTable { + + public ProblemTable() { + super(); + setTableHeader(null); + } + + @Override + public void setModel(TableModel dataModel) { + super.setModel(dataModel); + try { + getColumnModel().getColumn(0).setMaxWidth(20); + } catch (ArrayIndexOutOfBoundsException ignored) { + } + } + + +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/dialog/ProblemViewer.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/dialog/ProblemViewer.java new file mode 100644 index 0000000..03f0abe --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/dialog/ProblemViewer.java @@ -0,0 +1,76 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.dialog; + +import com.skcraft.launcher.creator.model.creator.Problem; +import com.skcraft.launcher.creator.model.swing.ProblemTableModel; +import com.skcraft.launcher.swing.SwingHelper; +import com.skcraft.launcher.swing.TextFieldPopupMenu; +import net.miginfocom.swing.MigLayout; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.KeyEvent; +import java.util.List; + +public class ProblemViewer extends JDialog { + + private static final String DEFAULT_EXPLANATION = "Select a problem on the left to see the explanation here."; + private final ProblemTable problemTable = new ProblemTable(); + private final ProblemTableModel problemTableModel; + private final JTextArea explanationText = new JTextArea(DEFAULT_EXPLANATION); + + public ProblemViewer(Window parent, List problems) { + super(parent, "Potential Problems", ModalityType.DOCUMENT_MODAL); + + setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + initComponents(); + pack(); + setLocationRelativeTo(parent); + + problemTableModel = new ProblemTableModel(problems); + problemTable.setModel(problemTableModel); + } + + private void initComponents() { + explanationText.setComponentPopupMenu(TextFieldPopupMenu.INSTANCE); + + explanationText.setFont(new JTextField().getFont()); + explanationText.setEditable(false); + explanationText.setLineWrap(true); + explanationText.setWrapStyleWord(true); + + JPanel container = new JPanel(); + container.setLayout(new MigLayout("fill, insets dialog")); + + JSplitPane splitPane = new JSplitPane( + JSplitPane.HORIZONTAL_SPLIT, + SwingHelper.wrapScrollPane(problemTable), SwingHelper. + wrapScrollPane(explanationText)); + splitPane.setDividerLocation(180); + SwingHelper.flattenJSplitPane(splitPane); + + container.add(splitPane, "grow, w 220:500, h 240, wrap"); + + JButton closeButton = new JButton("Close"); + container.add(closeButton, "tag cancel, gaptop unrel"); + + add(container, BorderLayout.CENTER); + + getRootPane().setDefaultButton(closeButton); + getRootPane().registerKeyboardAction(e -> closeButton.doClick(), KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW); + + problemTable.getSelectionModel().addListSelectionListener(e -> { + Problem selected = problemTableModel.getProblem(problemTable.getSelectedRow()); + if (selected != null) { + SwingHelper.setTextAndResetCaret(explanationText, selected.getExplanation()); + } + }); + + closeButton.addActionListener(e -> dispose()); + } +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/dialog/VersionCheckDialog.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/dialog/VersionCheckDialog.java new file mode 100644 index 0000000..b632fce --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/dialog/VersionCheckDialog.java @@ -0,0 +1,76 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.dialog; + +import com.jidesoft.swing.SearchableUtils; +import com.jidesoft.swing.TableSearchable; +import com.skcraft.launcher.swing.DefaultTable; +import com.skcraft.launcher.swing.SwingHelper; +import com.skcraft.launcher.swing.TableColumnAdjuster; +import lombok.Getter; +import net.miginfocom.swing.MigLayout; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.KeyEvent; + +public class VersionCheckDialog extends JDialog { + + @Getter private final JTable knownModsTable = new DefaultTable(); + @Getter private final JTable unknownModsTable = new DefaultTable(); + @Getter private final JButton closeButton = new JButton("Close"); + private final TableColumnAdjuster updateTableAdjuster = new TableColumnAdjuster(knownModsTable); + private final TableColumnAdjuster unknownTableAdjuster = new TableColumnAdjuster(unknownModsTable); + + public VersionCheckDialog(Window parent) { + super(parent, "Update Check", ModalityType.DOCUMENT_MODAL); + + setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + initComponents(); + pack(); + setLocationRelativeTo(parent); + } + + private void initComponents() { + updateTableAdjuster.adjustColumns(); + updateTableAdjuster.setDynamicAdjustment(true); + + unknownTableAdjuster.adjustColumns(); + unknownTableAdjuster.setDynamicAdjustment(true); + + knownModsTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN); + knownModsTable.setAutoCreateRowSorter(true); + + unknownModsTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN); + unknownModsTable.setAutoCreateRowSorter(true); + + JPanel container = new JPanel(); + container.setLayout(new MigLayout("insets dialog, fill")); + + container.add(new JLabel("With Potential Updates:"), "span"); + container.add(SwingHelper.wrapScrollPane(knownModsTable), "grow, pushy, span, w 500:900, h 230"); + + container.add(new JLabel("Unknown Status:"), "span"); + container.add(SwingHelper.wrapScrollPane(unknownModsTable), "grow, pushy, span, w 500:900, h 150, gapbottom unrel, wrap"); + + container.add(new JLabel("Version data is sourced from NotEnoughMods.com."), ""); + container.add(closeButton, "tag cancel, sizegroup bttn"); + + add(container, BorderLayout.CENTER); + + getRootPane().registerKeyboardAction(e -> closeButton.doClick(), KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW); + + TableSearchable tableSearchable; + + tableSearchable = SearchableUtils.installSearchable(knownModsTable); + tableSearchable.setMainIndex(-1); + + tableSearchable = SearchableUtils.installSearchable(unknownModsTable); + tableSearchable.setMainIndex(-1); + } + +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/dialog/WelcomeDialog.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/dialog/WelcomeDialog.java new file mode 100644 index 0000000..0e0b0f3 --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/dialog/WelcomeDialog.java @@ -0,0 +1,70 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.dialog; + +import com.skcraft.launcher.creator.Creator; +import com.skcraft.launcher.creator.model.creator.RecentEntry; +import com.skcraft.launcher.creator.swing.BorderCellRenderer; +import com.skcraft.launcher.swing.SwingHelper; +import lombok.Getter; +import net.miginfocom.swing.MigLayout; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.KeyEvent; + +public class WelcomeDialog extends JFrame { + + @Getter private final JButton newButton = new JButton("New Workspace...", SwingHelper.createIcon(Creator.class, "new.png")); + @Getter private final JButton openButton = new JButton("Open Workspace...", SwingHelper.createIcon(Creator.class, "open_folder.png")); + @Getter private final JButton helpButton = new JButton("Help"); + @Getter private final JButton aboutButton = new JButton("About"); + @Getter private final JButton quitButton = new JButton("Quit"); + @Getter private final JList recentList = new JList<>(); + + public WelcomeDialog() { + super("Modpack Creator"); + + setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + initComponents(); + setResizable(false); + pack(); + setLocationRelativeTo(null); + + SwingHelper.setFrameIcon(this, Creator.class, "icon.png"); + } + + private void initComponents() { + recentList.setCellRenderer(new BorderCellRenderer(BorderFactory.createEmptyBorder(5, 5, 5, 5))); + + JPanel container = new JPanel(); + container.setLayout(new MigLayout("insets 50 20")); + + container.add(new JLabel(SwingHelper.createIcon(Creator.class, "welcome_logo.png")), "wrap, gap 20 20, gapbottom 30"); + + container.add(newButton, "grow, gap 50 50, wrap"); + container.add(openButton, "grow, gap 50 50, wrap"); + + JScrollPane recentScrollPane = new JScrollPane(recentList); + recentScrollPane.setBorder(BorderFactory.createMatteBorder(0, 1, 0, 0, Color.LIGHT_GRAY)); + recentScrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); + container.add(recentScrollPane, "dock east, w 280, h 390"); + + JPanel buttons = new JPanel(); + buttons.setLayout(new MigLayout("insets 20", "[][]push[]")); + buttons.add(helpButton); + buttons.add(aboutButton); + buttons.add(quitButton); + container.add(buttons, "dock south"); + + add(container, BorderLayout.CENTER); + + getRootPane().registerKeyboardAction(e -> quitButton.doClick(), KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW); + } + + +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/model/creator/CreatorConfig.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/model/creator/CreatorConfig.java new file mode 100644 index 0000000..fabbc65 --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/model/creator/CreatorConfig.java @@ -0,0 +1,34 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.model.creator; + +import com.google.common.collect.Lists; +import lombok.Data; + +import java.util.List; + +@Data +public class CreatorConfig { + + private List recentEntries = Lists.newArrayList(); + private boolean offlineEnabled; + + public void setRecentEntries(List recentEntries) { + this.recentEntries = recentEntries != null ? recentEntries : Lists.newArrayList(); + } + + @Override + public boolean equals(Object o) { + return super.equals(o); + } + + @Override + public int hashCode() { + return super.hashCode(); + } + +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/model/creator/ManifestEntry.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/model/creator/ManifestEntry.java new file mode 100644 index 0000000..bfe51b1 --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/model/creator/ManifestEntry.java @@ -0,0 +1,27 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.model.creator; + +import com.google.common.collect.Lists; +import com.skcraft.launcher.model.modpack.ManifestInfo; +import lombok.Data; + +import java.util.List; + +@Data +public class ManifestEntry implements Comparable { + + private boolean selected = false; + private ManifestInfo manifestInfo; + private List gameKeys = Lists.newArrayList(); + + @Override + public int compareTo(ManifestEntry o) { + return manifestInfo.compareTo(o.getManifestInfo()); + } + +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/model/creator/ModFile.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/model/creator/ModFile.java new file mode 100644 index 0000000..45843ab --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/model/creator/ModFile.java @@ -0,0 +1,38 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.model.creator; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; + +import java.io.File; +import java.net.URL; +import java.util.regex.Pattern; + +@Data +public class ModFile { + + private File file; + private String modId; + private String name; + private String gameVersion; + private String version; + private String latestVersion; + private String latestDevVersion; + private URL url; + + @JsonIgnore + public String getCleanVersion() { + String version = getVersion(); + return version != null ? + version + .replaceAll("^" + Pattern.quote(gameVersion) + "\\-", "") + .replaceAll("\\-" + Pattern.quote(gameVersion) + "$", "") + : null; + } + +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/model/creator/Pack.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/model/creator/Pack.java new file mode 100644 index 0000000..eabe625 --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/model/creator/Pack.java @@ -0,0 +1,71 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.model.creator; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.skcraft.launcher.builder.BuilderConfig; +import com.skcraft.launcher.builder.BuilderOptions; +import com.skcraft.launcher.persistence.Persistence; +import lombok.Data; + +import java.io.File; + +@Data +public class Pack { + + private String location; + @JsonIgnore private Workspace workspace; + @JsonIgnore private BuilderConfig cachedConfig; + + @JsonIgnore + public File getDirectory() { + File path = new File(location); + if (path.isAbsolute()) { + return path; + } else { + return new File(workspace.getDirectory(), location); + } + } + + @JsonIgnore + public File getLoadersDir() { + return new File(getDirectory(), "loaders"); + } + + @JsonIgnore + public File getSourceDir() { + return new File(getDirectory(), "src"); + } + + @JsonIgnore + public File getModsDir() { + return new File(getSourceDir(), "mods"); + } + + @JsonIgnore + public File getConfigFile() { + return new File(getDirectory(), BuilderOptions.DEFAULT_CONFIG_FILENAME); + } + + public void load() { + setCachedConfig(Persistence.read(getConfigFile(), BuilderConfig.class, true)); + getLoadersDir().mkdirs(); + getSourceDir().mkdirs(); + } + + public void createGuideFolders() { + new File(getSourceDir(), "config").mkdirs(); + new File(getSourceDir(), "mods").mkdirs(); + new File(getSourceDir(), "resourcepacks").mkdirs(); + } + + @JsonIgnore + public boolean isLoaded() { + return cachedConfig != null; + } + +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/model/creator/Problem.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/model/creator/Problem.java new file mode 100644 index 0000000..fcc56ef --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/model/creator/Problem.java @@ -0,0 +1,27 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.model.creator; + +import lombok.Data; + +@Data +public class Problem { + + private final String title; + private final String explanation; + + public Problem(String title, String explanation) { + this.title = title; + this.explanation = explanation; + } + + @Override + public String toString() { + return title; + } + +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/model/creator/RecentEntry.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/model/creator/RecentEntry.java new file mode 100644 index 0000000..7b1b244 --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/model/creator/RecentEntry.java @@ -0,0 +1,27 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.model.creator; + +import lombok.Data; + +import java.io.File; + +@Data +public class RecentEntry { + + private File path; + + public void setPath(File path) { + this.path = path != null ? path : new File("."); + } + + @Override + public String toString() { + return path.getAbsolutePath(); + } + +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/model/creator/Workspace.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/model/creator/Workspace.java new file mode 100644 index 0000000..6b7b1f9 --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/model/creator/Workspace.java @@ -0,0 +1,78 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.model.creator; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.google.common.collect.Lists; +import com.skcraft.launcher.creator.model.swing.ListingType; +import lombok.Data; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +@Data +public class Workspace { + + public static final String DIR_NAME = ".modpacks"; + public static final String FILENAME = "workspace.json"; + + @JsonIgnore private File directory; + private List packs = Lists.newArrayList(); + private List packageListingEntries = Lists.newArrayList(); + private ListingType packageListingType = ListingType.STATIC; + + public void setPacks(List packs) { + this.packs = packs != null ? packs : Lists.newArrayList(); + } + + public void setPackageListingEntries(List entries) { + this.packageListingEntries = entries != null ? entries : Lists.newArrayList(); + } + + public void setPackageListingType(ListingType packageListingType) { + this.packageListingType = packageListingType != null ? packageListingType : ListingType.STATIC; + } + + public boolean hasPack(File dir) { + for (Pack pack : packs) { + try { + if (pack.getDirectory().getCanonicalPath().equals(dir.getCanonicalPath())) { + return true; + } + } catch (IOException ignored) { + } + } + + return false; + } + + public void load() { + for (Pack pack : getPacks()) { + pack.setWorkspace(this); + } + } + + @Override + public boolean equals(Object o) { + return super.equals(o); + } + + @Override + public int hashCode() { + return super.hashCode(); + } + + public static File getDataDir(File workspaceDir) { + return new File(workspaceDir, DIR_NAME); + } + + public static File getWorkspaceFile(File workspaceDir) { + return new File(getDataDir(workspaceDir), FILENAME); + } + +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/model/swing/FeaturePatternTableModel.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/model/swing/FeaturePatternTableModel.java new file mode 100644 index 0000000..5eb2879 --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/model/swing/FeaturePatternTableModel.java @@ -0,0 +1,97 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.model.swing; + +import com.skcraft.launcher.builder.FeaturePattern; + +import javax.swing.table.AbstractTableModel; +import java.util.List; + +public class FeaturePatternTableModel extends AbstractTableModel { + + private final List features; + + public FeaturePatternTableModel(List features) { + this.features = features; + } + + @Override + public String getColumnName(int columnIndex) { + switch (columnIndex) { + case 0: + return "Feature"; + case 1: + return "Recommendation"; + case 2: + return "Default?"; + default: + return null; + } + } + + @Override + public Class getColumnClass(int columnIndex) { + switch (columnIndex) { + case 0: + return String.class; + case 1: + return String.class; + case 2: + return String.class; + default: + return null; + } + } + + @Override + public void setValueAt(Object value, int rowIndex, int columnIndex) { + } + + @Override + public boolean isCellEditable(int rowIndex, int columnIndex) { + return false; + } + + @Override + public int getRowCount() { + return features.size(); + } + + @Override + public int getColumnCount() { + return 3; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + switch (columnIndex) { + case 0: + return features.get(rowIndex).getFeature().getName(); + case 1: + return features.get(rowIndex).getFeature().getRecommendation(); + case 2: + return features.get(rowIndex).getFeature().isSelected() ? "Yes" : ""; + default: + return null; + } + } + + public FeaturePattern getFeature(int index) { + return features.get(index); + } + + public void addFeature(FeaturePattern pattern) { + features.add(pattern); + fireTableDataChanged(); + } + + public void removeFeature(int index) { + features.remove(index); + fireTableDataChanged(); + } + +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/model/swing/ListingType.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/model/swing/ListingType.java new file mode 100644 index 0000000..0056e8f --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/model/swing/ListingType.java @@ -0,0 +1,96 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.model.swing; + +import com.google.common.base.Joiner; +import com.google.common.collect.Lists; +import com.skcraft.launcher.creator.model.creator.ManifestEntry; +import com.skcraft.launcher.model.modpack.ManifestInfo; +import com.skcraft.launcher.model.modpack.PackageList; +import com.skcraft.launcher.persistence.Persistence; +import lombok.Getter; + +import java.io.IOException; +import java.util.List; +import java.util.stream.Collectors; + +public enum ListingType { + + STATIC("packages.json (static)", false, "packages.json") { + @Override + public String generate(List entries) throws IOException { + PackageList list = new PackageList(); + list.setPackages(Lists.newArrayList()); + list.setMinimumVersion(PackageList.MIN_VERSION); + for (ManifestEntry entry : entries) { + if (entry.getGameKeys().isEmpty()) { + list.getPackages().add(entry.getManifestInfo()); + } + } + return Persistence.writeValueAsString(list, Persistence.L2F_LIST_PRETTY_PRINTER); + } + }, + PHP("packages.php (requires PHP on web server) ", true, "packages.php") { + @Override + public String generate(List entries) throws IOException { + StringBuilder builder = new StringBuilder(); + builder.append(" keys = entry.getGameKeys(); + + if (!keys.isEmpty()) { + builder.append("if (count(array_intersect(array(").append(escapeKeys(keys)).append("), $keys)) > 0)\r\n"); + } + + builder.append("$packages[] = array(\r\n"); + builder.append(" 'name' => '").append(escape(info.getName())).append("',\r\n"); + if (info.getTitle() != null) { + builder.append(" 'title' => '").append(escape(info.getTitle())).append("',\r\n"); + } + builder.append(" 'version' => '").append(escape(info.getVersion())).append("',\r\n"); + builder.append(" 'priority' => ").append(info.getPriority()).append(",\r\n"); + builder.append(" 'location' => '").append(escape(info.getLocation())).append("',\r\n"); + builder.append(");\r\n\r\n"); + } + + builder.append("$out = array('minimumVersion' => ").append(PackageList.MIN_VERSION).append(", 'packages' => $packages);\r\n"); + builder.append("header('Content-Type: text/plain; charset=utf-8');\r\n"); + builder.append("echo json_encode($out);\r\n"); + return builder.toString(); + } + + private String escape(String t) { + return t.replaceAll("[\\\\\"']", "\\$0").replace("\r", "\\r").replace("\n", "\\n"); + } + + private String escapeKeys(List list) { + return Joiner.on(", ").join(list.stream().map(s -> "'" + escape(s.toLowerCase()) + "'").collect(Collectors.toList())); + } + }; + + @Getter private final String name; + @Getter private final boolean gameKeyCompatible; + @Getter private final String filename; + + ListingType(String name, boolean gameKeyCompatible, String filename) { + this.name = name; + this.gameKeyCompatible = gameKeyCompatible; + this.filename = filename; + } + + @Override + public String toString() { + return name; + } + + public abstract String generate(List entries) throws IOException; + +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/model/swing/ListingTypeComboBoxModel.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/model/swing/ListingTypeComboBoxModel.java new file mode 100644 index 0000000..2c1ca28 --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/model/swing/ListingTypeComboBoxModel.java @@ -0,0 +1,35 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.model.swing; + +import javax.swing.*; + +public class ListingTypeComboBoxModel extends AbstractListModel implements ComboBoxModel { + + private ListingType selection = ListingType.STATIC; + + @Override + public void setSelectedItem(Object anItem) { + selection = (ListingType) anItem; + } + + @Override + public ListingType getSelectedItem() { + return selection; + } + + @Override + public int getSize() { + return ListingType.values().length; + } + + @Override + public ListingType getElementAt(int index) { + return ListingType.values()[index]; + } + +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/model/swing/ManifestEntryTableModel.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/model/swing/ManifestEntryTableModel.java new file mode 100644 index 0000000..304fc2b --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/model/swing/ManifestEntryTableModel.java @@ -0,0 +1,122 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.model.swing; + +import com.google.common.base.Joiner; +import com.skcraft.launcher.creator.model.creator.ManifestEntry; + +import javax.swing.table.AbstractTableModel; +import java.util.List; + +public class ManifestEntryTableModel extends AbstractTableModel { + + private final Joiner GAME_KEY_JOINER = Joiner.on(", "); + private final List entries; + + public ManifestEntryTableModel(List entries) { + this.entries = entries; + } + + @Override + public String getColumnName(int columnIndex) { + switch (columnIndex) { + case 0: + return ""; + case 1: + return "Modpack"; + case 2: + return "Version"; + case 3: + return "Priority"; + case 4: + return "Location"; + case 5: + return "Game Keys"; + default: + return null; + } + } + + @Override + public Class getColumnClass(int columnIndex) { + switch (columnIndex) { + case 0: + return Boolean.class; + case 1: + return String.class; + case 2: + return String.class; + case 3: + return Integer.class; + case 4: + return String.class; + case 5: + return String.class; + default: + return null; + } + } + @Override + public void setValueAt(Object value, int rowIndex, int columnIndex) { + switch (columnIndex) { + case 0: + entries.get(rowIndex).setSelected((Boolean) value); + break; + default: + break; + } + } + + @Override + public boolean isCellEditable(int rowIndex, int columnIndex) { + switch (columnIndex) { + case 0: + return true; + default: + return false; + } + } + + @Override + public int getRowCount() { + return entries.size(); + } + + @Override + public int getColumnCount() { + return 6; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + ManifestEntry entry = entries.get(rowIndex); + + switch (columnIndex) { + case 0: + return entry.isSelected(); + case 1: + String title = entry.getManifestInfo().getTitle(); + if (title != null) { + return title; + } else { + return entry.getManifestInfo().getName(); + } + case 2: + return entry.getManifestInfo().getVersion(); + case 3: + return entry.getManifestInfo().getPriority(); + case 4: + return entry.getManifestInfo().getLocation(); + case 5: + List gameKeys = entry.getGameKeys(); + return gameKeys != null ? GAME_KEY_JOINER.join(gameKeys) : ""; + default: + return null; + } + } + +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/model/swing/ModFileTableModel.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/model/swing/ModFileTableModel.java new file mode 100644 index 0000000..1130c52 --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/model/swing/ModFileTableModel.java @@ -0,0 +1,101 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.model.swing; + +import com.skcraft.launcher.creator.Creator; +import com.skcraft.launcher.creator.model.creator.ModFile; +import com.skcraft.launcher.swing.SwingHelper; + +import javax.annotation.Nullable; +import javax.swing.*; +import javax.swing.table.AbstractTableModel; +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class ModFileTableModel extends AbstractTableModel { + + private static final Icon WWW_ICON; + private final List mods; + + static { + WWW_ICON = SwingHelper.createIcon(Creator.class, "www_icon.png"); + } + + public ModFileTableModel(List mods) { + checkNotNull(mods, "mods"); + this.mods = mods; + } + + @Override + public String getColumnName(int columnIndex) { + switch (columnIndex) { + case 0: return ""; + case 1: return "Mod"; + case 2: return "Version"; + case 3: return "Latest Release"; + case 4: return "Latest Dev"; + case 5: return "Mod ID"; + case 6: return "Filename"; + default: return null; + } + } + + @Override + public Class getColumnClass(int columnIndex) { + switch (columnIndex) { + case 0: + return Icon.class; + default: + return String.class; + } + } + + @Override + public void setValueAt(Object value, int rowIndex, int columnIndex) { + } + + @Override + public boolean isCellEditable(int rowIndex, int columnIndex) { + return false; + } + + @Override + public int getRowCount() { + return mods.size(); + } + + @Override + public int getColumnCount() { + return 7; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + @Nullable ModFile mod = mods.get(rowIndex); + + if (mod == null) { + return null; + } + + switch (columnIndex) { + case 0: return mod.getUrl() != null ? WWW_ICON : null; + case 1: return mod.getName() != null ? mod.getName() : mod.getFile().getName(); + case 2: return mod.getCleanVersion(); + case 3: return mod.getLatestVersion(); + case 4: return mod.getLatestDevVersion(); + case 5: return mod.getModId(); + case 6: return mod.getFile().getName(); + default: return null; + } + } + + public ModFile getMod(int index) { + return mods.get(index); + } + +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/model/swing/PackTableModel.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/model/swing/PackTableModel.java new file mode 100644 index 0000000..528e515 --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/model/swing/PackTableModel.java @@ -0,0 +1,100 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.model.swing; + +import com.skcraft.launcher.builder.BuilderConfig; +import com.skcraft.launcher.creator.Creator; +import com.skcraft.launcher.creator.model.creator.Pack; +import com.skcraft.launcher.swing.SwingHelper; + +import javax.swing.*; +import javax.swing.table.AbstractTableModel; +import java.util.List; + +public class PackTableModel extends AbstractTableModel { + + private final Icon instanceIcon; + private final Icon warningIcon; + private final List packs; + + public PackTableModel(List packs) { + this.packs = packs; + + instanceIcon = SwingHelper.createIcon(Creator.class, "pack_icon.png"); + warningIcon = SwingHelper.createIcon(Creator.class, "warning_icon.png"); + } + + @Override + public String getColumnName(int columnIndex) { + switch (columnIndex) { + case 0: + return ""; + case 1: + return "Name"; + case 2: + return "Title"; + case 3: + return "Game Version"; + case 4: + return "Location"; + default: + return null; + } + } + + @Override + public Class getColumnClass(int columnIndex) { + switch (columnIndex) { + case 0: + return Icon.class; + default: + return String.class; + } + } + + @Override + public void setValueAt(Object value, int rowIndex, int columnIndex) { + } + + @Override + public boolean isCellEditable(int rowIndex, int columnIndex) { + return false; + } + + @Override + public int getRowCount() { + return packs.size(); + } + + @Override + public int getColumnCount() { + return 5; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + Pack pack = packs.get(rowIndex); + + BuilderConfig config = pack.getCachedConfig(); + + switch (columnIndex) { + case 0: + return config != null ? instanceIcon : warningIcon; + case 1: + return config != null ? config.getName() : ""; + case 2: + return config != null ? config.getTitle() : "?"; + case 3: + return config != null ? config.getGameVersion() : "?"; + case 4: + return pack.getLocation(); + default: + return null; + } + } + +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/model/swing/ProblemTableModel.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/model/swing/ProblemTableModel.java new file mode 100644 index 0000000..5758886 --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/model/swing/ProblemTableModel.java @@ -0,0 +1,90 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.model.swing; + +import com.skcraft.launcher.creator.Creator; +import com.skcraft.launcher.creator.model.creator.Problem; +import com.skcraft.launcher.swing.SwingHelper; + +import javax.swing.*; +import javax.swing.table.AbstractTableModel; +import java.util.List; + +public class ProblemTableModel extends AbstractTableModel { + + private static final Icon WARNING_ICON; + + static { + WARNING_ICON = SwingHelper.createIcon(Creator.class, "warning_icon.png"); + } + + private final List problems; + + public ProblemTableModel(List problems) { + this.problems = problems; + } + + @Override + public String getColumnName(int columnIndex) { + switch (columnIndex) { + case 0: + return ""; + case 1: + return "Problem"; + default: + return null; + } + } + + @Override + public Class getColumnClass(int columnIndex) { + switch (columnIndex) { + case 0: + return Icon.class; + case 1: + return Problem.class; + default: + return null; + } + } + + @Override + public void setValueAt(Object value, int rowIndex, int columnIndex) { + } + + @Override + public boolean isCellEditable(int rowIndex, int columnIndex) { + return false; + } + + @Override + public int getRowCount() { + return problems.size(); + } + + @Override + public int getColumnCount() { + return 2; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + switch (columnIndex) { + case 0: + return WARNING_ICON; + case 1: + return problems.get(rowIndex); + default: + return null; + } + } + + public Problem getProblem(int index) { + return problems.get(index); + } + +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/model/swing/RecentListModel.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/model/swing/RecentListModel.java new file mode 100644 index 0000000..2852147 --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/model/swing/RecentListModel.java @@ -0,0 +1,35 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.model.swing; + +import com.skcraft.launcher.creator.model.creator.RecentEntry; + +import javax.swing.*; +import java.util.List; + +public class RecentListModel extends AbstractListModel { + + private final List recentEntries; + + public RecentListModel(List recentEntries) { + this.recentEntries = recentEntries; + } + + @Override + public int getSize() { + return recentEntries.size(); + } + + @Override + public RecentEntry getElementAt(int index) { + return recentEntries.get(index); + } + + public void fireUpdate() { + fireContentsChanged(this, 0, Integer.MAX_VALUE); + } +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/model/swing/RecommendationComboBoxModel.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/model/swing/RecommendationComboBoxModel.java new file mode 100644 index 0000000..2bd78c6 --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/model/swing/RecommendationComboBoxModel.java @@ -0,0 +1,40 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.model.swing; + +import com.skcraft.launcher.model.modpack.Feature.Recommendation; + +import javax.swing.*; + +public class RecommendationComboBoxModel extends AbstractListModel implements ComboBoxModel { + + private Recommendation selection; + + @Override + public void setSelectedItem(Object anItem) { + selection = (Recommendation) anItem; + } + + @Override + public Object getSelectedItem() { + return selection; + } + + @Override + public int getSize() { + return Recommendation.values().length + 1; + } + + @Override + public Object getElementAt(int index) { + if (index == 0) { + return null; + } else { + return Recommendation.values()[index - 1]; + } + } +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/server/LatestHandler.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/server/LatestHandler.java new file mode 100644 index 0000000..aa4f752 --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/server/LatestHandler.java @@ -0,0 +1,41 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.server; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.skcraft.launcher.selfupdate.LatestVersionInfo; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.handler.AbstractHandler; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.URL; + +class LatestHandler extends AbstractHandler { + + private final ObjectMapper mapper; + + public LatestHandler(ObjectMapper mapper) { + this.mapper = mapper; + } + + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { + response.setContentType("text/plain; charset=utf-8"); + response.setStatus(HttpServletResponse.SC_OK); + + LatestVersionInfo info = new LatestVersionInfo(); + info.setVersion("0.0.0"); + info.setUrl(new URL("http://localhost")); + mapper.writeValue(response.getWriter(), info); + + baseRequest.setHandled(true); + } + +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/server/NewsHandler.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/server/NewsHandler.java new file mode 100644 index 0000000..2e5e607 --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/server/NewsHandler.java @@ -0,0 +1,39 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.server; + +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.handler.AbstractHandler; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; + +class NewsHandler extends AbstractHandler { + + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { + response.setContentType("text/html; charset=utf-8"); + response.setStatus(HttpServletResponse.SC_OK); + + PrintWriter writer = response.getWriter(); + writer.write(""); + writer.write(""); + writer.write(""); + writer.write("Staging Tool"); + writer.write(""); + writer.write(""); + writer.write("

Welcome to the staging tool!

"); + writer.write(""); + writer.write(""); + + baseRequest.setHandled(true); + } + +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/server/PackagesHandler.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/server/PackagesHandler.java new file mode 100644 index 0000000..2468c0c --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/server/PackagesHandler.java @@ -0,0 +1,66 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.server; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.Lists; +import com.skcraft.launcher.model.modpack.Manifest; +import com.skcraft.launcher.model.modpack.ManifestInfo; +import com.skcraft.launcher.model.modpack.PackageList; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.handler.AbstractHandler; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.util.List; + +class PackagesHandler extends AbstractHandler { + + private final ObjectMapper mapper; + private final File baseDir; + + public PackagesHandler(ObjectMapper mapper, File baseDir) { + this.mapper = mapper; + this.baseDir = baseDir; + } + + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { + response.setContentType("text/plain; charset=utf-8"); + response.setStatus(HttpServletResponse.SC_OK); + + List packages = Lists.newArrayList(); + PackageList packageList = new PackageList(); + packageList.setPackages(packages); + + File[] files = baseDir.listFiles(new PackageFileFilter()); + if (files != null) { + for (File file : files) { + Manifest manifest = mapper.readValue(file, Manifest.class); + ManifestInfo info = new ManifestInfo(); + info.setName(manifest.getName()); + info.setTitle(manifest.getTitle()); + info.setVersion(manifest.getVersion()); + info.setLocation(file.getName()); + packages.add(info); + } + } + + mapper.writeValue(response.getWriter(), packageList); + baseRequest.setHandled(true); + } + + private static class PackageFileFilter implements FileFilter { + @Override + public boolean accept(File pathname) { + return pathname.getName().toLowerCase().endsWith(".json"); + } + } +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/server/TestServer.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/server/TestServer.java new file mode 100644 index 0000000..a2750e1 --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/server/TestServer.java @@ -0,0 +1,32 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.server; + +import lombok.Getter; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; + +public class TestServer { + + @Getter private final Server server; + + public TestServer(Server server) { + this.server = server; + } + + public void start() throws Exception { + getServer().start(); + } + + public int getLocalPort() { + return ((ServerConnector) server.getConnectors()[0]).getLocalPort(); + } + + public void stop() throws Exception { + getServer().stop(); + } +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/server/TestServerBuilder.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/server/TestServerBuilder.java new file mode 100644 index 0000000..2a02ad4 --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/server/TestServerBuilder.java @@ -0,0 +1,81 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.server; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.ContextHandler; +import org.eclipse.jetty.server.handler.ContextHandlerCollection; +import org.eclipse.jetty.server.handler.ErrorHandler; +import org.eclipse.jetty.server.handler.ResourceHandler; +import org.eclipse.jetty.server.handler.gzip.GzipHandler; + +import java.io.File; + +public class TestServerBuilder { + + private File baseDir = new File("."); + private int port = 28888; + + public File getBaseDir() { + return baseDir; + } + + public TestServerBuilder setBaseDir(File baseDir) { + this.baseDir = baseDir; + return this; + } + + public int getPort() { + return port; + } + + public TestServerBuilder setPort(int port) { + this.port = port; + return this; + } + + public TestServer build() { + Server server = new Server(port); + + ObjectMapper mapper = new ObjectMapper(); + + ResourceHandler resourceHandler = new ResourceHandler(); + resourceHandler.setDirectoriesListed(true); + resourceHandler.setResourceBase(baseDir.getAbsolutePath()); + resourceHandler.setMinMemoryMappedContentLength(-1); // Causes file locking on Windows + + ContextHandler rootContext = new ContextHandler(); + rootContext.setContextPath("/"); + rootContext.setHandler(resourceHandler); + + ContextHandler packagesContext = new ContextHandler("/packages.json"); + packagesContext.setAllowNullPathInfo(true); + packagesContext.setHandler(new PackagesHandler(mapper, baseDir)); + + ContextHandler latestContext = new ContextHandler("/latest.json"); + latestContext.setAllowNullPathInfo(true); + latestContext.setHandler(new LatestHandler(mapper)); + + ContextHandler newsContext = new ContextHandler("/news.html"); + newsContext.setAllowNullPathInfo(true); + newsContext.setHandler(new NewsHandler()); + + ContextHandlerCollection contexts = new ContextHandlerCollection(); + contexts.setHandlers(new Handler[]{packagesContext, latestContext, newsContext, rootContext}); + + GzipHandler gzip = new GzipHandler(); + server.setHandler(gzip); + gzip.setHandler(contexts); + + server.addBean(new ErrorHandler()); + + return new TestServer(server); + } + +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/swing/BorderCellRenderer.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/swing/BorderCellRenderer.java new file mode 100644 index 0000000..00ca509 --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/swing/BorderCellRenderer.java @@ -0,0 +1,28 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.swing; + +import javax.swing.*; +import javax.swing.border.Border; +import java.awt.*; + +public class BorderCellRenderer implements ListCellRenderer { + + private final Border border; + private final DefaultListCellRenderer defaultRenderer = new DefaultListCellRenderer(); + + public BorderCellRenderer(Border border) { + this.border = border; + } + + public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { + JLabel renderer = (JLabel) defaultRenderer.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + renderer.setBorder(border); + return renderer; + } + +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/swing/PackDirectoryFilter.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/swing/PackDirectoryFilter.java new file mode 100644 index 0000000..333c859 --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/swing/PackDirectoryFilter.java @@ -0,0 +1,26 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.swing; + +import com.skcraft.launcher.creator.model.creator.Workspace; + +import javax.swing.filechooser.FileFilter; +import java.io.File; + +public class PackDirectoryFilter extends FileFilter { + + @Override + public boolean accept(File f) { + return f.isDirectory() && !f.getName().equals(Workspace.DIR_NAME); + } + + @Override + public String getDescription() { + return "Directories"; + } + +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/swing/WorkspaceDirectoryFilter.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/swing/WorkspaceDirectoryFilter.java new file mode 100644 index 0000000..4e936b5 --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/swing/WorkspaceDirectoryFilter.java @@ -0,0 +1,24 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.swing; + +import javax.swing.filechooser.FileFilter; +import java.io.File; + +public class WorkspaceDirectoryFilter extends FileFilter { + + @Override + public boolean accept(File f) { + return f.isDirectory(); + } + + @Override + public String getDescription() { + return "Directories"; + } + +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/util/ModInfoReader.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/util/ModInfoReader.java new file mode 100644 index 0000000..a0363f7 --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/util/ModInfoReader.java @@ -0,0 +1,169 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.util; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Charsets; +import com.google.common.collect.ImmutableList; +import com.google.common.io.CharStreams; +import com.google.common.io.Closer; +import lombok.Data; +import lombok.extern.java.Log; + +import java.io.*; +import java.util.Collections; +import java.util.List; +import java.util.logging.Level; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +/** + * Reads the mod information file from a mod .jar, with support for both Forge + * and LiteLoader. + */ +@Log +public class ModInfoReader { + + private static final String FORGE_INFO_FILENAME = "mcmod.info"; + private static final String LITELOADER_INFO_FILENAME = "litemod.json"; + private final ObjectMapper mapper = new ObjectMapper(); + + /** + * Detect the mods listed in the given .jar + * + * @param file The file + * @return A list of detected mods + */ + public List detectMods(File file) { + Closer closer = Closer.create(); + + try { + FileInputStream fis = closer.register(new FileInputStream(file)); + BufferedInputStream bis = closer.register(new BufferedInputStream(fis)); + ZipInputStream zis = closer.register(new ZipInputStream(bis)); + + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + if (entry.getName().equalsIgnoreCase(FORGE_INFO_FILENAME)) { + List mods; + String content = CharStreams.toString(new InputStreamReader(zis, Charsets.UTF_8)); + + try { + mods = mapper.readValue(content, ForgeModManifest.class).getMods(); + } catch (JsonMappingException | JsonParseException e) { + mods = mapper.readValue(content, new TypeReference>() {}); + } + + if (mods != null) { + // Ignore "examplemod" + return Collections.unmodifiableList( + mods.stream() + .filter(info -> !info.getModId().equals("examplemod")) + .collect(Collectors.toList())); + } else { + return Collections.emptyList(); + } + + } else if (entry.getName().equalsIgnoreCase(LITELOADER_INFO_FILENAME)) { + String content = CharStreams.toString(new InputStreamReader(zis, Charsets.UTF_8)); + return new ImmutableList.Builder().add(mapper.readValue(content, LiteLoaderModInfo.class)).build(); + } + } + + return Collections.emptyList(); + } catch (JsonMappingException e) { + log.log(Level.WARNING, "Unknown format " + FORGE_INFO_FILENAME + " file in " + file.getAbsolutePath(), e); + return Collections.emptyList(); + } catch (JsonParseException e) { + log.log(Level.WARNING, "Corrupt " + FORGE_INFO_FILENAME + " file in " + file.getAbsolutePath(), e); + return Collections.emptyList(); + } catch (IOException e) { + log.log(Level.WARNING, "Failed to read " + file.getAbsolutePath(), e); + return Collections.emptyList(); + } finally { + try { + closer.close(); + } catch (IOException ignored) { + } + } + } + + public interface ModInfo { + + String getModId(); + String getName(); + String getDescription(); + String getVersion(); + String getGameVersion(); + String getUrl(); + + } + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + private static class ForgeModInfo implements ModInfo { + + @JsonProperty("modid") + private String modId; + private String name; + private String description; + private String version; + @JsonProperty("mcversion") + private String gameVersion; + private String url; + private String updateUrl; + private List authorList; + private String credits; + private List dependencies; + + } + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + private static class ForgeModManifest { + + @JsonProperty("modListVersion") + private int version; + @JsonProperty("modList") + private List mods; + + } + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + private static class LiteLoaderModInfo implements ModInfo { + + private String name; + private String version; + @JsonProperty("mcversion") + private String gameVersion; + private String revision; + private String author; + private String description; + + @JsonIgnore + @Override + public String getModId() { + return null; + } + + @Override + public String getUrl() { + return null; + } + + } + + +} diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/util/NemModList.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/util/NemModList.java new file mode 100644 index 0000000..89a3d82 --- /dev/null +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/util/NemModList.java @@ -0,0 +1,91 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.creator.util; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.type.TypeReference; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import com.skcraft.launcher.util.HttpRequest; +import lombok.Data; +import lombok.Getter; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.net.URL; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class NemModList { + + @Getter + private Map mods = ImmutableMap.of(); + + public void load(String version) throws IOException, InterruptedException { + checkNotNull(version, "version"); + + List mods = HttpRequest.get(HttpRequest.url("https://bot.notenoughmods.com/" + version + ".json")) + .execute() + .expectResponseCode(200) + .returnContent() + .asJson(new TypeReference>() {}); + + Map index = Maps.newHashMap(); + + for (ModEntry entry : mods) { + index.put(entry.getModId(), entry); + } + + this.mods = Collections.unmodifiableMap(index); + } + + @Nullable + public ModEntry get(String modId) { + checkNotNull(modId, "modId"); + return mods.get(modId); + } + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class ModEntry { + + @JsonProperty("modid") + private String modId; + private String name; + private List aliases; + + @JsonProperty("version") + private String latestVersion; + @JsonProperty("dev") + private String latestDevVersion; + @JsonProperty("prevversion") + private String previousVersion; + + private List dependencies; + + @JsonProperty("longurl") + private URL url; + private List tags; + private String comment; + private String author; + + private String license; + private URL repo; + + @JsonProperty("added_at") + private Date addedAt; + @JsonProperty("lastupdated") + private Date lastUpdated; + + } + +} diff --git a/creator-tools/src/main/resources/com/skcraft/launcher/creator/about_header.png b/creator-tools/src/main/resources/com/skcraft/launcher/creator/about_header.png new file mode 100644 index 0000000..0986006 Binary files /dev/null and b/creator-tools/src/main/resources/com/skcraft/launcher/creator/about_header.png differ diff --git a/creator-tools/src/main/resources/com/skcraft/launcher/creator/build.png b/creator-tools/src/main/resources/com/skcraft/launcher/creator/build.png new file mode 100644 index 0000000..9360f7d Binary files /dev/null and b/creator-tools/src/main/resources/com/skcraft/launcher/creator/build.png differ diff --git a/creator-tools/src/main/resources/com/skcraft/launcher/creator/check.png b/creator-tools/src/main/resources/com/skcraft/launcher/creator/check.png new file mode 100644 index 0000000..aab0b96 Binary files /dev/null and b/creator-tools/src/main/resources/com/skcraft/launcher/creator/check.png differ diff --git a/creator-tools/src/main/resources/com/skcraft/launcher/creator/clean.png b/creator-tools/src/main/resources/com/skcraft/launcher/creator/clean.png new file mode 100644 index 0000000..2c5d827 Binary files /dev/null and b/creator-tools/src/main/resources/com/skcraft/launcher/creator/clean.png differ diff --git a/creator-tools/src/main/resources/com/skcraft/launcher/creator/creator.properties b/creator-tools/src/main/resources/com/skcraft/launcher/creator/creator.properties new file mode 100644 index 0000000..4b5c12b --- /dev/null +++ b/creator-tools/src/main/resources/com/skcraft/launcher/creator/creator.properties @@ -0,0 +1,7 @@ +# +# SK's Minecraft Launcher +# Copyright (C) 2010-2014 Albert Pham and contributors +# Please see LICENSE.txt for license information. +# + +version=${project.version} diff --git a/creator-tools/src/main/resources/com/skcraft/launcher/creator/edit.png b/creator-tools/src/main/resources/com/skcraft/launcher/creator/edit.png new file mode 100644 index 0000000..6997784 Binary files /dev/null and b/creator-tools/src/main/resources/com/skcraft/launcher/creator/edit.png differ diff --git a/creator-tools/src/main/resources/com/skcraft/launcher/creator/help.png b/creator-tools/src/main/resources/com/skcraft/launcher/creator/help.png new file mode 100644 index 0000000..7bb1c35 Binary files /dev/null and b/creator-tools/src/main/resources/com/skcraft/launcher/creator/help.png differ diff --git a/creator-tools/src/main/resources/com/skcraft/launcher/creator/icon.png b/creator-tools/src/main/resources/com/skcraft/launcher/creator/icon.png new file mode 100644 index 0000000..03aa713 Binary files /dev/null and b/creator-tools/src/main/resources/com/skcraft/launcher/creator/icon.png differ diff --git a/creator-tools/src/main/resources/com/skcraft/launcher/creator/import.png b/creator-tools/src/main/resources/com/skcraft/launcher/creator/import.png new file mode 100644 index 0000000..e044513 Binary files /dev/null and b/creator-tools/src/main/resources/com/skcraft/launcher/creator/import.png differ diff --git a/creator-tools/src/main/resources/com/skcraft/launcher/creator/log.png b/creator-tools/src/main/resources/com/skcraft/launcher/creator/log.png new file mode 100644 index 0000000..296df88 Binary files /dev/null and b/creator-tools/src/main/resources/com/skcraft/launcher/creator/log.png differ diff --git a/creator-tools/src/main/resources/com/skcraft/launcher/creator/new.png b/creator-tools/src/main/resources/com/skcraft/launcher/creator/new.png new file mode 100644 index 0000000..ce39f47 Binary files /dev/null and b/creator-tools/src/main/resources/com/skcraft/launcher/creator/new.png differ diff --git a/creator-tools/src/main/resources/com/skcraft/launcher/creator/open_folder.png b/creator-tools/src/main/resources/com/skcraft/launcher/creator/open_folder.png new file mode 100644 index 0000000..3d11f52 Binary files /dev/null and b/creator-tools/src/main/resources/com/skcraft/launcher/creator/open_folder.png differ diff --git a/creator-tools/src/main/resources/com/skcraft/launcher/creator/options.png b/creator-tools/src/main/resources/com/skcraft/launcher/creator/options.png new file mode 100644 index 0000000..39b0a41 Binary files /dev/null and b/creator-tools/src/main/resources/com/skcraft/launcher/creator/options.png differ diff --git a/creator-tools/src/main/resources/com/skcraft/launcher/creator/pack.png b/creator-tools/src/main/resources/com/skcraft/launcher/creator/pack.png new file mode 100644 index 0000000..8737899 Binary files /dev/null and b/creator-tools/src/main/resources/com/skcraft/launcher/creator/pack.png differ diff --git a/creator-tools/src/main/resources/com/skcraft/launcher/creator/pack_icon.png b/creator-tools/src/main/resources/com/skcraft/launcher/creator/pack_icon.png new file mode 100644 index 0000000..5322f13 Binary files /dev/null and b/creator-tools/src/main/resources/com/skcraft/launcher/creator/pack_icon.png differ diff --git a/creator-tools/src/main/resources/com/skcraft/launcher/creator/server.png b/creator-tools/src/main/resources/com/skcraft/launcher/creator/server.png new file mode 100644 index 0000000..0c2c232 Binary files /dev/null and b/creator-tools/src/main/resources/com/skcraft/launcher/creator/server.png differ diff --git a/creator-tools/src/main/resources/com/skcraft/launcher/creator/test.png b/creator-tools/src/main/resources/com/skcraft/launcher/creator/test.png new file mode 100644 index 0000000..7acd97a Binary files /dev/null and b/creator-tools/src/main/resources/com/skcraft/launcher/creator/test.png differ diff --git a/creator-tools/src/main/resources/com/skcraft/launcher/creator/warning_icon.png b/creator-tools/src/main/resources/com/skcraft/launcher/creator/warning_icon.png new file mode 100644 index 0000000..209b290 Binary files /dev/null and b/creator-tools/src/main/resources/com/skcraft/launcher/creator/warning_icon.png differ diff --git a/creator-tools/src/main/resources/com/skcraft/launcher/creator/welcome_logo.png b/creator-tools/src/main/resources/com/skcraft/launcher/creator/welcome_logo.png new file mode 100644 index 0000000..cc1602d Binary files /dev/null and b/creator-tools/src/main/resources/com/skcraft/launcher/creator/welcome_logo.png differ diff --git a/creator-tools/src/main/resources/com/skcraft/launcher/creator/www_icon.png b/creator-tools/src/main/resources/com/skcraft/launcher/creator/www_icon.png new file mode 100644 index 0000000..9a60fed Binary files /dev/null and b/creator-tools/src/main/resources/com/skcraft/launcher/creator/www_icon.png differ diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e708b1c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..aa991fc --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..4f906e0 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/launcher-bootstrap/build.gradle b/launcher-bootstrap/build.gradle new file mode 100644 index 0000000..12969d6 --- /dev/null +++ b/launcher-bootstrap/build.gradle @@ -0,0 +1,30 @@ +plugins { + id 'application' + id "com.github.johnrengelman.shadow" + id 'io.freefair.lombok' +} + +application { + mainClassName = "com.skcraft.launcher.Bootstrap" +} + +dependencies { + implementation 'com.googlecode.json-simple:json-simple:1.1.1' + implementation 'javax.xml.bind:jaxb-api:2.3.1' +} + +processResources { + filesMatching('**/*.properties') { + filter { + it.replace('${project.version}', project.version) + } + } +} + +shadowJar { + archiveClassifier.set("") +} + +build { + dependsOn(shadowJar) +} diff --git a/launcher-bootstrap/lombok.config b/launcher-bootstrap/lombok.config new file mode 100644 index 0000000..6aa51d7 --- /dev/null +++ b/launcher-bootstrap/lombok.config @@ -0,0 +1,2 @@ +# This file is generated by the 'io.freefair.lombok' Gradle plugin +config.stopBubbling = true diff --git a/launcher-bootstrap/src/main/java/com/skcraft/launcher/Bootstrap.java b/launcher-bootstrap/src/main/java/com/skcraft/launcher/Bootstrap.java new file mode 100644 index 0000000..42a913f --- /dev/null +++ b/launcher-bootstrap/src/main/java/com/skcraft/launcher/Bootstrap.java @@ -0,0 +1,219 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher; + +import com.skcraft.launcher.bootstrap.*; +import lombok.Getter; +import lombok.extern.java.Log; + +import javax.swing.*; +import javax.swing.filechooser.FileSystemView; +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.*; +import java.util.logging.Level; + +import static com.skcraft.launcher.bootstrap.SharedLocale.tr; + +@Log +public class Bootstrap { + + private static final int BOOTSTRAP_VERSION = 1; + + @Getter private final File baseDir; + @Getter private final boolean portable; + @Getter private final File binariesDir; + @Getter private final Properties properties; + private final String[] originalArgs; + + public static void main(String[] args) throws Throwable { + SimpleLogFormatter.configureGlobalLogger(); + SharedLocale.loadBundle("com.skcraft.launcher.lang.Bootstrap", Locale.getDefault()); + + boolean portable = isPortableMode(); + + Bootstrap bootstrap = new Bootstrap(portable, args); + try { + bootstrap.cleanup(); + bootstrap.launch(); + } catch (Throwable t) { + Bootstrap.log.log(Level.WARNING, "Error", t); + Bootstrap.setSwingLookAndFeel(); + SwingHelper.showErrorDialog(null, tr("errors.bootstrapError"), tr("errorTitle"), t); + } + } + + public Bootstrap(boolean portable, String[] args) throws IOException { + this.properties = BootstrapUtils.loadProperties(Bootstrap.class, "bootstrap.properties"); + + File baseDir = portable ? new File(".") : getUserLauncherDir(); + + this.baseDir = baseDir; + this.portable = portable; + this.binariesDir = new File(baseDir, "launcher"); + this.originalArgs = args; + + binariesDir.mkdirs(); + } + + public void cleanup() { + File[] files = binariesDir.listFiles(new FileFilter() { + @Override + public boolean accept(File pathname) { + return pathname.getName().endsWith(".tmp"); + } + }); + + if (files != null) { + for (File file : files) { + file.delete(); + } + } + } + + public void launch() throws Throwable { + File[] files = binariesDir.listFiles(new LauncherBinary.Filter()); + List binaries = new ArrayList(); + + if (files != null) { + for (File file : files) { + Bootstrap.log.info("Found " + file.getAbsolutePath() + "..."); + binaries.add(new LauncherBinary(file)); + } + } + + if (!binaries.isEmpty()) { + launchExisting(binaries, true); + } else { + launchInitial(); + } + } + + public void launchInitial() throws Exception { + Bootstrap.log.info("Downloading the launcher..."); + Thread thread = new Thread(new Downloader(this)); + thread.start(); + } + + public void launchExisting(List binaries, boolean redownload) throws Exception { + Collections.sort(binaries); + LauncherBinary working = null; + Class clazz = null; + + for (LauncherBinary binary : binaries) { + File testFile = binary.getPath(); + try { + testFile = binary.getExecutableJar(); + Bootstrap.log.info("Trying " + testFile.getAbsolutePath() + "..."); + clazz = load(testFile); + Bootstrap.log.info("Launcher loaded successfully."); + working = binary; + break; + } catch (Throwable t) { + Bootstrap.log.log(Level.WARNING, "Failed to load " + testFile.getAbsoluteFile(), t); + } + } + + if (working != null) { + for (LauncherBinary binary : binaries) { + if (working != binary) { + log.info("Removing " + binary.getPath() + "..."); + binary.remove(); + } + } + + execute(clazz); + } else { + if (redownload) { + launchInitial(); + } else { + throw new IOException("Failed to find launchable .jar"); + } + } + } + + public void execute(Class clazz) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException { + Method method = clazz.getDeclaredMethod("main", String[].class); + String[] launcherArgs; + + if (portable) { + launcherArgs = new String[] { + "--portable", + "--dir", + baseDir.getAbsolutePath(), + "--bootstrap-version", + String.valueOf(BOOTSTRAP_VERSION) }; + } else { + launcherArgs = new String[] { + "--dir", + baseDir.getAbsolutePath(), + "--bootstrap-version", + String.valueOf(BOOTSTRAP_VERSION) }; + } + + String[] args = new String[originalArgs.length + launcherArgs.length]; + System.arraycopy(launcherArgs, 0, args, 0, launcherArgs.length); + System.arraycopy(originalArgs, 0, args, launcherArgs.length, originalArgs.length); + + log.info("Launching with arguments " + Arrays.toString(args)); + + method.invoke(null, new Object[] { args }); + } + + public Class load(File jarFile) throws MalformedURLException, ClassNotFoundException { + URL[] urls = new URL[] { jarFile.toURI().toURL() }; + URLClassLoader child = new URLClassLoader(urls, this.getClass().getClassLoader()); + Class clazz = Class.forName(getProperties().getProperty("launcherClass"), true, child); + return clazz; + } + + public static void setSwingLookAndFeel() { + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (Throwable e) { + } + } + + private static File getFileChooseDefaultDir() { + JFileChooser chooser = new JFileChooser(); + FileSystemView fsv = chooser.getFileSystemView(); + return fsv.getDefaultDirectory(); + } + + private File getUserLauncherDir() { + String osName = System.getProperty("os.name").toLowerCase(); + if (osName.contains("win")) { + return new File(getFileChooseDefaultDir(), getProperties().getProperty("homeFolderWindows")); + } + + File dotFolder = new File(System.getProperty("user.home"), getProperties().getProperty("homeFolder")); + String xdgFolderName = getProperties().getProperty("homeFolderLinux"); + + if (osName.contains("linux") && !dotFolder.exists() && xdgFolderName != null && !xdgFolderName.isEmpty()) { + String xdgDataHome = System.getenv("XDG_DATA_HOME"); + if (xdgDataHome.isEmpty()) { + xdgDataHome = System.getProperty("user.home") + "/.local/share"; + } + + return new File(xdgDataHome, xdgFolderName); + } + + return dotFolder; + } + + private static boolean isPortableMode() { + return new File("portable.txt").exists(); + } + + +} diff --git a/launcher-bootstrap/src/main/java/com/skcraft/launcher/bootstrap/BootstrapUtils.java b/launcher-bootstrap/src/main/java/com/skcraft/launcher/bootstrap/BootstrapUtils.java new file mode 100644 index 0000000..c103e96 --- /dev/null +++ b/launcher-bootstrap/src/main/java/com/skcraft/launcher/bootstrap/BootstrapUtils.java @@ -0,0 +1,49 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.bootstrap; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; +import java.util.regex.Pattern; + +public final class BootstrapUtils { + + private static final Pattern absoluteUrlPattern = Pattern.compile("^[A-Za-z0-9\\-]+://.*$"); + + private BootstrapUtils() { + } + + public static void checkInterrupted() throws InterruptedException { + if (Thread.interrupted()) { + throw new InterruptedException(); + } + } + + public static void closeQuietly(Closeable closeable) { + try { + if (closeable != null) { + closeable.close(); + } + } catch (IOException e) { + } + } + + public static Properties loadProperties(Class clazz, String name) throws IOException { + Properties prop = new Properties(); + InputStream in = null; + try { + in = clazz.getResourceAsStream(name); + prop.load(in); + } finally { + closeQuietly(in); + } + return prop; + } + +} diff --git a/launcher-bootstrap/src/main/java/com/skcraft/launcher/bootstrap/DownloadFrame.java b/launcher-bootstrap/src/main/java/com/skcraft/launcher/bootstrap/DownloadFrame.java new file mode 100644 index 0000000..71fc314 --- /dev/null +++ b/launcher-bootstrap/src/main/java/com/skcraft/launcher/bootstrap/DownloadFrame.java @@ -0,0 +1,149 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.bootstrap; + +import com.skcraft.launcher.Bootstrap; +import lombok.extern.java.Log; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.util.Timer; +import java.util.TimerTask; + +import static com.skcraft.launcher.bootstrap.SharedLocale.tr; + +@Log +public class DownloadFrame extends JFrame { + + private Downloader downloader; + private Timer timer; + + private final JLabel label = new JLabel(); + private final JPanel progressPanel = new JPanel(new BorderLayout(0, 5)); + private final JPanel textAreaPanel = new JPanel(new BorderLayout()); + private final JProgressBar progressBar = new JProgressBar(); + private final LinedBoxPanel buttonsPanel = new LinedBoxPanel(true); + private final JButton cancelButton = new JButton(tr("button.cancal")); + + public DownloadFrame(ProgressObservable observable) { + super(tr("downloader.title")); + setResizable(false); + initComponents(); + label.setText(tr("downloader.pleaseWait")); + setMinimumSize(new Dimension(400, 100)); + pack(); + setLocationRelativeTo(null); + SwingHelper.setIconImage(this, Bootstrap.class, "bootstrapper_icon.png"); + + setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); + addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent event) { + if (confirmCancel()) { + cancel(); + dispose(); + } + } + }); + } + + private void initComponents() { + buttonsPanel.addGlue(); + buttonsPanel.addElement(cancelButton); + buttonsPanel.setBorder(BorderFactory.createEmptyBorder(30, 13, 13, 13));; + + progressBar.setIndeterminate(true); + progressBar.setMinimum(0); + progressBar.setMaximum(1000); + progressBar.setPreferredSize(new Dimension(0, 16)); + + progressPanel.add(label, BorderLayout.NORTH); + progressPanel.setBorder(BorderFactory.createEmptyBorder(13, 13, 0, 13)); + progressPanel.add(progressBar, BorderLayout.CENTER); + textAreaPanel.setBorder(BorderFactory.createEmptyBorder(10, 13, 0, 13)); + + add(progressPanel, BorderLayout.NORTH); + add(textAreaPanel, BorderLayout.CENTER); + add(buttonsPanel, BorderLayout.SOUTH); + + textAreaPanel.setVisible(false); + cancelButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + if (confirmCancel()) { + cancel(); + dispose(); + } + } + }); + } + + private boolean confirmCancel() { + return SwingHelper.confirmDialog(this, tr("progress.confirmCancel"), tr("progress.confirmCancelTitle")); + } + + private void cancel() { + Downloader downloader = this.downloader; + if (downloader != null) { + downloader.cancel(); + } else { + System.exit(0); + } + } + + public synchronized void setDownloader(Downloader downloader) { + this.downloader = downloader; + + if (downloader == null) { + if (timer != null) { + timer.cancel(); + timer = null; + } + } else { + if (timer == null) { + timer = new Timer(); + timer.scheduleAtFixedRate(new UpdateProgress( downloader), 500, 500); + } + } + } + + private class UpdateProgress extends TimerTask { + private final Downloader downloader; + + public UpdateProgress(Downloader downloader) { + this.downloader = downloader; + } + + @Override + public void run() { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + Downloader downloader = DownloadFrame.this.downloader; + if (downloader != null) { + label.setText(downloader.getStatus()); + double progress = downloader.getProgress(); + if (progress < 0) { + progressBar.setIndeterminate(true); + } else { + progressBar.setIndeterminate(false); + progressBar.setValue((int) (1000 * progress)); + } + } else { + label.setText(tr("downloader.pleaseWait")); + progressBar.setIndeterminate(true); + } + } + }); + } + } + +} diff --git a/launcher-bootstrap/src/main/java/com/skcraft/launcher/bootstrap/Downloader.java b/launcher-bootstrap/src/main/java/com/skcraft/launcher/bootstrap/Downloader.java new file mode 100644 index 0000000..3247f4d --- /dev/null +++ b/launcher-bootstrap/src/main/java/com/skcraft/launcher/bootstrap/Downloader.java @@ -0,0 +1,145 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.bootstrap; + +import com.skcraft.launcher.Bootstrap; +import lombok.extern.java.Log; +import org.json.simple.JSONObject; +import org.json.simple.JSONValue; + +import javax.swing.*; +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; + +import static com.skcraft.launcher.bootstrap.BootstrapUtils.checkInterrupted; +import static com.skcraft.launcher.bootstrap.SharedLocale.tr; + +@Log +public class Downloader implements Runnable, ProgressObservable { + + private final Bootstrap bootstrap; + private DownloadFrame dialog; + private HttpRequest httpRequest; + private Thread thread; + + public Downloader(Bootstrap bootstrap) { + this.bootstrap = bootstrap; + } + + @Override + public void run() { + this.thread = Thread.currentThread(); + + try { + execute(); + } catch (InterruptedException e) { + log.log(Level.WARNING, "Interrupted"); + System.exit(0); + } catch (Throwable t) { + log.log(Level.WARNING, "Failed to download launcher", t); + SwingHelper.showErrorDialog(null, tr("errors.failedDownloadError"), tr("errorTitle"), t); + System.exit(0); + } + } + + private void execute() throws Exception { + SwingUtilities.invokeAndWait(new Runnable() { + @Override + public void run() { + Bootstrap.setSwingLookAndFeel(); + dialog = new DownloadFrame(Downloader.this); + dialog.setVisible(true); + dialog.setDownloader(Downloader.this); + } + }); + + URL updateUrl = HttpRequest.url(bootstrap.getProperties().getProperty("latestUrl")); + + log.info("Reading update URL " + updateUrl + "..."); + List binaries = new ArrayList(); + + try { + String data = HttpRequest + .get(updateUrl) + .execute() + .expectResponseCode(200) + .returnContent() + .asString("UTF-8"); + + Object object = JSONValue.parse(data); + URL url; + + if (object instanceof JSONObject) { + String rawUrl = String.valueOf(((JSONObject) object).get("url")); + if (rawUrl != null) { + url = HttpRequest.url(rawUrl.trim()); + } else { + log.warning("Did not get valid update document - got:\n\n" + data); + throw new IOException("Update URL did not return a valid result"); + } + } else { + log.warning("Did not get valid update document - got:\n\n" + data); + throw new IOException("Update URL did not return a valid result"); + } + + checkInterrupted(); + + File finalFile = new File(bootstrap.getBinariesDir(), System.currentTimeMillis() + ".jar"); + File tempFile = new File(finalFile.getParentFile(), finalFile.getName() + ".tmp"); + + log.info("Downloading " + url + " to " + tempFile.getAbsolutePath()); + + httpRequest = HttpRequest.get(url); + httpRequest + .execute() + .expectResponseCode(200) + .saveContent(tempFile); + + finalFile.delete(); + tempFile.renameTo(finalFile); + + LauncherBinary binary = new LauncherBinary(finalFile); + binaries.add(binary); + } finally { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + dialog.setDownloader(null); + dialog.dispose(); + } + }); + } + + bootstrap.launchExisting(binaries, false); + } + + public void cancel() { + thread.interrupt(); + } + + public String getStatus() { + HttpRequest httpRequest = this.httpRequest; + if (httpRequest != null) { + double progress = httpRequest.getProgress(); + if (progress >= 0) { + return String.format(tr("downloader.progressStatus"), progress * 100); + } + } + + return tr("downloader.status"); + } + + @Override + public double getProgress() { + HttpRequest httpRequest = this.httpRequest; + return httpRequest != null ? httpRequest.getProgress() : -1; + } +} diff --git a/launcher-bootstrap/src/main/java/com/skcraft/launcher/bootstrap/HttpRequest.java b/launcher-bootstrap/src/main/java/com/skcraft/launcher/bootstrap/HttpRequest.java new file mode 100644 index 0000000..94d3737 --- /dev/null +++ b/launcher-bootstrap/src/main/java/com/skcraft/launcher/bootstrap/HttpRequest.java @@ -0,0 +1,504 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.bootstrap; + +import lombok.Getter; +import lombok.extern.java.Log; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Unmarshaller; +import java.io.*; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.skcraft.launcher.bootstrap.BootstrapUtils.checkInterrupted; +import static com.skcraft.launcher.bootstrap.BootstrapUtils.closeQuietly; + +/** + * A simple fluent interface for performing HTTP requests that uses + * {@link java.net.HttpURLConnection} or {@link javax.net.ssl.HttpsURLConnection}. + */ +@Log +public class HttpRequest implements Closeable, ProgressObservable { + + private static final int READ_TIMEOUT = 1000 * 60 * 10; + private static final int READ_BUFFER_SIZE = 1024 * 8; + + private final Map headers = new HashMap(); + private String method; + @Getter + private final URL url; + private String contentType; + private byte[] body; + private HttpURLConnection conn; + private InputStream inputStream; + private int redirectCount; + + private long contentLength = -1; + private long readBytes = 0; + + /** + * Create a new HTTP request. + * + * @param method the method + * @param url the URL + */ + private HttpRequest(String method, URL url) { + this.method = method; + this.url = url; + } + + /** + * Submit form data. + * + * @param form the form + * @return this object + */ + public HttpRequest bodyForm(Form form) { + contentType = "application/x-www-form-urlencoded"; + body = form.toString().getBytes(); + return this; + } + + /** + * Add a header. + * + * @param key the header key + * @param value the header value + * @return this object + */ + public HttpRequest header(String key, String value) { + headers.put(key, value); + return this; + } + + /** + * Execute the request. + *

+ * After execution, {@link #close()} should be called. + * + * @return this object + * @throws java.io.IOException on I/O error + */ + public HttpRequest execute() throws IOException { + boolean successful = false; + + try { + if (conn != null) { + throw new IllegalArgumentException("Connection already executed"); + } + + conn = this.runRequest(url); + + inputStream = conn.getResponseCode() == HttpURLConnection.HTTP_OK ? + conn.getInputStream() : conn.getErrorStream(); + + successful = true; + } finally { + if (!successful) { + close(); + } + } + + return this; + } + + private HttpURLConnection runRequest(URL url) throws IOException { + if (redirectCount > 20) { + throw new IOException("Too many redirects!"); + } + + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Java) SKMCLauncher"); + conn.setInstanceFollowRedirects(false); + + if (body != null) { + conn.setRequestProperty("Content-Type", contentType); + conn.setRequestProperty("Content-Length", Integer.toString(body.length)); + conn.setDoInput(true); + } + + for (Map.Entry entry : headers.entrySet()) { + conn.setRequestProperty(entry.getKey(), entry.getValue()); + } + + conn.setRequestMethod(method); + conn.setUseCaches(false); + conn.setDoOutput(true); + conn.setReadTimeout(READ_TIMEOUT); + + conn.connect(); + + if (body != null) { + DataOutputStream out = new DataOutputStream(conn.getOutputStream()); + out.write(body); + out.flush(); + out.close(); + } + + switch (conn.getResponseCode()) { + case HttpURLConnection.HTTP_SEE_OTHER: + method = "GET"; + body = null; + case HttpURLConnection.HTTP_MOVED_PERM: + case HttpURLConnection.HTTP_MOVED_TEMP: + case HttpURLConnection.HTTP_ACCEPTED: + case 307: + case 308: + String location = conn.getHeaderField("Location"); + redirectCount++; + + return runRequest(new URL(this.url, location)); + default: + break; + } + + return conn; + } + + /** + * Require that the response code is one of the given response codes. + * + * @param codes a list of codes + * @return this object + * @throws java.io.IOException if there is an I/O error or the response code is not expected + */ + public HttpRequest expectResponseCode(int... codes) throws IOException { + int responseCode = getResponseCode(); + + for (int code : codes) { + if (code == responseCode) { + return this; + } + } + + close(); + throw new IOException("Did not get expected response code, got " + responseCode + " for " + url); + } + + /** + * Get the response code. + * + * @return the response code + * @throws java.io.IOException on I/O error + */ + public int getResponseCode() throws IOException { + if (conn == null) { + throw new IllegalArgumentException("No connection has been made"); + } + + return conn.getResponseCode(); + } + + /** + * Get the input stream. + * + * @return the input stream + */ + public InputStream getInputStream() { + return inputStream; + } + + /** + * Buffer the returned response. + * + * @return the buffered response + * @throws java.io.IOException on I/O error + * @throws InterruptedException on interruption + */ + public BufferedResponse returnContent() throws IOException, InterruptedException { + if (inputStream == null) { + throw new IllegalArgumentException("No input stream available"); + } + + try { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + int b = 0; + while ((b = inputStream.read()) != -1) { + checkInterrupted(); + bos.write(b); + } + return new BufferedResponse(bos.toByteArray()); + } finally { + close(); + } + } + + /** + * Save the result to a file. + * + * @param file the file + * @return this object + * @throws java.io.IOException on I/O error + * @throws InterruptedException on interruption + */ + public HttpRequest saveContent(File file) throws IOException, InterruptedException { + FileOutputStream fos = null; + BufferedOutputStream bos = null; + + try { + fos = new FileOutputStream(file); + bos = new BufferedOutputStream(fos); + + saveContent(bos); + } finally { + closeQuietly(bos); + closeQuietly(fos); + } + + return this; + } + + /** + * Save the result to an output stream. + * + * @param out the output stream + * @return this object + * @throws java.io.IOException on I/O error + * @throws InterruptedException on interruption + */ + public HttpRequest saveContent(OutputStream out) throws IOException, InterruptedException { + BufferedInputStream bis; + + try { + String field = conn.getHeaderField("Content-Length"); + if (field != null) { + long len = Long.parseLong(field); + if (len >= 0) { // Let's just not deal with really big numbers + contentLength = len; + } + } + } catch (NumberFormatException e) { + } + + try { + bis = new BufferedInputStream(inputStream); + + byte[] data = new byte[READ_BUFFER_SIZE]; + int len = 0; + while ((len = bis.read(data, 0, READ_BUFFER_SIZE)) >= 0) { + out.write(data, 0, len); + readBytes += len; + checkInterrupted(); + } + } finally { + close(); + } + + return this; + } + + @Override + public double getProgress() { + if (contentLength >= 0) { + return readBytes / (double) contentLength; + } else { + return -1; + } + } + + @Override + public void close() throws IOException { + if (conn != null) conn.disconnect(); + } + + /** + * Perform a GET request. + * + * @param url the URL + * @return a new request object + */ + public static HttpRequest get(URL url) { + return request("GET", url); + } + + /** + * Perform a POST request. + * + * @param url the URL + * @return a new request object + */ + public static HttpRequest post(URL url) { + return request("POST", url); + } + + /** + * Perform a request. + * + * @param method the method + * @param url the URL + * @return a new request object + */ + public static HttpRequest request(String method, URL url) { + return new HttpRequest(method, url); + } + + /** + * Create a new {@link java.net.URL} and throw a {@link RuntimeException} if the URL + * is not valid. + * + * @param url the url + * @return a URL object + * @throws RuntimeException if the URL is invalid + */ + public static URL url(String url) { + try { + return new URL(url); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } + + /** + * Used with {@link #bodyForm(Form)}. + */ + public final static class Form { + public final List elements = new ArrayList(); + + private Form() { + } + + /** + * Add a key/value to the form. + * + * @param key the key + * @param value the value + * @return this object + */ + public Form add(String key, String value) { + try { + elements.add(URLEncoder.encode(key, "UTF-8") + + "=" + URLEncoder.encode(value, "UTF-8")); + return this; + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + boolean first = true; + for (String element : elements) { + if (first) { + first = false; + } else { + builder.append("&"); + } + builder.append(element); + } + return builder.toString(); + } + + /** + * Create a new form. + * + * @return a new form + */ + public static Form form() { + return new Form(); + } + } + + /** + * Used to buffer the response in memory. + */ + public class BufferedResponse { + private final byte[] data; + + private BufferedResponse(byte[] data) { + this.data = data; + } + + /** + * Return the result as bytes. + * + * @return the data + */ + public byte[] asBytes() { + return data; + } + + /** + * Return the result as a string. + * + * @param encoding the encoding + * @return the string + * @throws java.io.IOException on I/O error + */ + public String asString(String encoding) throws IOException { + return new String(data, encoding); + } + + /** + * Return the result as an instance of the given class that has been + * deserialized from a XML payload. + * + * @return the object + * @throws java.io.IOException on I/O error + */ + @SuppressWarnings("unchecked") + public T asXml(Class cls) throws IOException { + try { + JAXBContext context = JAXBContext.newInstance(cls); + Unmarshaller um = context.createUnmarshaller(); + return (T) um.unmarshal(new ByteArrayInputStream(data)); + } catch (JAXBException e) { + throw new IOException(e); + } + } + + /** + * Save the result to a file. + * + * @param file the file + * @return this object + * @throws java.io.IOException on I/O error + * @throws InterruptedException on interruption + */ + public BufferedResponse saveContent(File file) throws IOException, InterruptedException { + FileOutputStream fos = null; + BufferedOutputStream bos = null; + + file.getParentFile().mkdirs(); + + try { + fos = new FileOutputStream(file); + bos = new BufferedOutputStream(fos); + + saveContent(bos); + } finally { + closeQuietly(bos); + closeQuietly(fos); + } + + return this; + } + + /** + * Save the result to an output stream. + * + * @param out the output stream + * @return this object + * @throws java.io.IOException on I/O error + * @throws InterruptedException on interruption + */ + public BufferedResponse saveContent(OutputStream out) throws IOException, InterruptedException { + out.write(data); + + return this; + } + } + +} diff --git a/launcher-bootstrap/src/main/java/com/skcraft/launcher/bootstrap/LauncherBinary.java b/launcher-bootstrap/src/main/java/com/skcraft/launcher/bootstrap/LauncherBinary.java new file mode 100644 index 0000000..0935ca5 --- /dev/null +++ b/launcher-bootstrap/src/main/java/com/skcraft/launcher/bootstrap/LauncherBinary.java @@ -0,0 +1,81 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.bootstrap; + +import lombok.Getter; +import lombok.extern.java.Log; + +import java.io.File; +import java.io.FileFilter; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@Log +public class LauncherBinary implements Comparable { + + public static final Pattern PATTERN = Pattern.compile("^([0-9]+)\\.jar(\\.pack)?$"); + @Getter + private final File path; + private final long time; + private final boolean packed; + + public LauncherBinary(File path) { + this.path = path; + String name = path.getName(); + Matcher m = PATTERN.matcher(name); + if (!m.matches()) { + throw new IllegalArgumentException("Invalid filename: " + path); + } + time = Long.parseLong(m.group(1)); + packed = m.group(2) != null; + } + + public File getExecutableJar() throws PackedJarException { + if (packed) { + log.warning("Launcher binary " + path.getAbsolutePath() + " is a pack200 file, which is " + + "no longer supported."); + + throw new PackedJarException("Cannot unpack .jar.pack files!"); + } else { + return path; + } + } + + @Override + public int compareTo(LauncherBinary o) { + if (time > o.time) { + return -1; + } else if (time < o.time) { + return 1; + } else { + if (packed && !o.packed) { + return 1; + } else if (!packed && o.packed) { + return -1; + } else { + return 0; + } + } + } + + public void remove() { + path.delete(); + } + + public static class Filter implements FileFilter { + @Override + public boolean accept(File file) { + return file.isFile() && LauncherBinary.PATTERN.matcher(file.getName()).matches(); + } + } + + public static class PackedJarException extends Exception { + public PackedJarException(String message) { + super(message); + } + } +} diff --git a/launcher-bootstrap/src/main/java/com/skcraft/launcher/bootstrap/LinedBoxPanel.java b/launcher-bootstrap/src/main/java/com/skcraft/launcher/bootstrap/LinedBoxPanel.java new file mode 100644 index 0000000..76d9e3f --- /dev/null +++ b/launcher-bootstrap/src/main/java/com/skcraft/launcher/bootstrap/LinedBoxPanel.java @@ -0,0 +1,52 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.bootstrap; + +import lombok.Getter; +import lombok.Setter; + +import javax.swing.*; +import java.awt.*; + +public class LinedBoxPanel extends JPanel { + + @Getter + private final boolean horizontal; + @Getter @Setter + private int spacing = 6; + private boolean needsSpacer = false; + + public LinedBoxPanel(boolean horizontal) { + this.horizontal = horizontal; + setLayout(new BoxLayout(this, + horizontal ? BoxLayout.X_AXIS : BoxLayout.Y_AXIS)); + setBorder(BorderFactory.createEmptyBorder(0, 10, 10, 10)); + } + + public LinedBoxPanel fullyPadded() { + setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + return this; + } + + public void addElement(Component component) { + if (needsSpacer) { + add(horizontal ? + Box.createHorizontalStrut(spacing) : + Box.createVerticalStrut(spacing)); + } + add(component); + needsSpacer = true; + } + + public void addGlue() { + add(horizontal ? + Box.createHorizontalGlue() : + Box.createVerticalGlue()); + needsSpacer = false; + } + +} diff --git a/launcher-bootstrap/src/main/java/com/skcraft/launcher/bootstrap/ProgressObservable.java b/launcher-bootstrap/src/main/java/com/skcraft/launcher/bootstrap/ProgressObservable.java new file mode 100644 index 0000000..bf13a5c --- /dev/null +++ b/launcher-bootstrap/src/main/java/com/skcraft/launcher/bootstrap/ProgressObservable.java @@ -0,0 +1,23 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.bootstrap; + +/** + * Implementations of this interface can provide information on the progress + * of a task. + */ +public interface ProgressObservable { + + /** + * Get the progress value as a number between 0 and 1 (inclusive), or -1 + * if progress information is unavailable. + * + * @return the progress value + */ + double getProgress(); + +} diff --git a/launcher-bootstrap/src/main/java/com/skcraft/launcher/bootstrap/SharedLocale.java b/launcher-bootstrap/src/main/java/com/skcraft/launcher/bootstrap/SharedLocale.java new file mode 100644 index 0000000..e699500 --- /dev/null +++ b/launcher-bootstrap/src/main/java/com/skcraft/launcher/bootstrap/SharedLocale.java @@ -0,0 +1,106 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.bootstrap; + +import lombok.NonNull; +import lombok.extern.java.Log; + +import java.text.MessageFormat; +import java.util.Locale; +import java.util.MissingResourceException; +import java.util.ResourceBundle; +import java.util.logging.Level; + +/** + * Handles loading a shared message {@link ResourceBundle}. + */ +@Log +public class SharedLocale { + + private static Locale locale = Locale.getDefault(); + private static ResourceBundle bundle; + + /** + * Get the current locale. + * + * @return the current locale + */ + public static Locale getLocale() { + return locale; + } + + /** + * Get the current resource bundle. + * + * @return the current resource bundle, or null if not available + */ + public static ResourceBundle getBundle() { + return bundle; + } + + /** + * Translate a string. + * + *

If the string is not available, then ${key} will be returned.

+ * + * @param key the key + * @return the translated string + */ + public static String tr(String key) { + if (bundle != null) { + try { + return bundle.getString(key); + } catch (MissingResourceException e) { + log.log(Level.WARNING, "Failed to find message", e); + } + } + + return "${" + key + "}"; + } + + /** + * Format a translated string. + * + *

If the string is not available, then ${key}:args will be returned.

+ * + * @param key the key + * @param args arguments + * @return a translated string + */ + public static String tr(String key, Object... args) { + if (bundle != null) { + try { + MessageFormat formatter = new MessageFormat(tr(key)); + formatter.setLocale(getLocale()); + return formatter.format(args); + } catch (MissingResourceException e) { + log.log(Level.WARNING, "Failed to find message", e); + } + } + + return "${" + key + "}:" + args; + } + + /** + * Load a shared resource bundle. + * + * @param baseName the bundle name + * @param locale the locale + * @return true if loaded successfully + */ + public static boolean loadBundle(@NonNull String baseName, @NonNull Locale locale) { + try { + SharedLocale.locale = locale; + bundle = ResourceBundle.getBundle(baseName, locale, + SharedLocale.class.getClassLoader()); + return true; + } catch (MissingResourceException e) { + log.log(Level.SEVERE, "Failed to load resource bundle", e); + return false; + } + } +} diff --git a/launcher-bootstrap/src/main/java/com/skcraft/launcher/bootstrap/SimpleLogFormatter.java b/launcher-bootstrap/src/main/java/com/skcraft/launcher/bootstrap/SimpleLogFormatter.java new file mode 100644 index 0000000..fbfa990 --- /dev/null +++ b/launcher-bootstrap/src/main/java/com/skcraft/launcher/bootstrap/SimpleLogFormatter.java @@ -0,0 +1,63 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.bootstrap; + +import lombok.extern.java.Log; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.logging.*; + +@Log +public final class SimpleLogFormatter extends Formatter { + + private static final String LINE_SEPARATOR = System.getProperty("line.separator"); + + @Override + public String format(LogRecord record) { + StringBuilder sb = new StringBuilder(); + + sb.append("[") + .append(record.getLevel().getLocalizedName().toLowerCase()) + .append("] ") + .append(formatMessage(record)) + .append(LINE_SEPARATOR); + + if (record.getThrown() != null) { + try { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + record.getThrown().printStackTrace(pw); + pw.close(); + sb.append(sw.toString()); + } catch (Exception e) { + } + } + + return sb.toString(); + } + + public static void configureGlobalLogger() { + Logger globalLogger = Logger.getLogger(""); + + // Set formatter + for (Handler handler : globalLogger.getHandlers()) { + handler.setFormatter(new SimpleLogFormatter()); + } + + // Set level + String logLevel = System.getProperty( + SimpleLogFormatter.class.getCanonicalName() + ".logLevel", "INFO"); + try { + Level level = Level.parse(logLevel); + globalLogger.setLevel(level); + } catch (IllegalArgumentException e) { + log.log(Level.WARNING, "Invalid log level of " + logLevel, e); + } + } + +} \ No newline at end of file diff --git a/launcher-bootstrap/src/main/java/com/skcraft/launcher/bootstrap/SwingHelper.java b/launcher-bootstrap/src/main/java/com/skcraft/launcher/bootstrap/SwingHelper.java new file mode 100644 index 0000000..c2e2e73 --- /dev/null +++ b/launcher-bootstrap/src/main/java/com/skcraft/launcher/bootstrap/SwingHelper.java @@ -0,0 +1,227 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.bootstrap; + +import lombok.NonNull; +import lombok.extern.java.Log; + +import javax.imageio.ImageIO; +import javax.swing.*; +import javax.swing.text.JTextComponent; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.reflect.InvocationTargetException; +import java.util.concurrent.atomic.AtomicBoolean; + +import static com.skcraft.launcher.bootstrap.BootstrapUtils.closeQuietly; +import static com.skcraft.launcher.bootstrap.SharedLocale.tr; + +/** + * Swing utility methods. + */ +@Log +public final class SwingHelper { + + private SwingHelper() { + } + + public static String htmlEscape(String str) { + return str.replace(">", ">") + .replace("<", "<") + .replace("&", "&"); + } + + /** + * Shows an popup error dialog, with potential extra details shown either immediately + * or available on the dialog. + * + * @param parentComponent the frame from which the dialog is displayed, otherwise + * null to use the default frame + * @param message the message to display + * @param title the title string for the dialog + * @see #showMessageDialog(java.awt.Component, String, String, String, int) for details + */ + public static void showErrorDialog(Component parentComponent, @NonNull String message, + @NonNull String title) { + showErrorDialog(parentComponent, message, title, null); + } + + /** + * Shows an popup error dialog, with potential extra details shown either immediately + * or available on the dialog. + * + * @param parentComponent the frame from which the dialog is displayed, otherwise + * null to use the default frame + * @param message the message to display + * @param title the title string for the dialog + * @param throwable the exception, or null if there is no exception to show + * @see #showMessageDialog(java.awt.Component, String, String, String, int) for details + */ + public static void showErrorDialog(Component parentComponent, @NonNull String message, + @NonNull String title, Throwable throwable) { + String detailsText = null; + + // Get a string version of the exception and use that for + // the extra details text + if (throwable != null) { + StringWriter sw = new StringWriter(); + throwable.printStackTrace(new PrintWriter(sw)); + detailsText = sw.toString(); + } + + showMessageDialog(parentComponent, + message, title, + detailsText, JOptionPane.ERROR_MESSAGE); + } + + /** + * Show a message dialog using + * {@link javax.swing.JOptionPane#showMessageDialog(java.awt.Component, Object, String, int)}. + * + *

The dialog will be shown from the Event Dispatch Thread, regardless of the + * thread it is called from. In either case, the method will block until the + * user has closed the dialog (or dialog creation fails for whatever reason).

+ * + * @param parentComponent the frame from which the dialog is displayed, otherwise + * null to use the default frame + * @param message the message to display + * @param title the title string for the dialog + * @param messageType see {@link javax.swing.JOptionPane#showMessageDialog(java.awt.Component, Object, String, int)} + * for available message types + */ + public static void showMessageDialog(final Component parentComponent, + @NonNull final String message, + @NonNull final String title, + final String detailsText, + final int messageType) { + + if (SwingUtilities.isEventDispatchThread()) { + // To force the label to wrap, convert the message to broken HTML + String htmlMessage = "
" + htmlEscape(message); + + JPanel panel = new JPanel(new BorderLayout(0, detailsText != null ? 20 : 0)); + + // Add the main message + panel.add(new JLabel(htmlMessage), BorderLayout.NORTH); + + // Add the extra details + if (detailsText != null) { + JTextArea textArea = new JTextArea(tr("errorDialog.reportError") + "\n\n" + detailsText); + JLabel tempLabel = new JLabel(); + textArea.setFont(tempLabel.getFont()); + textArea.setBackground(tempLabel.getBackground()); + textArea.setTabSize(2); + textArea.setEditable(false);; + + JScrollPane scrollPane = new JScrollPane(textArea); + scrollPane.setPreferredSize(new Dimension(350, 120)); + panel.add(scrollPane, BorderLayout.CENTER); + } + + JOptionPane.showMessageDialog( + parentComponent, panel, title, messageType); + } else { + // Call method again from the Event Dispatch Thread + try { + SwingUtilities.invokeAndWait(new Runnable() { + @Override + public void run() { + showMessageDialog( + parentComponent, message, title, + detailsText, messageType); + } + }); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (InvocationTargetException e) { + throw new RuntimeException(e); + } + } + } + + /** + * Asks the user a binary yes or no question. + * + * @param parentComponent the component + * @param message the message to display + * @param title the title string for the dialog + * @return whether 'yes' was selected + */ + public static boolean confirmDialog(final Component parentComponent, + @NonNull final String message, + @NonNull final String title) { + if (SwingUtilities.isEventDispatchThread()) { + return JOptionPane.showConfirmDialog( + parentComponent, message, title, JOptionPane.YES_NO_OPTION) == + JOptionPane.YES_OPTION; + } else { + // Use an AtomicBoolean to pass the result back from the + // Event Dispatcher Thread + final AtomicBoolean yesSelected = new AtomicBoolean(); + + try { + SwingUtilities.invokeAndWait(new Runnable() { + @Override + public void run() { + yesSelected.set(confirmDialog(parentComponent, title, message)); + } + }); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (InvocationTargetException e) { + throw new RuntimeException(e); + } + + return yesSelected.get(); + } + } + + public static BufferedImage readIconImage(Class clazz, String path) { + InputStream in = null; + try { + in = clazz.getResourceAsStream(path); + if (in != null) { + return ImageIO.read(in); + } + } catch (IOException e) { + } finally { + closeQuietly(in); + } + return null; + } + + public static void setIconImage(Window frame, Class clazz, String path) { + BufferedImage image = readIconImage(clazz, path); + if (image != null) { + frame.setIconImage(image); + } + } + + /** + * Focus a component. + * + *

The focus call happens in {@link javax.swing.SwingUtilities#invokeLater(Runnable)}.

+ * + * @param component the component + */ + public static void focusLater(@NonNull final Component component) { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + if (component instanceof JTextComponent) { + ((JTextComponent) component).selectAll(); + } + component.requestFocusInWindow(); + } + }); + } + +} diff --git a/launcher-bootstrap/src/main/resources/com/skcraft/launcher/bootstrap.properties b/launcher-bootstrap/src/main/resources/com/skcraft/launcher/bootstrap.properties new file mode 100644 index 0000000..678f616 --- /dev/null +++ b/launcher-bootstrap/src/main/resources/com/skcraft/launcher/bootstrap.properties @@ -0,0 +1,11 @@ +# +# SK's Minecraft Launcher +# Copyright (C) 2010-2014 Albert Pham and contributors +# Please see LICENSE.txt for license information. +# + +homeFolderWindows=Example Launcher +homeFolderLinux=example_launcher +homeFolder=.examplelauncher +launcherClass=com.skcraft.launcher.Launcher +latestUrl=http://update.skcraft.com/quark/launcher/latest.json diff --git a/launcher-bootstrap/src/main/resources/com/skcraft/launcher/bootstrapper_icon.png b/launcher-bootstrap/src/main/resources/com/skcraft/launcher/bootstrapper_icon.png new file mode 100644 index 0000000..8c74095 Binary files /dev/null and b/launcher-bootstrap/src/main/resources/com/skcraft/launcher/bootstrapper_icon.png differ diff --git a/launcher-bootstrap/src/main/resources/com/skcraft/launcher/lang/Bootstrap.properties b/launcher-bootstrap/src/main/resources/com/skcraft/launcher/lang/Bootstrap.properties new file mode 100644 index 0000000..9b9f6a2 --- /dev/null +++ b/launcher-bootstrap/src/main/resources/com/skcraft/launcher/lang/Bootstrap.properties @@ -0,0 +1,21 @@ +# +# SK's Minecraft Launcher +# Copyright (C) 2010-2014 Albert Pham and contributors +# Please see LICENSE.txt for license information. +# + +button.cancal=Cancel + +errorTitle=Error +errors.failedDownloadError=The SKCraft launcher could not be downloaded. +errors.bootstrapError=An error occurred while trying to bootstrap the launcher\! + +errorDialog.reportError=To report this error, provide\: + +downloader.title=Downloading SKCraft Launcher... +downloader.pleaseWait=Please wait... +downloader.status=Downloading latest SKCraft Launcher... +downloader.progressStatus=Downloading latest SKCraft Launcher (%.2f%%)... + +progress.confirmCancel=Are you sure that you wish to cancel? +progress.confirmCancelTitle=Cancel \ No newline at end of file diff --git a/launcher-builder/build.gradle b/launcher-builder/build.gradle new file mode 100644 index 0000000..2248957 --- /dev/null +++ b/launcher-builder/build.gradle @@ -0,0 +1,28 @@ +plugins { + id 'application' + id 'java-library' + id "com.github.johnrengelman.shadow" + id 'io.freefair.lombok' +} + +application { + mainClassName = "com.skcraft.launcher.builder.PackageBuilder" +} + +dependencies { + api project(':launcher') + implementation 'org.apache.commons:commons-compress:1.21' +} + +shadowJar { + dependsOn ':launcher:shadowJar' + archiveClassifier.set("") +} + +// Work around gradle shadow bug +// see https://github.com/johnrengelman/shadow/issues/713 +startScripts.dependsOn(':launcher:shadowJar') +distTar.dependsOn(':launcher:shadowJar') +distZip.dependsOn(':launcher:shadowJar') + +build.dependsOn(shadowJar) diff --git a/launcher-builder/lombok.config b/launcher-builder/lombok.config new file mode 100644 index 0000000..6aa51d7 --- /dev/null +++ b/launcher-builder/lombok.config @@ -0,0 +1,2 @@ +# This file is generated by the 'io.freefair.lombok' Gradle plugin +config.stopBubbling = true diff --git a/launcher-builder/src/main/java/com/skcraft/launcher/builder/BuilderConfig.java b/launcher-builder/src/main/java/com/skcraft/launcher/builder/BuilderConfig.java new file mode 100644 index 0000000..5101417 --- /dev/null +++ b/launcher-builder/src/main/java/com/skcraft/launcher/builder/BuilderConfig.java @@ -0,0 +1,62 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.builder; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.Lists; +import com.skcraft.launcher.model.modpack.LaunchModifier; +import com.skcraft.launcher.model.modpack.Manifest; +import lombok.Data; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Strings.emptyToNull; + +@Data +public class BuilderConfig { + + private String name; + private String title; + private String gameVersion; + @JsonProperty("launch") + private LaunchModifier launchModifier = new LaunchModifier(); + private List features = Lists.newArrayList(); + private FnPatternList userFiles = new FnPatternList(); + + public void setLaunchModifier(LaunchModifier launchModifier) { + this.launchModifier = launchModifier != null ? launchModifier : new LaunchModifier(); + } + + public void setFeatures(List features) { + this.features = features != null ? features : Lists.newArrayList(); + } + + public void setUserFiles(FnPatternList userFiles) { + this.userFiles = userFiles != null ? userFiles : new FnPatternList(); + } + + public void update(Manifest manifest) { + manifest.updateName(getName()); + manifest.updateTitle(getTitle()); + manifest.updateGameVersion(getGameVersion()); + manifest.setLaunchModifier(getLaunchModifier()); + } + + public void registerProperties(PropertiesApplicator applicator) { + if (features != null) { + for (FeaturePattern feature : features) { + checkNotNull(emptyToNull(feature.getFeature().getName()), + "Empty feature name found"); + applicator.register(feature); + } + } + + applicator.setUserFiles(userFiles); + } + +} diff --git a/launcher-builder/src/main/java/com/skcraft/launcher/builder/BuilderOptions.java b/launcher-builder/src/main/java/com/skcraft/launcher/builder/BuilderOptions.java new file mode 100644 index 0000000..8a62b86 --- /dev/null +++ b/launcher-builder/src/main/java/com/skcraft/launcher/builder/BuilderOptions.java @@ -0,0 +1,114 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.builder; + +import com.beust.jcommander.Parameter; +import com.beust.jcommander.ParameterException; +import lombok.Data; + +import java.io.File; + +@Data +public class BuilderOptions { + + public static final String DEFAULT_CONFIG_FILENAME = "modpack.json"; + public static final String DEFAULT_VERSION_FILENAME = "version.json"; + public static final String DEFAULT_SRC_DIRNAME = "src"; + public static final String DEFAULT_LOADERS_DIRNAME = "loaders"; + + // Configuration + + // Override config + @Parameter(names = "--name") + private String name; + @Parameter(names = "--title") + private String title; + @Parameter(names = "--mc-version") + private String gameVersion; + + // Required + @Parameter(names = "--version", required = true) + private String version; + @Parameter(names = "--manifest-dest", required = true) + private File manifestPath; + + // Overall paths + @Parameter(names = {"--input", "-i"}) + private File inputPath; + @Parameter(names = {"--output", "-o"}) + private File outputPath; + + // Input paths + @Parameter(names = "--config") + private File configPath; + @Parameter(names = "--version-file") + private File versionManifestPath; + @Parameter(names = "--files") + private File filesDir; + @Parameter(names = "--loaders") + private File loadersDir; + + // Output paths + @Parameter(names = "--objects-dest") + private File objectsDir; + @Parameter(names = "--libraries-dest") + private File librariesDir; + + @Parameter(names = "--libs-url") + private String librariesLocation = "libraries"; + @Parameter(names = "--objects-url") + private String objectsLocation = "objects"; + + // Misc + @Parameter(names = "--pretty-print") + private boolean prettyPrinting; + + public void choosePaths() throws ParameterException { + if (configPath == null) { + requireInputPath("--config"); + configPath = new File(inputPath, DEFAULT_CONFIG_FILENAME); + } + + if (versionManifestPath == null) { + requireInputPath("--version"); + versionManifestPath = new File(inputPath, DEFAULT_VERSION_FILENAME); + } + + if (filesDir == null) { + requireInputPath("--files"); + filesDir = new File(inputPath, DEFAULT_SRC_DIRNAME); + } + + if (loadersDir == null) { + requireInputPath("--loaders"); + loadersDir = new File(inputPath, DEFAULT_LOADERS_DIRNAME); + } + + if (objectsDir == null) { + requireOutputPath("--objects-dest"); + objectsDir = new File(outputPath, objectsLocation); + } + + if (librariesDir == null) { + requireOutputPath("--libs-dest"); + librariesDir = new File(outputPath, librariesLocation); + } + } + + private void requireOutputPath(String name) throws ParameterException { + if (outputPath == null) { + throw new ParameterException("Because " + name + " was not specified, --output needs to be specified as the output directory and then " + name + " will be default to a pre-set path within the output directory"); + } + } + + private void requireInputPath(String name) throws ParameterException { + if (inputPath == null) { + throw new ParameterException("Because " + name + " was not specified, --input needs to be specified as the project directory and then " + name + " will be default to a pre-set path within the project directory"); + } + } + +} diff --git a/launcher-builder/src/main/java/com/skcraft/launcher/builder/BuilderUtils.java b/launcher-builder/src/main/java/com/skcraft/launcher/builder/BuilderUtils.java new file mode 100644 index 0000000..c291ee0 --- /dev/null +++ b/launcher-builder/src/main/java/com/skcraft/launcher/builder/BuilderUtils.java @@ -0,0 +1,68 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.builder; + +import com.google.common.collect.Lists; +import com.google.common.io.CharStreams; +import org.apache.commons.compress.compressors.CompressorStreamFactory; + +import java.io.IOException; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +public final class BuilderUtils { + + private static final DateFormat VERSION_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss"); + + private BuilderUtils() { + } + + public static String normalizePath(String path) { + return path.replaceAll("^[/\\\\]*", "").replaceAll("[/\\\\]+", "/"); + } + + public static ZipEntry getZipEntry(ZipFile jarFile, String path) { + Enumeration entries = jarFile.entries(); + String expected = normalizePath(path); + + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + String test = normalizePath(entry.getName()); + if (expected.equals(test)) { + return entry; + } + } + + return null; + } + + public static String readStringFromStream(Readable r) throws IOException { + String data = CharStreams.toString(r); + data = data.replaceAll(",\\s*\\}", "}"); // Fix issues with trailing commas + + return data; + } + + public static List getCompressors(String repoUrl) { + if (repoUrl.matches("^https?://files.minecraftforge.net/maven/?")) { + return Lists.newArrayList( + new Compressor("xz", CompressorStreamFactory.XZ), + new Compressor("pack", CompressorStreamFactory.PACK200)); + } else { + return Collections.emptyList(); + } + } + + public static String generateVersionFromDate() { + Date today = Calendar.getInstance().getTime(); + return VERSION_DATE_FORMAT.format(today); + } + +} diff --git a/launcher-builder/src/main/java/com/skcraft/launcher/builder/ClientFileCollector.java b/launcher-builder/src/main/java/com/skcraft/launcher/builder/ClientFileCollector.java new file mode 100644 index 0000000..79c9dd8 --- /dev/null +++ b/launcher-builder/src/main/java/com/skcraft/launcher/builder/ClientFileCollector.java @@ -0,0 +1,105 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.builder; + +import com.google.common.hash.HashFunction; +import com.google.common.hash.Hashing; +import com.google.common.io.Files; +import com.skcraft.launcher.model.modpack.FileInstall; +import com.skcraft.launcher.model.modpack.Manifest; +import lombok.NonNull; +import lombok.extern.java.Log; +import org.apache.commons.io.FilenameUtils; + +import java.io.File; +import java.io.IOException; + +/** + * Walks a path and adds hashed path versions to the given + * {@link com.skcraft.launcher.model.modpack.Manifest}. + */ +@Log +public class ClientFileCollector extends DirectoryWalker { + + private final Manifest manifest; + private final PropertiesApplicator applicator; + private final File destDir; + private HashFunction hf = Hashing.sha1(); + + /** + * Create a new collector. + * + * @param manifest the manifest + * @param applicator applies properties to manifest entries + * @param destDir the destination directory to copy the hashed objects + */ + public ClientFileCollector(@NonNull Manifest manifest, @NonNull PropertiesApplicator applicator, + @NonNull File destDir) { + this.manifest = manifest; + this.applicator = applicator; + this.destDir = destDir; + } + + @Override + protected DirectoryBehavior getBehavior(@NonNull String name) { + return getDirectoryBehavior(name); + } + + @Override + protected void onFile(File file, String relPath) throws IOException { + if (file.getName().endsWith(FileInfoScanner.FILE_SUFFIX) + || file.getName().endsWith(FileUrlScanner.URL_FILE_SUFFIX)) { + return; + } + + FileInstall entry = new FileInstall(); + String hash = Files.hash(file, hf).toString(); + String to = FilenameUtils.separatorsToUnix(FilenameUtils.normalize(relPath)); + + // url.txt override file + File urlFile = new File(file.getAbsoluteFile().getParentFile(), + file.getName() + FileUrlScanner.URL_FILE_SUFFIX); + String location; + boolean copy = true; + if (urlFile.exists() && FileUrlScanner.isEnabled()) { + FileUrlRedirect redirect = FileUrlRedirect.fromFile(urlFile); + + location = redirect.getUrl().toString(); + copy = false; + } else { + location = hash.substring(0, 2) + "/" + hash.substring(2, 4) + "/" + hash; + } + + File destPath = new File(destDir, location); + entry.setHash(hash); + entry.setLocation(location); + entry.setTo(to); + entry.setSize(file.length()); + applicator.apply(entry); + destPath.getParentFile().mkdirs(); + ClientFileCollector.log.info(String.format("Adding %s from %s...", relPath, file.getAbsolutePath())); + if (copy) { + Files.copy(file, destPath); + } + manifest.getTasks().add(entry); + } + + public static DirectoryBehavior getDirectoryBehavior(@NonNull String name) { + if (name.startsWith(".")) { + return DirectoryBehavior.SKIP; + } else if (name.equals("_OPTIONAL")) { + return DirectoryBehavior.IGNORE; + } else if (name.equals("_SERVER")) { + return DirectoryBehavior.SKIP; + } else if (name.equals("_CLIENT")) { + return DirectoryBehavior.IGNORE; + } else { + return DirectoryBehavior.CONTINUE; + } + } + +} diff --git a/launcher-builder/src/main/java/com/skcraft/launcher/builder/Compressor.java b/launcher-builder/src/main/java/com/skcraft/launcher/builder/Compressor.java new file mode 100644 index 0000000..40958b1 --- /dev/null +++ b/launcher-builder/src/main/java/com/skcraft/launcher/builder/Compressor.java @@ -0,0 +1,48 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.builder; + +import org.apache.commons.compress.compressors.CompressorException; +import org.apache.commons.compress.compressors.CompressorStreamFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class Compressor { + + private static final CompressorStreamFactory factory = new CompressorStreamFactory(); + + private final String extension; + private final String format; + + public Compressor(String extension, String format) { + this.extension = extension; + this.format = format; + } + + public String transformPathname(String filename) { + return filename + "." + extension; + } + + public InputStream createInputStream(InputStream inputStream) throws IOException { + try { + return factory.createCompressorInputStream(format, inputStream); + } catch (CompressorException e) { + throw new IOException("Failed to create decompressor", e); + } + } + + public OutputStream createOutputStream(OutputStream outputStream) throws IOException { + try { + return factory.createCompressorOutputStream(format, outputStream); + } catch (CompressorException e) { + throw new IOException("Failed to create compressor", e); + } + } + +} diff --git a/launcher-builder/src/main/java/com/skcraft/launcher/builder/DirectoryWalker.java b/launcher-builder/src/main/java/com/skcraft/launcher/builder/DirectoryWalker.java new file mode 100644 index 0000000..f92726c --- /dev/null +++ b/launcher-builder/src/main/java/com/skcraft/launcher/builder/DirectoryWalker.java @@ -0,0 +1,99 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.builder; + +import lombok.NonNull; + +import java.io.File; +import java.io.IOException; + +/** + * Abstract class to recursively walk a directory, keep track of a relative + * path (which may be modified by dropping certain directory entries), + * and call {@link #onFile(java.io.File, String)} with each file. + */ +public abstract class DirectoryWalker { + + public enum DirectoryBehavior { + /** + * Continue and add the given directory to the relative path. + */ + CONTINUE, + /** + * Continue but don't add the given directory to the relative path. + */ + IGNORE, + /** + * Don't walk this directory. + */ + SKIP + } + + /** + * Walk the given directory. + * + * @param dir the directory + * @throws IOException thrown on I/O error + */ + public final void walk(@NonNull File dir) throws IOException { + walk(dir, ""); + } + + /** + * Recursively walk the given directory and keep track of the relative path. + * + * @param dir the directory + * @param basePath the base path + * @throws IOException + */ + private void walk(@NonNull File dir, @NonNull String basePath) throws IOException { + if (!dir.isDirectory()) { + throw new IllegalArgumentException(dir.getAbsolutePath() + " is not a directory"); + } + + File[] files = dir.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + String newPath = basePath; + + switch (getBehavior(file.getName())) { + case CONTINUE: + newPath += file.getName() + "/"; + case IGNORE: + walk(file, newPath); + break; + case SKIP: break; + } + } else { + onFile(file, basePath + file.getName()); + } + } + } + } + + /** + * Return the behavior for the given directory name. + * + * @param name the directory name + * @return the behavor + */ + protected DirectoryBehavior getBehavior(String name) { + return DirectoryBehavior.CONTINUE; + } + + /** + * Callback on each file. + * + * @param file the file + * @param relPath the relative path + * @throws IOException thrown on I/O error + */ + protected abstract void onFile(File file, String relPath) throws IOException; + + +} diff --git a/launcher-builder/src/main/java/com/skcraft/launcher/builder/FeaturePattern.java b/launcher-builder/src/main/java/com/skcraft/launcher/builder/FeaturePattern.java new file mode 100644 index 0000000..13a2e71 --- /dev/null +++ b/launcher-builder/src/main/java/com/skcraft/launcher/builder/FeaturePattern.java @@ -0,0 +1,24 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.builder; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.skcraft.launcher.model.modpack.Feature; +import lombok.Data; + +@Data +public class FeaturePattern { + + @JsonProperty("properties") + private Feature feature; + @JsonProperty("files") + private FnPatternList filePatterns = new FnPatternList(); + + public boolean matches(String path) { + return filePatterns != null && filePatterns.matches(path); + } +} diff --git a/launcher-builder/src/main/java/com/skcraft/launcher/builder/FileInfo.java b/launcher-builder/src/main/java/com/skcraft/launcher/builder/FileInfo.java new file mode 100644 index 0000000..bea808a --- /dev/null +++ b/launcher-builder/src/main/java/com/skcraft/launcher/builder/FileInfo.java @@ -0,0 +1,17 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.builder; + +import com.skcraft.launcher.model.modpack.Feature; +import lombok.Data; + +@Data +public class FileInfo { + + private Feature feature; + +} diff --git a/launcher-builder/src/main/java/com/skcraft/launcher/builder/FileInfoScanner.java b/launcher-builder/src/main/java/com/skcraft/launcher/builder/FileInfoScanner.java new file mode 100644 index 0000000..bd1c1b7 --- /dev/null +++ b/launcher-builder/src/main/java/com/skcraft/launcher/builder/FileInfoScanner.java @@ -0,0 +1,75 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.builder; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.skcraft.launcher.model.modpack.Feature; +import lombok.Getter; +import lombok.extern.java.Log; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Strings.emptyToNull; +import static com.skcraft.launcher.builder.ClientFileCollector.getDirectoryBehavior; +import static org.apache.commons.io.FilenameUtils.*; + +@Log +public class FileInfoScanner extends DirectoryWalker { + + private static final EnumSet MATCH_FLAGS = EnumSet.of( + FnMatch.Flag.CASEFOLD, FnMatch.Flag.PERIOD, FnMatch.Flag.PATHNAME); + public static final String FILE_SUFFIX = ".info.json"; + + private final ObjectMapper mapper; + @Getter + private final List patterns = new ArrayList(); + + public FileInfoScanner(ObjectMapper mapper) { + this.mapper = mapper; + } + + @Override + protected DirectoryBehavior getBehavior(String name) { + return getDirectoryBehavior(name); + } + + @Override + protected void onFile(File file, String relPath) throws IOException { + if (file.getName().endsWith(FILE_SUFFIX)) { + String fnPattern = + separatorsToUnix(getPath(relPath)) + + getBaseName(getBaseName(file.getName())) + "*"; + + FileInfo info = mapper.readValue(file, FileInfo.class); + Feature feature = info.getFeature(); + + if (feature != null) { + checkNotNull(emptyToNull(feature.getName()), + "Empty component name found in " + file.getAbsolutePath()); + + List patterns = new ArrayList(); + patterns.add(fnPattern); + FnPatternList patternList = new FnPatternList(); + patternList.setInclude(patterns); + patternList.setFlags(MATCH_FLAGS); + FeaturePattern fp = new FeaturePattern(); + fp.setFeature(feature); + fp.setFilePatterns(patternList); + getPatterns().add(fp); + + FileInfoScanner.log.info("Found .info.json file at " + file.getAbsolutePath() + + ", with pattern " + fnPattern + ", and component " + feature); + } + } + } + +} diff --git a/launcher-builder/src/main/java/com/skcraft/launcher/builder/FileUrlRedirect.java b/launcher-builder/src/main/java/com/skcraft/launcher/builder/FileUrlRedirect.java new file mode 100644 index 0000000..6115f56 --- /dev/null +++ b/launcher-builder/src/main/java/com/skcraft/launcher/builder/FileUrlRedirect.java @@ -0,0 +1,43 @@ +package com.skcraft.launcher.builder; + +import com.google.common.io.Files; +import lombok.Data; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.charset.Charset; +import java.util.List; + +import static com.skcraft.launcher.util.HttpRequest.url; + +@Data +public class FileUrlRedirect { + private URL url; + private String hash; + + public void readFromFile(File file) throws IOException { + List lines = Files.readLines(file, Charset.defaultCharset()); + this.url = url(lines.get(0)); + + if (lines.size() > 1) { + String hash = lines.get(1); + + if (!hash.isEmpty()) { + this.hash = hash; + } + } + } + + public void writeToFile(File file) throws IOException { + String entry = url.toString() + '\n' + hash; + + Files.write(entry, file, Charset.defaultCharset()); + } + + public static FileUrlRedirect fromFile(File file) throws IOException { + FileUrlRedirect entry = new FileUrlRedirect(); + entry.readFromFile(file); + return entry; + } +} diff --git a/launcher-builder/src/main/java/com/skcraft/launcher/builder/FileUrlScanner.java b/launcher-builder/src/main/java/com/skcraft/launcher/builder/FileUrlScanner.java new file mode 100644 index 0000000..b89f3f8 --- /dev/null +++ b/launcher-builder/src/main/java/com/skcraft/launcher/builder/FileUrlScanner.java @@ -0,0 +1,58 @@ +package com.skcraft.launcher.builder; + +import com.google.common.io.Files; +import com.skcraft.launcher.util.FileUtils; +import com.skcraft.launcher.util.HttpRequest; +import lombok.extern.java.Log; + +import java.io.File; +import java.io.IOException; + +@Log +public class FileUrlScanner extends DirectoryWalker { + public static final String URL_FILE_SUFFIX = ".url.txt"; + + public static boolean isEnabled() { + return !System.getProperty("com.skcraft.builder.ignoreURLOverrides", "false") + .equalsIgnoreCase("true"); + } + + @Override + protected void onFile(File file, String relPath) throws IOException { + if (!file.getName().endsWith(URL_FILE_SUFFIX)) return; + + log.info("Found URL file " + file.getName()); + + File targetFile = new File(file.getAbsoluteFile().getParentFile(), + file.getName().replace(URL_FILE_SUFFIX, "")); + FileUrlRedirect info = FileUrlRedirect.fromFile(file); + + if (targetFile.exists()) { + String localHash = FileUtils.getShaHash(targetFile); + if (info.getHash() == null) { + // Disabled for now, let's not touch source files +// info.setHash(localHash); +// info.writeToFile(file); + return; + } + + // If everything matches, skip this file + if (info.getHash().equals(localHash)) return; + } + + File tempFile = File.createTempFile("launcherlib", null); + + try { + log.info("Downloading file " + targetFile.getName() + " from " + info.getUrl() + "..."); + HttpRequest.get(info.getUrl()) + .execute() + .expectResponseCode(200) + .saveContent(tempFile); + } catch (InterruptedException e) { + throw new IOException(e); + } + + Files.move(tempFile, targetFile); + log.info("Updated " + targetFile.getName() + " from " + file.getName()); + } +} diff --git a/launcher-builder/src/main/java/com/skcraft/launcher/builder/FnMatch.java b/launcher-builder/src/main/java/com/skcraft/launcher/builder/FnMatch.java new file mode 100644 index 0000000..9a65859 --- /dev/null +++ b/launcher-builder/src/main/java/com/skcraft/launcher/builder/FnMatch.java @@ -0,0 +1,246 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ +/* $OpenBSD: fnmatch.c,v 1.13 2006/03/31 05:34:14 deraadt Exp $ */ + +package com.skcraft.launcher.builder; + +import java.util.EnumSet; + +/* + * Function fnmatch() as specified in POSIX 1003.2-1992, section B.6. + * Compares a filename or pathname to a pattern. + */ +public class FnMatch { + + public static enum Flag { + + /** Disable backslash escaping. */ + NOESCAPE, + /** Slash must be matched by slash. */ + PATHNAME, + /** Period must be matched by period. */ + PERIOD, + /** Ignore / after Imatch. */ + LEADING_DIR, + /** Case insensitive search. */ + CASEFOLD + } + private static final int RANGE_ERROR = -1; + private static final int RANGE_NOMATCH = 0; + + public static boolean fnmatch(String pattern, String string, EnumSet flags) { + return match(pattern, 0, string, 0, flags); + } + + public static boolean fnmatch(String pattern, String string, int stringPos, Flag flag) { + return match(pattern, 0, string, stringPos, EnumSet.of(flag)); + } + + public static boolean fnmatch(String pattern, String string, int stringPos) { + return match(pattern, 0, string, stringPos, EnumSet.noneOf(Flag.class)); + } + + public static boolean fnmatch(String pattern, String string) { + return fnmatch(pattern, string, 0); + } + + private static boolean match(String pattern, int patternPos, + String string, int stringPos, EnumSet flags) { + char c; + + while (true) { + if (patternPos >= pattern.length()) { + if (flags.contains(Flag.LEADING_DIR) && string.charAt(stringPos) == '/') { + return true; + } + return stringPos == string.length(); + } + c = pattern.charAt(patternPos++); + switch (c) { + case '?': + if (stringPos >= string.length()) { + return false; + } + if (string.charAt(stringPos) == '/' && flags.contains(Flag.PATHNAME)) { + return false; + } + if (hasLeadingPeriod(string, stringPos, flags)) { + return false; + } + ++stringPos; + continue; + case '*': + /* Collapse multiple stars. */ + while (patternPos < pattern.length() && + (c = pattern.charAt(patternPos)) == '*') { + patternPos++; + } + + if (hasLeadingPeriod(string, stringPos, flags)) { + return false; + } + + /* Optimize for pattern with * at end or before /. */ + if (patternPos == pattern.length()) { + if (flags.contains(Flag.PATHNAME)) { + return flags.contains(Flag.LEADING_DIR) || + string.indexOf('/', stringPos) == -1; + } + return true; + } else if (c == '/' && flags.contains(Flag.PATHNAME)) { + stringPos = string.indexOf('/', stringPos); + if (stringPos == -1) { + return false; + } + continue; + } + + /* General case, use recursion. */ + while (stringPos < string.length()) { + if (flags.contains(Flag.PERIOD)) { + flags = EnumSet.copyOf(flags); + flags.remove(Flag.PERIOD); + } + if (match(pattern, patternPos, string, stringPos, flags)) { + return true; + } + if (string.charAt(stringPos) == '/' && flags.contains(Flag.PATHNAME)) { + break; + } + ++stringPos; + } + return false; + + case '[': + if (stringPos >= string.length()) { + return false; + } + if (string.charAt(stringPos) == '/' && flags.contains(Flag.PATHNAME)) { + return false; + } + if (hasLeadingPeriod(string, stringPos, flags)) { + return false; + } + + int result = matchRange(pattern, patternPos, string.charAt(stringPos), flags); + if (result == RANGE_ERROR) /* not a good range, treat as normal text */ { + break; + } + + if (result == RANGE_NOMATCH) { + return false; + } + + patternPos = result; + ++stringPos; + continue; + + case '\\': + if (!flags.contains(Flag.NOESCAPE)) { + if (patternPos >= pattern.length()) { + c = '\\'; + } else { + c = pattern.charAt(patternPos++); + } + } + break; + } + + if (stringPos >= string.length()) { + return false; + } + if (c != string.charAt(stringPos) && + !(flags.contains(Flag.CASEFOLD) && + Character.toLowerCase(c) == Character.toLowerCase(string.charAt(stringPos)))) { + return false; + } + ++stringPos; + } + /* NOTREACHED */ + } + + private static boolean hasLeadingPeriod(String string, int stringPos, EnumSet flags) { + if (stringPos > string.length() - 1) + return false; + return (stringPos == 0 + || (flags.contains(Flag.PATHNAME) && string.charAt(stringPos - 1) == '/')) + && string.charAt(stringPos) == '.' && flags.contains(Flag.PERIOD); + } + + private static int matchRange(String pattern, int patternPos, char test, EnumSet flags) { + boolean negate, ok; + char c, c2; + + if (patternPos >= pattern.length()) { + return RANGE_ERROR; + } + + /* + * A bracket expression starting with an unquoted circumflex + * character produces unspecified results (IEEE 1003.2-1992, + * 3.13.2). This implementation treats it like '!', for + * consistency with the regular expression syntax. + * J.T. Conklin (conklin@ngai.kaleida.com) + */ + c = pattern.charAt(patternPos); + negate = c == '!' || c == '^'; + if (negate) { + ++patternPos; + } + + if (flags.contains(Flag.CASEFOLD)) { + test = Character.toLowerCase(test); + } + + /* + * A right bracket shall lose its special meaning and represent + * itself in a bracket expression if it occurs first in the list. + * -- POSIX.2 2.8.3.2 + */ + ok = false; + while (true) { + if (patternPos >= pattern.length()) { + return RANGE_ERROR; + } + + c = pattern.charAt(patternPos++); + if (c == ']') { + break; + } + + if (c == '\\' && !flags.contains(Flag.NOESCAPE)) { + c = pattern.charAt(patternPos++); + } + if (c == '/' && flags.contains(Flag.PATHNAME)) { + return RANGE_NOMATCH; + } + if (flags.contains(Flag.CASEFOLD)) { + c = Character.toLowerCase(c); + } + if (pattern.charAt(patternPos) == '-' && + patternPos + 1 < pattern.length() && + (c2 = pattern.charAt(patternPos + 1)) != ']') { + patternPos += 2; + if (c2 == '\\' && !flags.contains(Flag.NOESCAPE)) { + if (patternPos >= pattern.length()) { + return RANGE_ERROR; + } + c = pattern.charAt(patternPos++); + } + if (flags.contains(Flag.CASEFOLD)) { + c2 = Character.toLowerCase(c2); + } + if (c <= test && test <= c2) { + ok = true; + } + } else if (c == test) { + ok = true; + } + } + + return ok == negate ? RANGE_NOMATCH : patternPos; + } +} \ No newline at end of file diff --git a/launcher-builder/src/main/java/com/skcraft/launcher/builder/FnPatternList.java b/launcher-builder/src/main/java/com/skcraft/launcher/builder/FnPatternList.java new file mode 100644 index 0000000..b800f8f --- /dev/null +++ b/launcher-builder/src/main/java/com/skcraft/launcher/builder/FnPatternList.java @@ -0,0 +1,52 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.builder; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.google.common.collect.Lists; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; + +import java.util.Collection; +import java.util.EnumSet; +import java.util.List; + +@Data +public class FnPatternList { + + private static final EnumSet DEFAULT_FLAGS = EnumSet.of( + FnMatch.Flag.CASEFOLD, FnMatch.Flag.PERIOD); + + private List include = Lists.newArrayList(); + private List exclude = Lists.newArrayList(); + @Getter @Setter @JsonIgnore + private EnumSet flags = DEFAULT_FLAGS; + + public void setInclude(List include) { + this.include = include != null ? include : Lists.newArrayList(); + } + + public void setExclude(List exclude) { + this.exclude = exclude != null ? exclude : Lists.newArrayList(); + } + + public boolean matches(String path) { + return include != null && matches(path, include) && (exclude == null || !matches(path, exclude)); + } + + public boolean matches(String path, Collection patterns) { + for (String pattern : patterns) { + if (FnMatch.fnmatch(pattern, path, flags)) { + return true; + } + } + + return false; + } + +} diff --git a/launcher-builder/src/main/java/com/skcraft/launcher/builder/JarFileFilter.java b/launcher-builder/src/main/java/com/skcraft/launcher/builder/JarFileFilter.java new file mode 100644 index 0000000..31ca410 --- /dev/null +++ b/launcher-builder/src/main/java/com/skcraft/launcher/builder/JarFileFilter.java @@ -0,0 +1,19 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.builder; + +import java.io.File; +import java.io.FileFilter; + +public class JarFileFilter implements FileFilter { + + @Override + public boolean accept(File pathname) { + return pathname.getName().toLowerCase().endsWith(".jar"); + } + +} diff --git a/launcher-builder/src/main/java/com/skcraft/launcher/builder/PackageBuilder.java b/launcher-builder/src/main/java/com/skcraft/launcher/builder/PackageBuilder.java new file mode 100644 index 0000000..9981a17 --- /dev/null +++ b/launcher-builder/src/main/java/com/skcraft/launcher/builder/PackageBuilder.java @@ -0,0 +1,451 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.builder; + +import com.beust.jcommander.JCommander; +import com.beust.jcommander.ParameterException; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.common.io.ByteStreams; +import com.google.common.io.Closer; +import com.google.common.io.Files; +import com.skcraft.launcher.Launcher; +import com.skcraft.launcher.LauncherUtils; +import com.skcraft.launcher.builder.loaders.*; +import com.skcraft.launcher.model.loader.BasicInstallProfile; +import com.skcraft.launcher.model.minecraft.Library; +import com.skcraft.launcher.model.minecraft.ReleaseList; +import com.skcraft.launcher.model.minecraft.Version; +import com.skcraft.launcher.model.minecraft.VersionManifest; +import com.skcraft.launcher.model.modpack.Manifest; +import com.skcraft.launcher.util.HttpRequest; +import com.skcraft.launcher.util.SimpleLogFormatter; +import lombok.Getter; +import lombok.NonNull; +import lombok.Setter; +import lombok.extern.java.Log; + +import java.io.*; +import java.net.URL; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Properties; +import java.util.jar.JarFile; +import java.util.logging.Level; +import java.util.zip.ZipEntry; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Strings.emptyToNull; +import static com.skcraft.launcher.util.HttpRequest.url; + +/** + * Builds packages for the launcher. + */ +@Log +public class PackageBuilder { + private final Properties properties; + private final ObjectMapper mapper; + private ObjectWriter writer; + private final Manifest manifest; + private final PropertiesApplicator applicator; + @Getter + private boolean prettyPrint = false; + + @Getter @Setter + private File baseDir; + + private List loaderLibraries = Lists.newArrayList(); + private List installerLibraries = Lists.newArrayList(); + private List mavenRepos; + private List jarMavens = Lists.newArrayList(); + + /** + * Create a new package builder. + * + * @param mapper the mapper + * @param manifest the manifest + */ + public PackageBuilder(@NonNull ObjectMapper mapper, @NonNull Manifest manifest) throws IOException { + this.properties = LauncherUtils.loadProperties(Launcher.class, + "launcher.properties", "com.skcraft.launcher.propertiesFile"); + + this.mapper = mapper; + this.manifest = manifest; + this.applicator = new PropertiesApplicator(manifest); + setPrettyPrint(false); // Set writer + + Closer closer = Closer.create(); + try { + mavenRepos = mapper.readValue(closer.register(Launcher.class.getResourceAsStream("maven_repos.json")), new TypeReference>() { + }); + } finally { + closer.close(); + } + } + + public void setPrettyPrint(boolean prettyPrint) { + if (prettyPrint) { + writer = mapper.writerWithDefaultPrettyPrinter(); + } else { + writer = mapper.writer(); + } + this.prettyPrint = prettyPrint; + } + + public void scan(File dir) throws IOException { + logSection("Scanning for .info.json files..."); + + FileInfoScanner infoScanner = new FileInfoScanner(mapper); + infoScanner.walk(dir); + for (FeaturePattern pattern : infoScanner.getPatterns()) { + applicator.register(pattern); + } + + logSection("Scanning for .url.txt files..."); + FileUrlScanner urlScanner = new FileUrlScanner(); + urlScanner.walk(dir); + } + + public void addFiles(File dir, File destDir) throws IOException { + logSection("Adding files to modpack..."); + + ClientFileCollector collector = new ClientFileCollector(this.manifest, applicator, destDir); + collector.walk(dir); + } + + public void addLoaders(File dir, File librariesDir) { + logSection("Checking for mod loaders to install..."); + + LinkedHashSet collected = new LinkedHashSet(); + + File[] files = dir.listFiles(new JarFileFilter()); + if (files != null) { + for (File file : files) { + try { + processLoader(collected, file, librariesDir); + } catch (IOException e) { + log.log(Level.WARNING, "Failed to add the loader at " + file.getAbsolutePath(), e); + } + } + } + + this.loaderLibraries.addAll(collected); + + VersionManifest version = manifest.getVersionManifest(); + collected.addAll(version.getLibraries()); + version.setLibraries(collected); + } + + private void processLoader(LinkedHashSet loaderLibraries, File file, File librariesDir) throws IOException { + log.info("Installing " + file.getName() + "..."); + + JarFile jarFile = new JarFile(file); + Closer closer = Closer.create(); + ILoaderProcessor processor = null; + + try { + ZipEntry profileEntry = BuilderUtils.getZipEntry(jarFile, "install_profile.json"); + + if (profileEntry != null) { + InputStream stream = jarFile.getInputStream(profileEntry); + InputStreamReader reader = closer.register(new InputStreamReader(stream)); + + BasicInstallProfile basicProfile = mapper.readValue(BuilderUtils.readStringFromStream(reader), + BasicInstallProfile.class); + + if (basicProfile.isLegacy()) { + processor = new OldForgeLoaderProcessor(); + } else { + processor = new ModernForgeLoaderProcessor(); + } + } else if (BuilderUtils.getZipEntry(jarFile, "fabric-installer.json") != null) { + processor = new FabricLoaderProcessor(FabricLoaderProcessor.Variant.FABRIC); + } else if (BuilderUtils.getZipEntry(jarFile, "quilt_installer.json") != null) { + processor = new FabricLoaderProcessor(FabricLoaderProcessor.Variant.QUILT); + } + } finally { + closer.close(); + jarFile.close(); + } + + if (processor != null) { + LoaderResult result = processor.process(file, manifest, mapper, baseDir); + + if (result == null) { + log.warning("Loader " + file.getName() + " failed to process."); + return; + } + + loaderLibraries.addAll(result.getLoaderLibraries()); + installerLibraries.addAll(result.getProcessorLibraries()); + jarMavens.addAll(result.getJarMavens()); + } else { + log.warning("Loader " + file.getName() + " was skipped due to missing metadata. " + + "Is it really a loader JAR?"); + } + } + + public void downloadLibraries(File librariesDir) throws IOException, InterruptedException { + logSection("Downloading libraries..."); + + for (Library library : Iterables.concat(loaderLibraries, installerLibraries)) { + library.ensureDownloadsExist(); + + for (Library.Artifact artifact : library.getDownloads().getAllArtifacts()) { + File outputPath = new File(librariesDir, artifact.getPath()); + + if (!outputPath.exists()) { + Files.createParentDirs(outputPath); + boolean found = false; + boolean urlEmpty = artifact.getUrl().isEmpty(); + + // If URL doesn't end with a /, it might be the direct file + if (!urlEmpty && !artifact.getUrl().endsWith("/")) { + found = tryDownloadLibrary(library, artifact, artifact.getUrl(), outputPath); + } + + // Look inside the loader JARs + if (!found) { + for (URL base : jarMavens) { + found = tryFetchLibrary(library, new URL(base, artifact.getPath()), outputPath); + if (found) break; + } + } + + // Assume artifact URL is a maven repository URL and try that + if (!found && !urlEmpty) { + URL url = LauncherUtils.concat(url(artifact.getUrl()), artifact.getPath()); + found = tryDownloadLibrary(library, artifact, url.toString(), outputPath); + } + + // Try each repository if not found yet + if (!found) { + for (String baseUrl : mavenRepos) { + found = tryDownloadLibrary(library, artifact, baseUrl + artifact.getPath(), + outputPath); + if (found) break; + } + } + + if (!found) { + log.warning("!! Failed to download the library " + library.getName() + + " -- this means your copy of the libraries will lack this file"); + } + } + } + } + } + + private boolean tryDownloadLibrary(Library library, Library.Artifact artifact, String baseUrl, File outputPath) + throws IOException, InterruptedException { + URL url = new URL(baseUrl); + + if (url.getPath().isEmpty() || url.getPath().equals("/")) { + // empty path, this is probably the first "is this a full URL" try. + return false; + } + + // Some repositories compress their files + List compressors = BuilderUtils.getCompressors(baseUrl); + for (Compressor compressor : Lists.reverse(compressors)) { + url = new URL(compressor.transformPathname(url.toString())); + } + + File tempFile = File.createTempFile("launcherlib", null); + + try { + log.info("Downloading library " + library.getName() + " from " + url + "..."); + HttpRequest.get(url).execute().expectResponseCode(200) + .expectContentType("application/java-archive", "application/octet-stream", "application/zip") + .saveContent(tempFile); + } catch (IOException e) { + log.info("Could not get file from " + url + ": " + e.getMessage()); + return false; + } + + writeLibraryToFile(outputPath, tempFile, compressors); + return true; + } + + private boolean tryFetchLibrary(Library library, URL url, File outputPath) + throws IOException { + File tempFile = File.createTempFile("launcherlib", null); + + Closer closer = Closer.create(); + try { + log.info("Reading library " + library.getName() + " from " + url.toString()); + InputStream stream = closer.register(url.openStream()); + stream = closer.register(new BufferedInputStream(stream)); + + ByteStreams.copy(stream, closer.register(new FileOutputStream(tempFile))); + } catch (IOException e) { + log.info("Could not get file from " + url + ": " + e.getMessage()); + return false; + } finally { + closer.close(); + } + + writeLibraryToFile(outputPath, tempFile, Collections.emptyList()); + return true; + } + + private void writeLibraryToFile(File outputPath, File inputFile, List compressors) throws IOException { + // Decompress (if needed) and write to file + Closer closer = Closer.create(); + InputStream inputStream = closer.register(new FileInputStream(inputFile)); + inputStream = closer.register(new BufferedInputStream(inputStream)); + for (Compressor compressor : compressors) { + inputStream = closer.register(compressor.createInputStream(inputStream)); + } + ByteStreams.copy(inputStream, closer.register(new FileOutputStream(outputPath))); + + inputFile.delete(); + closer.close(); + } + + public void validateManifest() { + checkNotNull(emptyToNull(manifest.getName()), "Package name is not defined"); + checkNotNull(emptyToNull(manifest.getGameVersion()), "Game version is not defined"); + } + + public void readConfig(File path) throws IOException { + if (path != null) { + BuilderConfig config = read(path, BuilderConfig.class); + config.update(manifest); + config.registerProperties(applicator); + } + } + + public void readVersionManifest(File path) throws IOException, InterruptedException { + logSection("Reading version manifest..."); + + if (path.exists()) { + VersionManifest versionManifest = read(path, VersionManifest.class); + manifest.setVersionManifest(versionManifest); + + log.info("Loaded version manifest from " + path.getAbsolutePath()); + } else { + URL url = url(properties.getProperty("versionManifestUrl")); + + log.info("Fetching version manifest from " + url + "..."); + + ReleaseList releases = HttpRequest.get(url) + .execute() + .expectResponseCode(200) + .returnContent() + .asJson(ReleaseList.class); + + Version version = releases.find(manifest.getGameVersion()); + VersionManifest versionManifest = HttpRequest.get(url(version.getUrl())) + .execute() + .expectResponseCode(200) + .returnContent() + .asJson(VersionManifest.class); + + manifest.setVersionManifest(versionManifest); + } + } + + public void writeManifest(@NonNull File path) throws IOException { + logSection("Writing manifest..."); + + manifest.setFeatures(applicator.getFeaturesInUse()); + VersionManifest versionManifest = manifest.getVersionManifest(); + if (versionManifest != null) { + versionManifest.setId(manifest.getGameVersion()); + } + validateManifest(); + path.getAbsoluteFile().getParentFile().mkdirs(); + writer.writeValue(path, manifest); + + log.info("Wrote manifest to " + path.getAbsolutePath()); + } + + private static BuilderOptions parseArgs(String[] args) { + BuilderOptions options = new BuilderOptions(); + new JCommander(options).parse(args); + options.choosePaths(); + return options; + } + + private V read(File path, Class clazz) throws IOException { + try { + if (path == null) { + return clazz.newInstance(); + } else { + return mapper.readValue(path, clazz); + } + } catch (InstantiationException e) { + throw new IOException("Failed to create " + clazz.getCanonicalName(), e); + } catch (IllegalAccessException e) { + throw new IOException("Failed to create " + clazz.getCanonicalName(), e); + } + } + + /** + * Build a package given the arguments. + * + * @param args arguments + * @throws IOException thrown on I/O error + * @throws InterruptedException on interruption + */ + public static void main(String[] args) throws IOException, InterruptedException { + BuilderOptions options; + try { + options = parseArgs(args); + } catch (ParameterException e) { + new JCommander().usage(); + System.err.println("error: " + e.getMessage()); + System.exit(1); + return; + } + + // Initialize + SimpleLogFormatter.configureGlobalLogger(); + ObjectMapper mapper = new ObjectMapper(); + mapper.setSerializationInclusion(JsonInclude.Include.NON_ABSENT); + + Manifest manifest = new Manifest(); + manifest.setMinimumVersion(Manifest.MIN_PROTOCOL_VERSION); + PackageBuilder builder = new PackageBuilder(mapper, manifest); + builder.setPrettyPrint(options.isPrettyPrinting()); + + // From config + builder.readConfig(options.getConfigPath()); + builder.readVersionManifest(options.getVersionManifestPath()); + builder.setBaseDir(options.getOutputPath()); + + // From options + manifest.updateName(options.getName()); + manifest.updateTitle(options.getTitle()); + manifest.updateGameVersion(options.getGameVersion()); + manifest.setVersion(options.getVersion()); + manifest.setLibrariesLocation(options.getLibrariesLocation()); + manifest.setObjectsLocation(options.getObjectsLocation()); + + builder.scan(options.getFilesDir()); + builder.addFiles(options.getFilesDir(), options.getObjectsDir()); + builder.addLoaders(options.getLoadersDir(), options.getLibrariesDir()); + builder.downloadLibraries(options.getLibrariesDir()); + builder.writeManifest(options.getManifestPath()); + + logSection("Done"); + + log.info("Now upload the contents of " + options.getOutputPath() + " to your web server or CDN!"); + } + + private static void logSection(String name) { + log.info(""); + log.info("--- " + name + " ---"); + } + +} diff --git a/launcher-builder/src/main/java/com/skcraft/launcher/builder/PropertiesApplicator.java b/launcher-builder/src/main/java/com/skcraft/launcher/builder/PropertiesApplicator.java new file mode 100644 index 0000000..586a04b --- /dev/null +++ b/launcher-builder/src/main/java/com/skcraft/launcher/builder/PropertiesApplicator.java @@ -0,0 +1,74 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.builder; + +import com.skcraft.launcher.model.modpack.*; +import lombok.Getter; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class PropertiesApplicator { + + private final Manifest manifest; + private final Set used = new HashSet(); + private final List features = new ArrayList(); + @Getter @Setter + private FnPatternList userFiles; + + public PropertiesApplicator(Manifest manifest) { + this.manifest = manifest; + } + + public void apply(ManifestEntry entry) { + if (entry instanceof FileInstall) { + apply((FileInstall) entry); + } + } + + private void apply(FileInstall entry) { + String path = entry.getTargetPath(); + entry.setWhen(fromFeature(path)); + entry.setUserFile(isUserFile(path)); + } + + public boolean isUserFile(String path) { + if (userFiles != null) { + return userFiles.matches(path); + } else { + return false; + } + } + + public Condition fromFeature(String path) { + List found = new ArrayList(); + for (FeaturePattern pattern : features) { + if (pattern.matches(path)) { + used.add(pattern.getFeature()); + found.add(pattern.getFeature()); + } + } + + if (!found.isEmpty()) { + return new RequireAny(found); + } else { + return null; + } + } + + public void register(FeaturePattern component) { + features.add(component); + } + + public List getFeaturesInUse() { + return new ArrayList(used); + } + +} diff --git a/launcher-builder/src/main/java/com/skcraft/launcher/builder/ServerCopyExport.java b/launcher-builder/src/main/java/com/skcraft/launcher/builder/ServerCopyExport.java new file mode 100644 index 0000000..fa3da44 --- /dev/null +++ b/launcher-builder/src/main/java/com/skcraft/launcher/builder/ServerCopyExport.java @@ -0,0 +1,61 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.builder; + +import com.beust.jcommander.JCommander; +import com.google.common.io.Files; +import com.skcraft.launcher.util.SimpleLogFormatter; +import lombok.NonNull; +import lombok.extern.java.Log; + +import java.io.File; +import java.io.IOException; + +@Log +public class ServerCopyExport extends DirectoryWalker { + + private final File destDir; + + public ServerCopyExport(@NonNull File destDir) { + this.destDir = destDir; + } + + @Override + protected DirectoryBehavior getBehavior(String name) { + if (name.startsWith(".")) { + return DirectoryBehavior.SKIP; + } else if (name.equals("_SERVER")) { + return DirectoryBehavior.IGNORE; + } else if (name.equals("_CLIENT")) { + return DirectoryBehavior.SKIP; + } else { + return DirectoryBehavior.CONTINUE; + } + } + + @Override + protected void onFile(File file, String relPath) throws IOException { + File dest = new File(destDir, relPath); + + log.info("Copying " + file.getAbsolutePath() + " to " + dest.getAbsolutePath()); + dest.getParentFile().mkdirs(); + Files.copy(file, dest); + } + + public static void main(String[] args) throws IOException { + SimpleLogFormatter.configureGlobalLogger(); + + ServerExportOptions options = new ServerExportOptions(); + new JCommander(options, args); + + log.info("From: " + options.getSourceDir().getAbsolutePath()); + log.info("To: " + options.getDestDir().getAbsolutePath()); + ServerCopyExport task = new ServerCopyExport(options.getDestDir()); + task.walk(options.getSourceDir()); + } + +} diff --git a/launcher-builder/src/main/java/com/skcraft/launcher/builder/ServerExportOptions.java b/launcher-builder/src/main/java/com/skcraft/launcher/builder/ServerExportOptions.java new file mode 100644 index 0000000..2e243f4 --- /dev/null +++ b/launcher-builder/src/main/java/com/skcraft/launcher/builder/ServerExportOptions.java @@ -0,0 +1,22 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.builder; + +import com.beust.jcommander.Parameter; +import lombok.Data; + +import java.io.File; + +@Data +public class ServerExportOptions { + + @Parameter(names = "--source", required = true) + private File sourceDir; + @Parameter(names = "--dest", required = true) + private File destDir; + +} diff --git a/launcher-builder/src/main/java/com/skcraft/launcher/builder/loaders/FabricLoaderProcessor.java b/launcher-builder/src/main/java/com/skcraft/launcher/builder/loaders/FabricLoaderProcessor.java new file mode 100644 index 0000000..f3362a6 --- /dev/null +++ b/launcher-builder/src/main/java/com/skcraft/launcher/builder/loaders/FabricLoaderProcessor.java @@ -0,0 +1,103 @@ +package com.skcraft.launcher.builder.loaders; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.io.Closer; +import com.skcraft.launcher.builder.BuilderUtils; +import com.skcraft.launcher.model.loader.FabricMod; +import com.skcraft.launcher.model.loader.QuiltMod; +import com.skcraft.launcher.model.loader.Versionable; +import com.skcraft.launcher.model.minecraft.Library; +import com.skcraft.launcher.model.minecraft.VersionManifest; +import com.skcraft.launcher.model.modpack.Manifest; +import com.skcraft.launcher.util.HttpRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.java.Log; + +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.util.jar.JarFile; +import java.util.zip.ZipEntry; + +@Log +@RequiredArgsConstructor +public class FabricLoaderProcessor implements ILoaderProcessor { + private final Variant variant; + + @Override + public LoaderResult process(File loaderJar, Manifest manifest, ObjectMapper mapper, File baseDir) throws IOException { + JarFile jarFile = new JarFile(loaderJar); + LoaderResult result = new LoaderResult(); + Closer closer = Closer.create(); + + try { + Versionable loaderMod; + + ZipEntry modEntry = BuilderUtils.getZipEntry(jarFile, variant.modJsonName); + if (modEntry != null) { + InputStreamReader reader = new InputStreamReader(jarFile.getInputStream(modEntry)); + + loaderMod = mapper.readValue( + BuilderUtils.readStringFromStream(closer.register(reader)), variant.mappedClass); + } else { + log.warning(String.format("%s loader has no '%s' file, is it really a %s Loader jar?", + variant.friendlyName, variant.modJsonName, variant.friendlyName)); + return null; + } + + log.info(String.format("Downloading %s metadata...", variant.friendlyName)); + URL metaUrl = HttpRequest.url( + String.format(variant.metaUrl, manifest.getGameVersion(), loaderMod.getVersion())); + VersionManifest fabricManifest = HttpRequest.get(metaUrl) + .execute() + .expectResponseCode(200) + .returnContent() + .asJson(VersionManifest.class); + + for (Library library : fabricManifest.getLibraries()) { + // To quote a famous comment: "And here we come upon a sad state of affairs." + // Quilt's meta API returns broken data about how to launch the game. It specifies its own incomplete + // and ultimately broken set of intermediary mappings called "hashed". If the loader finds "hashed" in + // the classpath it tries to use it and blows up because it doesn't work. + // We work around this here by just throwing the hashed library out - they do now at least specify + // fabric's intermediary mappings in the library list, which DO work. + // Historical note: previously they didn't do this! Every launcher that added Quilt support had to add + // a hack that replaced hashed with intermediary! This is a lot of technical debt that is gonna come + // back to bite them in the ass later, because it's all still there! + // TODO pester Quilt again about fixing this.... + if (library.getName().startsWith("org.quiltmc:hashed") && loaderMod instanceof QuiltMod) { + continue; + } + + result.getLoaderLibraries().add(library); + log.info("Adding loader library " + library.getName()); + } + + String mainClass = fabricManifest.getMainClass(); + if (mainClass != null) { + manifest.getVersionManifest().setMainClass(mainClass); + log.info("Using main class " + mainClass); + } + } catch (InterruptedException e) { + log.warning(String.format("HTTP request to %s metadata API was interrupted!", variant.friendlyName)); + throw new IOException(e); + } finally { + closer.close(); + jarFile.close(); + } + + return result; + } + + @RequiredArgsConstructor + public enum Variant { + FABRIC("Fabric", "fabric.mod.json", "https://meta.fabricmc.net/v2/versions/loader/%s/%s/profile/json", FabricMod.class), + QUILT("Quilt", "quilt.mod.json", "https://meta.quiltmc.org/v3/versions/loader/%s/%s/profile/json", QuiltMod.class); + + private final String friendlyName; + private final String modJsonName; + private final String metaUrl; + private final Class mappedClass; + } +} diff --git a/launcher-builder/src/main/java/com/skcraft/launcher/builder/loaders/ILoaderProcessor.java b/launcher-builder/src/main/java/com/skcraft/launcher/builder/loaders/ILoaderProcessor.java new file mode 100644 index 0000000..3006b86 --- /dev/null +++ b/launcher-builder/src/main/java/com/skcraft/launcher/builder/loaders/ILoaderProcessor.java @@ -0,0 +1,11 @@ +package com.skcraft.launcher.builder.loaders; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.skcraft.launcher.model.modpack.Manifest; + +import java.io.File; +import java.io.IOException; + +public interface ILoaderProcessor { + LoaderResult process(File loaderJar, Manifest manifest, ObjectMapper mapper, File baseDir) throws IOException; +} diff --git a/launcher-builder/src/main/java/com/skcraft/launcher/builder/loaders/LoaderResult.java b/launcher-builder/src/main/java/com/skcraft/launcher/builder/loaders/LoaderResult.java new file mode 100644 index 0000000..9fe2783 --- /dev/null +++ b/launcher-builder/src/main/java/com/skcraft/launcher/builder/loaders/LoaderResult.java @@ -0,0 +1,15 @@ +package com.skcraft.launcher.builder.loaders; + +import com.google.common.collect.Lists; +import com.skcraft.launcher.model.minecraft.Library; +import lombok.Data; + +import java.net.URL; +import java.util.List; + +@Data +public class LoaderResult { + private final List loaderLibraries = Lists.newArrayList(); + private final List processorLibraries = Lists.newArrayList(); + private final List jarMavens = Lists.newArrayList(); +} 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 new file mode 100644 index 0000000..17c8c36 --- /dev/null +++ b/launcher-builder/src/main/java/com/skcraft/launcher/builder/loaders/ModernForgeLoaderProcessor.java @@ -0,0 +1,191 @@ +package com.skcraft.launcher.builder.loaders; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.Lists; +import com.google.common.io.CharStreams; +import com.google.common.io.Closer; +import com.skcraft.launcher.builder.BuilderUtils; +import com.skcraft.launcher.model.loader.LoaderManifest; +import com.skcraft.launcher.model.loader.SidedData; +import com.skcraft.launcher.model.loader.VersionInfo; +import com.skcraft.launcher.model.loader.profiles.ModernForgeInstallProfile; +import com.skcraft.launcher.model.minecraft.GameArgument; +import com.skcraft.launcher.model.minecraft.Library; +import com.skcraft.launcher.model.minecraft.Side; +import com.skcraft.launcher.model.minecraft.VersionManifest; +import com.skcraft.launcher.model.modpack.DownloadableFile; +import com.skcraft.launcher.model.modpack.Manifest; +import com.skcraft.launcher.util.FileUtils; +import lombok.extern.java.Log; + +import java.io.*; +import java.net.URL; +import java.util.List; +import java.util.jar.JarFile; +import java.util.zip.ZipEntry; + +@Log +public class ModernForgeLoaderProcessor implements ILoaderProcessor { + @Override + public LoaderResult process(File loaderJar, Manifest manifest, ObjectMapper mapper, File baseDir) throws IOException { + JarFile jarFile = new JarFile(loaderJar); + Closer closer = Closer.create(); + LoaderResult result = new LoaderResult(); + + try { + ZipEntry versionEntry = BuilderUtils.getZipEntry(jarFile, "version.json"); + String loaderName = jarFile.getName(); + + if (versionEntry != null) { + InputStream stream = jarFile.getInputStream(versionEntry); + + VersionInfo info = mapper.readValue( + BuilderUtils.readStringFromStream(closer.register(new InputStreamReader(stream))), + VersionInfo.class); + VersionManifest version = manifest.getVersionManifest(); + + if (info.getId() != null) { + loaderName = info.getId(); + } + + // Copy game arguments + List gameArguments = info.getArguments().getGameArguments(); + if (gameArguments != null) { + if (info.isOverridingArguments()) { + version.getArguments().getGameArguments().clear(); + } + + 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) { + for (Library library : libraries) { + result.getLoaderLibraries().add(library); + log.info("Adding loader library " + library.getName()); + } + } + + // Copy logging config + SidedData loggingConfig = info.getLogging(); + if (loggingConfig != null) { + version.setLogging(loggingConfig); + } + + // Copy main class + String mainClass = info.getMainClass(); + if (mainClass != null) { + version.setMainClass(mainClass); + log.info("Using " + mainClass + " as the main class"); + } + } else { + log.warning("The loader " + loaderJar.getAbsolutePath() + " does not appear to have a " + + "version.json file inside -- is it actually an installer for Forge?"); + } + + ZipEntry profileEntry = BuilderUtils.getZipEntry(jarFile, "install_profile.json"); + if (profileEntry != null) { + InputStream stream = jarFile.getInputStream(profileEntry); + String data = CharStreams.toString(closer.register(new InputStreamReader(stream))); + data = data.replace(",\\s*\\}", "}"); + + ModernForgeInstallProfile profile = mapper.readValue(data, ModernForgeInstallProfile.class); + + if (!profile.getMinecraft().equals(manifest.getGameVersion())) { + // TODO: Ideally this would show up as a Problem when running the check command. + // Getting the data from here to there is quite difficult, however. + log.warning(String.format("The Forge installer inside loaders/ is for Minecraft version %s; your " + + "manifest is set to %s.", profile.getMinecraft(), manifest.getGameVersion())); + } + + // Import the libraries for the installer + result.getProcessorLibraries().addAll(profile.getLibraries()); + + // 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)), + objectsDir); + + entry.setName("client.lzma"); + entry.setSide(Side.CLIENT); + extraFiles.add(entry); + profile.getData().get("BINPATCH").setClient("&" + entry.getName() + "&"); + } + + ZipEntry serverBinpatch = BuilderUtils.getZipEntry(jarFile, "data/server.lzma"); + if (serverBinpatch != null) { + DownloadableFile entry = FileUtils.saveStreamToObjectsDir( + closer.register(jarFile.getInputStream(serverBinpatch)), + objectsDir); + + entry.setName("server.lzma"); + entry.setSide(Side.SERVER); + extraFiles.add(entry); + 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")); + + // Add loader manifest to the map + manifest.getLoaders().put(loaderName, new LoaderManifest(profile.getLibraries(), profile.getData(), extraFiles)); + + // Find name of final patched library and mark it as excluded from download + // TODO: we should generalize this to all process outputs, really + SidedData finalJars = profile.getData().get("PATCHED"); + if (finalJars != null) { + String libraryName = finalJars.getClient(); + libraryName = libraryName.substring(1, libraryName.length() - 1); + + for (Library lib : result.getLoaderLibraries()) { + if (lib.matches(libraryName)) { + lib.setGenerated(true); + log.info(String.format("Setting generated flag on library '%s'", lib.getName())); + break; + } + } + } + + // Add processors + manifest.getTasks().addAll(profile.toProcessorEntries(loaderName)); + } + + ZipEntry mavenEntry = BuilderUtils.getZipEntry(jarFile, "maven/"); + if (mavenEntry != null) { + URL jarUrl = new URL("jar:file:" + loaderJar.getAbsolutePath() + "!/"); + result.getJarMavens().add(new URL(jarUrl, "/maven/")); + } + } finally { + closer.close(); + jarFile.close(); + } + + return result; + } +} 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 new file mode 100644 index 0000000..c5e970d --- /dev/null +++ b/launcher-builder/src/main/java/com/skcraft/launcher/builder/loaders/OldForgeLoaderProcessor.java @@ -0,0 +1,112 @@ +package com.skcraft.launcher.builder.loaders; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.io.ByteStreams; +import com.google.common.io.Closer; +import com.google.common.io.Files; +import com.skcraft.launcher.builder.BuilderUtils; +import com.skcraft.launcher.model.loader.profiles.LegacyInstallProfile; +import com.skcraft.launcher.model.minecraft.GameArgument; +import com.skcraft.launcher.model.minecraft.Library; +import com.skcraft.launcher.model.minecraft.MinecraftArguments; +import com.skcraft.launcher.model.minecraft.VersionManifest; +import com.skcraft.launcher.model.modpack.Manifest; +import lombok.extern.java.Log; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.Iterator; +import java.util.List; +import java.util.jar.JarFile; +import java.util.zip.ZipEntry; + +@Log +public class OldForgeLoaderProcessor implements ILoaderProcessor { + @Override + public LoaderResult process(File loaderJar, Manifest manifest, ObjectMapper mapper, File baseDir) throws IOException { + JarFile jarFile = new JarFile(loaderJar); + LoaderResult result = new LoaderResult(); + Closer closer = Closer.create(); + + try { + ZipEntry profileEntry = BuilderUtils.getZipEntry(jarFile, "install_profile.json"); + + if (profileEntry != null) { + InputStream stream = jarFile.getInputStream(profileEntry); + + // Read file + String data = BuilderUtils.readStringFromStream(closer.register(new InputStreamReader(stream))); + LegacyInstallProfile profile = mapper.readValue(data, LegacyInstallProfile.class); + VersionManifest version = manifest.getVersionManifest(); + + // Copy tweak class arguments + MinecraftArguments args = profile.getVersionInfo().getArguments(); + if (args != null) { + Iterator iter = args.getGameArguments().iterator(); + while (iter.hasNext()) { + GameArgument cur = iter.next(); + if (cur.getValues().contains("--tweakClass")) { + String tweakClass = cur.getValues().size() > 1 + ? cur.getValues().get(1) + : iter.next().getJoinedValue(); + + List gameArgs = manifest.getVersionManifest().getArguments().getGameArguments(); + gameArgs.add(new GameArgument("--tweakClass")); + gameArgs.add(new GameArgument(tweakClass)); + + log.info(String.format("Adding tweak class '%s' to arguments", tweakClass)); + } + } + } + + // Add libraries + List libraries = profile.getVersionInfo().getLibraries(); + if (libraries != null) { + for (Library library : libraries) { + if (!version.getLibraries().contains(library)) { + result.getLoaderLibraries().add(library); + } + } + } + + // Copy main class + String mainClass = profile.getVersionInfo().getMainClass(); + if (mainClass != null) { + version.setMainClass(mainClass); + log.info("Using " + mainClass + " as the main class"); + } + + // Extract the library + String filePath = profile.getInstallData().getFilePath(); + String libraryPath = profile.getInstallData().getPath(); + + if (filePath != null && libraryPath != null) { + ZipEntry libraryEntry = BuilderUtils.getZipEntry(jarFile, filePath); + + if (libraryEntry != null) { + File librariesDir = new File(baseDir, "libraries"); + File extractPath = new File(librariesDir, Library.mavenNameToPath(libraryPath)); + + Files.createParentDirs(extractPath); + ByteStreams.copy(closer.register(jarFile.getInputStream(libraryEntry)), + Files.newOutputStreamSupplier(extractPath)); + } else { + log.warning("Could not find the file '" + filePath + "' in " + + loaderJar.getAbsolutePath() + + ", which means that this mod loader will not work correctly"); + } + } + } else { + log.warning("The file at " + loaderJar.getAbsolutePath() + " did not appear to have an " + + "install_profile.json file inside -- is it actually an installer for a mod loader?"); + } + } finally { + closer.close(); + jarFile.close(); + } + + return result; + } +} diff --git a/launcher-fancy/build.gradle b/launcher-fancy/build.gradle new file mode 100644 index 0000000..22af705 --- /dev/null +++ b/launcher-fancy/build.gradle @@ -0,0 +1,36 @@ +plugins { + id 'application' + id "com.github.johnrengelman.shadow" + id 'io.freefair.lombok' +} + +application { + mainClassName = "com.skcraft.launcher.FancyLauncher" +} + +repositories { + maven { + name = 'bbkr.space maven' + url = 'https://server.bbkr.space/artifactory/libs-snapshot' + } +} + +dependencies { + implementation project(path: ':launcher') + implementation 'io.github.cottonmc.insubstantial:substance:7.3.1-SNAPSHOT' +} + +shadowJar { + dependsOn ':launcher:shadowJar' + archiveClassifier.set("") +} + +// Work around gradle shadow bug +// see https://github.com/johnrengelman/shadow/issues/713 +startScripts.dependsOn(':launcher:shadowJar') +distTar.dependsOn(':launcher:shadowJar') +distZip.dependsOn(':launcher:shadowJar') + +build { + dependsOn(shadowJar) +} diff --git a/launcher-fancy/lombok.config b/launcher-fancy/lombok.config new file mode 100644 index 0000000..6aa51d7 --- /dev/null +++ b/launcher-fancy/lombok.config @@ -0,0 +1,2 @@ +# This file is generated by the 'io.freefair.lombok' Gradle plugin +config.stopBubbling = true diff --git a/launcher-fancy/src/main/java/com/skcraft/launcher/FancyBackgroundPanel.java b/launcher-fancy/src/main/java/com/skcraft/launcher/FancyBackgroundPanel.java new file mode 100644 index 0000000..73f747f --- /dev/null +++ b/launcher-fancy/src/main/java/com/skcraft/launcher/FancyBackgroundPanel.java @@ -0,0 +1,61 @@ +/* + * SKCraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher; + +import javax.imageio.ImageIO; +import javax.swing.*; +import java.awt.*; +import java.io.IOException; + +public class FancyBackgroundPanel extends JPanel { + + private Image background; + + public FancyBackgroundPanel() { + try { + background = ImageIO.read(FancyBackgroundPanel.class.getResourceAsStream("launcher_bg.jpg")); + } catch (IOException e) { + background = null; + } + } + + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + if (background != null) { + double multi; + int w, h; + + // Calculate Aspect Ratio Multiplier depending on window size + if (this.getHeight() <= this.getWidth()) { + multi = this.getWidth() / (float)background.getWidth(null); + } + else { + multi = this.getHeight() / (float)background.getHeight(null); + } + + // Calculate new width and height + w = (int) Math.floor((float)background.getWidth(null) * multi); + h = (int) Math.floor((float)background.getHeight(null) * multi); + + // Check if it needs to be switched (eg. in case of a square window) + if (h < this.getHeight() || w < this.getWidth()) { + if (h < this.getHeight()) { + multi = this.getHeight() / (float)background.getHeight(null); + } + else if (w < this.getWidth()) { + multi = this.getWidth() / (float) background.getWidth(null); + } + + w = (int) Math.floor((float)background.getWidth(null) * multi); + h = (int) Math.floor((float)background.getHeight(null) * multi); + } + + g.drawImage(background, 0, 0, w, h,null); + } + } +} diff --git a/launcher-fancy/src/main/java/com/skcraft/launcher/FancyLauncher.java b/launcher-fancy/src/main/java/com/skcraft/launcher/FancyLauncher.java new file mode 100644 index 0000000..871a215 --- /dev/null +++ b/launcher-fancy/src/main/java/com/skcraft/launcher/FancyLauncher.java @@ -0,0 +1,65 @@ +/* + * SKCraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher; + +import com.google.common.base.Supplier; +import com.skcraft.launcher.swing.SwingHelper; +import lombok.extern.java.Log; + +import javax.swing.*; +import java.awt.*; +import java.util.logging.Level; + +@Log +public class FancyLauncher { + + public static void main(final String[] args) { + Launcher.setupLogger(); + + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + try { + Thread.currentThread().setContextClassLoader(FancyLauncher.class.getClassLoader()); + UIManager.getLookAndFeelDefaults().put("ClassLoader", FancyLauncher.class.getClassLoader()); + UIManager.getDefaults().put("SplitPane.border", BorderFactory.createEmptyBorder()); + JFrame.setDefaultLookAndFeelDecorated(true); + JDialog.setDefaultLookAndFeelDecorated(true); + System.setProperty("sun.awt.noerasebackground", "true"); + System.setProperty("substancelaf.windowRoundedCorners", "false"); + + if (!SwingHelper.setLookAndFeel("com.skcraft.launcher.skin.LauncherLookAndFeel")) { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } + + Launcher launcher = Launcher.createFromArguments(args); + launcher.setMainWindowSupplier(new CustomWindowSupplier(launcher)); + launcher.showLauncherWindow(); + } catch (Throwable t) { + log.log(Level.WARNING, "Load failure", t); + SwingHelper.showErrorDialog(null, "Uh oh! The updater couldn't be opened because a " + + "problem was encountered.", "Launcher error", t); + } + } + }); + } + + private static class CustomWindowSupplier implements Supplier { + + private final Launcher launcher; + + private CustomWindowSupplier(Launcher launcher) { + this.launcher = launcher; + } + + @Override + public Window get() { + return new FancyLauncherFrame(launcher); + } + } + +} diff --git a/launcher-fancy/src/main/java/com/skcraft/launcher/FancyLauncherFrame.java b/launcher-fancy/src/main/java/com/skcraft/launcher/FancyLauncherFrame.java new file mode 100644 index 0000000..0d413dd --- /dev/null +++ b/launcher-fancy/src/main/java/com/skcraft/launcher/FancyLauncherFrame.java @@ -0,0 +1,46 @@ +/* + * SKCraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher; + +import com.skcraft.launcher.dialog.LauncherFrame; +import com.skcraft.launcher.swing.SwingHelper; +import com.skcraft.launcher.swing.WebpagePanel; +import lombok.NonNull; + +import javax.swing.*; + +public class FancyLauncherFrame extends LauncherFrame { + + /** + * Create a new frame. + * + * @param launcher the launcher + */ + public FancyLauncherFrame(@NonNull Launcher launcher) { + super(launcher); + + setSize(800, 500); + setLocationRelativeTo(null); + + SwingHelper.removeOpaqueness(getInstancesTable()); + SwingHelper.removeOpaqueness(getInstanceScroll()); + getInstanceScroll().setBorder(BorderFactory.createEmptyBorder()); + } + + @Override + protected JPanel createContainerPanel() { + return new FancyBackgroundPanel(); + } + + @Override + protected WebpagePanel createNewsPanel() { + WebpagePanel panel = super.createNewsPanel(); + panel.setBrowserBorder(BorderFactory.createEmptyBorder()); + return panel; + } + +} diff --git a/launcher-fancy/src/main/java/com/skcraft/launcher/skin/LauncherButtonShaper.java b/launcher-fancy/src/main/java/com/skcraft/launcher/skin/LauncherButtonShaper.java new file mode 100644 index 0000000..2ed037e --- /dev/null +++ b/launcher-fancy/src/main/java/com/skcraft/launcher/skin/LauncherButtonShaper.java @@ -0,0 +1,21 @@ +/* + * SKCraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.skin; + +import org.pushingpixels.substance.api.shaper.ClassicButtonShaper; + +import javax.swing.*; +import java.awt.*; + +public class LauncherButtonShaper extends ClassicButtonShaper { + + public Dimension getPreferredSize(AbstractButton button, Dimension uiPreferredSize) { + Dimension size = super.getPreferredSize(button, uiPreferredSize); + return new Dimension(size.width + 5, size.height + 4); + } + +} diff --git a/launcher-fancy/src/main/java/com/skcraft/launcher/skin/LauncherLookAndFeel.java b/launcher-fancy/src/main/java/com/skcraft/launcher/skin/LauncherLookAndFeel.java new file mode 100644 index 0000000..80d4725 --- /dev/null +++ b/launcher-fancy/src/main/java/com/skcraft/launcher/skin/LauncherLookAndFeel.java @@ -0,0 +1,16 @@ +/* + * SKCraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.skin; + +import org.pushingpixels.substance.api.SubstanceLookAndFeel; + +public class LauncherLookAndFeel extends SubstanceLookAndFeel { + + public LauncherLookAndFeel() { + super(new LauncherSkin()); + } +} diff --git a/launcher-fancy/src/main/java/com/skcraft/launcher/skin/LauncherSkin.java b/launcher-fancy/src/main/java/com/skcraft/launcher/skin/LauncherSkin.java new file mode 100644 index 0000000..15470d3 --- /dev/null +++ b/launcher-fancy/src/main/java/com/skcraft/launcher/skin/LauncherSkin.java @@ -0,0 +1,94 @@ +/* + * SKCraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.skin; + +import org.pushingpixels.substance.api.*; +import org.pushingpixels.substance.api.painter.border.ClassicBorderPainter; +import org.pushingpixels.substance.api.painter.border.CompositeBorderPainter; +import org.pushingpixels.substance.api.painter.border.DelegateBorderPainter; +import org.pushingpixels.substance.api.painter.decoration.FlatDecorationPainter; +import org.pushingpixels.substance.api.painter.fill.FractionBasedFillPainter; +import org.pushingpixels.substance.api.painter.highlight.ClassicHighlightPainter; +import org.pushingpixels.substance.api.skin.GraphiteSkin; + +public class LauncherSkin extends GraphiteSkin { + + public LauncherSkin() { + ColorSchemes schemes = SubstanceSkin.getColorSchemes("com/skcraft/launcher/skin/graphite.colorschemes"); + + SubstanceColorScheme activeScheme = schemes.get("Graphite Active"); + SubstanceColorScheme selectedDisabledScheme = schemes.get("Graphite Selected Disabled"); + SubstanceColorScheme selectedScheme = schemes.get("Graphite Selected"); + SubstanceColorScheme disabledScheme = schemes.get("Graphite Disabled"); + SubstanceColorScheme enabledScheme = schemes.get("Graphite Enabled"); + SubstanceColorScheme backgroundScheme = schemes.get("Graphite Background"); + SubstanceColorScheme highlightScheme = schemes.get("Graphite Highlight"); + SubstanceColorScheme borderScheme = schemes.get("Graphite Border"); + SubstanceColorScheme separatorScheme = schemes.get("Graphite Separator"); + SubstanceColorScheme textHighlightScheme = schemes.get("Graphite Text Highlight"); + SubstanceColorScheme highlightMarkScheme = schemes.get("Graphite Highlight Mark"); + SubstanceColorScheme tabHighlightScheme = schemes.get("Graphite Tab Highlight"); + + SubstanceColorSchemeBundle scheme = new SubstanceColorSchemeBundle(activeScheme, enabledScheme, disabledScheme); + + // highlight fill scheme + custom alpha for rollover unselected state + scheme.registerHighlightColorScheme(highlightScheme, 0.6f, ComponentState.ROLLOVER_UNSELECTED); + scheme.registerHighlightColorScheme(highlightScheme, 0.8f, ComponentState.SELECTED); + scheme.registerHighlightColorScheme(highlightScheme, 1.0f, ComponentState.ROLLOVER_SELECTED); + scheme.registerHighlightColorScheme(highlightScheme, 0.75f, ComponentState.ARMED, ComponentState.ROLLOVER_ARMED); + + // highlight border scheme + scheme.registerColorScheme(highlightScheme, ColorSchemeAssociationKind.HIGHLIGHT_BORDER, ComponentState.getActiveStates()); + scheme.registerColorScheme(borderScheme, ColorSchemeAssociationKind.BORDER); + scheme.registerColorScheme(separatorScheme, ColorSchemeAssociationKind.SEPARATOR); + + // text highlight scheme + scheme.registerColorScheme(textHighlightScheme, ColorSchemeAssociationKind.TEXT_HIGHLIGHT, ComponentState.SELECTED, ComponentState.ROLLOVER_SELECTED); + scheme.registerColorScheme(highlightScheme, ComponentState.ARMED, ComponentState.ROLLOVER_ARMED); + scheme.registerColorScheme(highlightMarkScheme, ColorSchemeAssociationKind.HIGHLIGHT_MARK, ComponentState.getActiveStates()); + scheme.registerColorScheme(highlightMarkScheme, ColorSchemeAssociationKind.MARK, ComponentState.ROLLOVER_SELECTED, ComponentState.ROLLOVER_UNSELECTED); + scheme.registerColorScheme(borderScheme, ColorSchemeAssociationKind.MARK, ComponentState.SELECTED); + scheme.registerColorScheme(disabledScheme, 0.5f, ComponentState.DISABLED_UNSELECTED); + scheme.registerColorScheme(selectedDisabledScheme, 0.65f, ComponentState.DISABLED_SELECTED); + scheme.registerColorScheme(highlightScheme, ComponentState.ROLLOVER_SELECTED); + scheme.registerColorScheme(selectedScheme, ComponentState.SELECTED); + + scheme.registerColorScheme(tabHighlightScheme, ColorSchemeAssociationKind.TAB, ComponentState.ROLLOVER_SELECTED); + + this.registerDecorationAreaSchemeBundle(scheme, backgroundScheme, DecorationAreaType.NONE); + + this.setSelectedTabFadeStart(0.1); + this.setSelectedTabFadeEnd(0.3); + + this.buttonShaper = new LauncherButtonShaper(); + this.watermark = null; + this.fillPainter = new FractionBasedFillPainter("Graphite", + new float[] { 0.0f, 0.5f, 1.0f }, + new ColorSchemeSingleColorQuery[] { + ColorSchemeSingleColorQuery.ULTRALIGHT, + ColorSchemeSingleColorQuery.LIGHT, + ColorSchemeSingleColorQuery.LIGHT }); + this.decorationPainter = new FlatDecorationPainter(); + this.highlightPainter = new ClassicHighlightPainter(); + this.borderPainter = new CompositeBorderPainter("Graphite", + new ClassicBorderPainter(), new DelegateBorderPainter( + "Graphite Inner", new ClassicBorderPainter(), + 0xA0FFFFFF, 0x60FFFFFF, 0x60FFFFFF, + new ColorSchemeTransform() { + @Override + public SubstanceColorScheme transform( + SubstanceColorScheme scheme) { + return scheme.tint(0.25f); + } + })); + + this.highlightBorderPainter = new ClassicBorderPainter(); + + this.watermarkScheme = schemes.get("Graphite Watermark"); + } + +} diff --git a/launcher-fancy/src/main/resources/com/skcraft/launcher/launcher_bg.jpg b/launcher-fancy/src/main/resources/com/skcraft/launcher/launcher_bg.jpg new file mode 100644 index 0000000..25cf947 Binary files /dev/null and b/launcher-fancy/src/main/resources/com/skcraft/launcher/launcher_bg.jpg differ diff --git a/launcher-fancy/src/main/resources/com/skcraft/launcher/skin/graphite.colorschemes b/launcher-fancy/src/main/resources/com/skcraft/launcher/skin/graphite.colorschemes new file mode 100644 index 0000000..2c05502 --- /dev/null +++ b/launcher-fancy/src/main/resources/com/skcraft/launcher/skin/graphite.colorschemes @@ -0,0 +1,153 @@ +Graphite Enabled { + kind=Dark + colorUltraLight=#4A4A4A + colorExtraLight=#464646 + colorLight=#424242 + colorMid=#3F3F3F + colorDark=#353535 + colorUltraDark=#313131 + colorForeground=#B4B4B4 +} + +Graphite Border { + kind=Dark + colorUltraLight=#616161 + colorExtraLight=#5A5A5A + colorLight=#4F4F4F + colorMid=#414141 + colorDark=#323232 + colorUltraDark=#2B2B2B + colorForeground=#EDEDED +} + +Graphite Background { + kind=Dark + colorUltraLight=#555555 + colorExtraLight=#444444 + colorLight=#333333 + colorMid=#222222 + colorDark=#111111 + colorUltraDark=#000000 + colorForeground=#B4B4B4 +} + +Graphite Watermark { + kind=Dark + colorUltraLight=#555555 + colorExtraLight=#444444 + colorLight=#333333 + colorMid=#222222 + colorDark=#111111 + colorUltraDark=#000000 + colorForeground=#B4B4B4 +} + +Graphite Active { + kind=Dark + colorUltraLight=#777777 + colorExtraLight=#6F6F6F + colorLight=#636363 + colorMid=#535353 + colorDark=#434343 + colorUltraDark=#3B3B3B + colorForeground=#C8C8C8 +} + +Graphite Selected { + kind=Dark + colorUltraLight=#727272 + colorExtraLight=#696969 + colorLight=#606060 + colorMid=#505050 + colorDark=#404040 + colorUltraDark=#383838 + colorForeground=#C8C8C8 +} + +Graphite Selected Disabled { + kind=Dark + colorUltraLight=#777777 + colorExtraLight=#6F6F6F + colorLight=#636363 + colorMid=#535353 + colorDark=#434343 + colorUltraDark=#3B3B3B + colorForeground=#202020 +} + +Graphite Disabled { + kind=Dark + colorUltraLight=#646464 + colorExtraLight=#5E5E5E + colorLight=#545454 + colorMid=#474747 + colorDark=#3A3A3A + colorUltraDark=#333333 + colorForeground=#CCCCCC +} + +Graphite Highlight { + kind=Dark + colorUltraLight=#373737 + colorExtraLight=#373737 + colorLight=#373737 + colorMid=#373737 + colorDark=#373737 + colorUltraDark=#373737 + colorForeground=#CCCCCC +} + +Graphite Tab Highlight { + kind=Light + colorUltraLight=#FBFCFF + colorExtraLight=#F2F6FB + colorLight=#CED7E0 + colorMid=#BCC0C5 + colorDark=#62666B + colorUltraDark=#363B3F + colorForeground=#EDEDED +} + +Graphite Highlight Mark { + kind=Dark + colorUltraLight=#616161 + colorExtraLight=#5A5A5A + colorLight=#4F4F4F + colorMid=#414141 + colorDark=#323232 + colorUltraDark=#2B2B2B + colorForeground=#2B2B2B +} + +Graphite Text Highlight { + kind=Light + colorUltraLight=#BDBEC0 + colorExtraLight=#B8BBBD + colorLight=#A4A9AE + colorMid=#9A9D9F + colorDark=#686B6D + colorUltraDark=#505355 + colorForeground=#2B2F33 +} + +Graphite Separator { + kind=Dark + colorUltraLight=#565656 + colorExtraLight=#525252 + colorLight=#4F4F4F + colorMid=#383838 + colorDark=#353535 + colorUltraDark=#333333 + colorForeground=#B4B4B4 +} + +Graphite Aqua { + kind=Light + colorUltraLight=#6383FF + colorExtraLight=#577EFF + colorLight=#4A6FFF + colorMid=#355FFB + colorDark=#1A3DE6 + colorUltraDark=#153BE1 + colorForeground=#FFFFFF +} \ No newline at end of file diff --git a/launcher/build.gradle b/launcher/build.gradle new file mode 100644 index 0000000..b41115a --- /dev/null +++ b/launcher/build.gradle @@ -0,0 +1,40 @@ +plugins { + id 'application' + id 'java-library' + id "com.github.johnrengelman.shadow" + id 'io.freefair.lombok' +} + +application { + mainClassName = "com.skcraft.launcher.Launcher" +} + +dependencies { + api 'javax.xml.bind:jaxb-api:2.3.1' + api 'com.fasterxml.jackson.core:jackson-databind:2.13.2.2' + api 'commons-lang:commons-lang:2.6' + api 'commons-io:commons-io:1.2' + api 'com.google.guava:guava:15.0' + api 'com.beust:jcommander:1.82' + api 'com.miglayout:miglayout:3.7.4' + api 'com.google.code.findbugs:jsr305:3.0.2' + + implementation 'com.googlecode.plist:dd-plist:1.23' + implementation 'net.java.dev.jna:jna-platform:5.11.0' +} + +processResources { + filesMatching('**/*.properties') { + filter { + it.replace('${project.version}', project.version) + } + } +} + +shadowJar { + archiveClassifier.set("") +} + +build { + dependsOn(shadowJar) +} diff --git a/launcher/lombok.config b/launcher/lombok.config new file mode 100644 index 0000000..6aa51d7 --- /dev/null +++ b/launcher/lombok.config @@ -0,0 +1,2 @@ +# This file is generated by the 'io.freefair.lombok' Gradle plugin +config.stopBubbling = true diff --git a/launcher/src/main/java/com/skcraft/concurrency/Callback.java b/launcher/src/main/java/com/skcraft/concurrency/Callback.java new file mode 100644 index 0000000..05f8bc0 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/concurrency/Callback.java @@ -0,0 +1,13 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.concurrency; + +public interface Callback { + + void handle(T value); + +} diff --git a/launcher/src/main/java/com/skcraft/concurrency/DefaultProgress.java b/launcher/src/main/java/com/skcraft/concurrency/DefaultProgress.java new file mode 100644 index 0000000..fd8a681 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/concurrency/DefaultProgress.java @@ -0,0 +1,28 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.concurrency; + +import lombok.Data; + +/** + * A simple default implementation of {@link com.skcraft.concurrency.ProgressObservable} + * with settable properties. + */ +@Data +public class DefaultProgress implements ProgressObservable { + + private String status; + private double progress = -1; + + public DefaultProgress() { + } + + public DefaultProgress(double progress, String status) { + this.progress = progress; + this.status = status; + } +} diff --git a/launcher/src/main/java/com/skcraft/concurrency/Deferred.java b/launcher/src/main/java/com/skcraft/concurrency/Deferred.java new file mode 100644 index 0000000..22c3900 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/concurrency/Deferred.java @@ -0,0 +1,142 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.concurrency; + +import com.google.common.base.Function; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; + +import java.util.concurrent.Callable; + +/** + * An extension of {@link ListenableFuture} that provides convenience methods + * to register functions that are triggered after upon the completion of + * the underlying task. + * + *

Dependent functions are executed using the "default" executor which + * is specified when {@code Deferred} is first created, unless + * the async variants are used to register the function.

+ * + * @param The type returned + */ +public interface Deferred extends ListenableFuture { + + /** + * Returns a new Deferred that represents the asynchronous computation + * of the given action, which is only executed upon the normal completion + * of this Deferred in the default executor. + * + * @param task The task + * @param The return type of the task + * @return The new Deferred + */ + Deferred thenRun(Callable task); + + /** + * Returns a new Deferred that represents the asynchronous computation + * of the given action, which is only executed upon the normal completion + * of this Deferred in the provided executor. + * + * @param task The task + * @param The return type of the task + * @return The new Deferred + */ + Deferred thenRunAsync(Callable task, ListeningExecutorService executor); + + /** + * Returns a new Deferred that represents the asynchronous execution + * of the given action, which is only executed upon the normal completion + * of this Deferred in the provided executor. + * + * @param task The task + * @return The new Deferred + */ + Deferred thenRun(Runnable task); + + /** + * Returns a new Deferred that represents the asynchronous execution + * of the given action, which is only executed upon the normal completion + * of this Deferred in the default executor. + * + * @param task The task + * @return The new Deferred + */ + Deferred thenRunAsync(Runnable task, ListeningExecutorService executor); + + /** + * Returns a new Deferred that represents the asynchronous execution + * of the given action as a side effect that does not change the value + * passed between the prior Deferred to any dependent Deferred instances. + * + *

The given action is only executed in the default executor upon the + * normal completion of this Deferred.

+ * + * @param task The task + * @return The new Deferred + */ + Deferred thenTap(Runnable task); + + /** + * Returns a new Deferred that represents the asynchronous execution + * of the given action as a side effect that does not change the value + * passed between the prior Deferred to any dependent Deferred instances. + * + *

The given action is only executed in the provided executor upon the + * normal completion of this Deferred.

+ * + * @param task The task + * @return The new Deferred + */ + Deferred thenTapAsync(Runnable task, ListeningExecutorService executor); + + /** + * Returns a new Deferred that represents the asynchronous execution + * of the given function, which transforms the value of the previous + * Deferred into a new value. + * + *

The given action is only executed in the default executor upon the + * normal completion of this Deferred.

+ * + * @param function The function + * @return The new Deferred + */ + Deferred thenApply(Function function); + + /** + * Returns a new Deferred that represents the asynchronous execution + * of the given function, which transforms the value of the previous + * Deferred into a new value. + * + *

The given action is only executed in the provided executor upon the + * normal completion of this Deferred.

+ * + * @param function The function + * @return The new Deferred + */ + Deferred thenApplyAsync(Function function, ListeningExecutorService executor); + + /** + * Adds callbacks that are executed asynchronously on success or failure + * using the default executor. + * + * @param onSuccess The success callback + * @param onFailure The failure callback + * @return The Deferred + */ + Deferred handle(Callback onSuccess, Callback onFailure); + + /** + * Adds callbacks that are executed asynchronously on success or failure. + * + * @param onSuccess The success callback + * @param onFailure The failure callback + * @param executor The executor + * @return The Deferred + */ + Deferred handleAsync(Callback onSuccess, Callback onFailure, ListeningExecutorService executor); + +} diff --git a/launcher/src/main/java/com/skcraft/concurrency/DeferredImpl.java b/launcher/src/main/java/com/skcraft/concurrency/DeferredImpl.java new file mode 100644 index 0000000..1a2b93a --- /dev/null +++ b/launcher/src/main/java/com/skcraft/concurrency/DeferredImpl.java @@ -0,0 +1,140 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.concurrency; + +import com.google.common.base.Function; +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 java.util.concurrent.*; + +class DeferredImpl implements Deferred { + + private final ListenableFuture future; + private final ListeningExecutorService defaultExecutor; + + DeferredImpl(ListenableFuture future, ListeningExecutorService defaultExecutor) { + this.future = future; + this.defaultExecutor = defaultExecutor; + } + + @Override + public Deferred thenRun(Callable task) { + return thenRunAsync(task, defaultExecutor); + } + + @Override + public Deferred thenRunAsync(final Callable task, ListeningExecutorService executor) { + return new DeferredImpl(Futures.transform(future, new Function() { + @Override + public O apply(I input) { + try { + return task.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + }, executor), defaultExecutor); + } + + @Override + public Deferred thenRun(Runnable task) { + return thenRunAsync(task, defaultExecutor); + } + + @Override + public Deferred thenRunAsync(final Runnable task, ListeningExecutorService executor) { + return new DeferredImpl(Futures.transform(future, new Function() { + @Override + public Void apply(I input) { + task.run(); + return null; + } + }), defaultExecutor); + } + + @Override + public Deferred thenTap(Runnable task) { + return thenTapAsync(task, defaultExecutor); + } + + @Override + public Deferred thenTapAsync(final Runnable task, ListeningExecutorService executor) { + return thenApplyAsync(new Function() { + @Override + public I apply(I input) { + task.run(); + return input; + } + }, executor); + } + + @Override + public Deferred thenApply(Function function) { + return thenApplyAsync(function, defaultExecutor); + } + + @Override + public Deferred thenApplyAsync(Function function, ListeningExecutorService executor) { + return new DeferredImpl(Futures.transform(future, function, executor), defaultExecutor); + } + + @Override + public Deferred handle(Callback onSuccess, Callback onFailure) { + return handleAsync(onSuccess, onFailure, defaultExecutor); + } + + @Override + public Deferred handleAsync(final Callback onSuccess, final Callback onFailure, ListeningExecutorService executor) { + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(I result) { + onSuccess.handle(result); + } + + @Override + public void onFailure(Throwable t) { + onFailure.handle(t); + } + }, executor); + + return this; + } + + @Override + public void addListener(Runnable listener, Executor executor) { + future.addListener(listener, executor); + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return future.cancel(mayInterruptIfRunning); + } + + @Override + public boolean isCancelled() { + return future.isCancelled(); + } + + @Override + public boolean isDone() { + return future.isDone(); + } + + @Override + public I get() throws InterruptedException, ExecutionException { + return future.get(); + } + + @Override + public I get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + return future.get(timeout, unit); + } + +} diff --git a/launcher/src/main/java/com/skcraft/concurrency/Deferreds.java b/launcher/src/main/java/com/skcraft/concurrency/Deferreds.java new file mode 100644 index 0000000..3144b37 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/concurrency/Deferreds.java @@ -0,0 +1,45 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.concurrency; + +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; + +/** + * Utility class for working with Deferred. + */ +public final class Deferreds { + + private Deferreds() { + } + + /** + * Make a new Deferred from the given future, using the same thread + * executor as the default executor. + * + * @param future The future + * @param The type returned by the future + * @return A new Deferred + */ + public static Deferred makeDeferred(ListenableFuture future) { + return makeDeferred(future, MoreExecutors.sameThreadExecutor()); + } + + /** + * Make a new Deferred from the given future. + * + * @param future The future + * @param executor The default executor + * @param The type returned by the future + * @return A new Deferred + */ + public static Deferred makeDeferred(ListenableFuture future, ListeningExecutorService executor) { + return new DeferredImpl(future, executor); + } + +} diff --git a/launcher/src/main/java/com/skcraft/concurrency/ObservableFuture.java b/launcher/src/main/java/com/skcraft/concurrency/ObservableFuture.java new file mode 100644 index 0000000..1458d17 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/concurrency/ObservableFuture.java @@ -0,0 +1,83 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.concurrency; + +import com.google.common.util.concurrent.ListenableFuture; +import lombok.NonNull; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * A pair of ProgressObservable and ListenableFuture. + * + * @param the result type + */ +public class ObservableFuture implements ListenableFuture, ProgressObservable { + + private final ListenableFuture future; + private final ProgressObservable observable; + + /** + * Construct a new ObservableFuture. + * + * @param future the delegate future + * @param observable the observable + */ + public ObservableFuture(@NonNull ListenableFuture future, @NonNull ProgressObservable observable) { + this.future = future; + this.observable = observable; + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return future.cancel(mayInterruptIfRunning); + } + + @Override + public boolean isCancelled() { + return future.isCancelled(); + } + + @Override + public void addListener(Runnable listener, Executor executor) { + future.addListener(listener, executor); + } + + @Override + public boolean isDone() { + return future.isDone(); + } + + @Override + public V get() throws InterruptedException, ExecutionException { + return future.get(); + } + + @Override + public V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + return future.get(timeout, unit); + } + + @Override + public String toString() { + return observable.toString(); + } + + @Override + public double getProgress() { + return observable.getProgress(); + } + + @Override + public String getStatus() { + return observable.getStatus(); + } + +} diff --git a/launcher/src/main/java/com/skcraft/concurrency/ProgressFilter.java b/launcher/src/main/java/com/skcraft/concurrency/ProgressFilter.java new file mode 100644 index 0000000..d9cf69e --- /dev/null +++ b/launcher/src/main/java/com/skcraft/concurrency/ProgressFilter.java @@ -0,0 +1,35 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.concurrency; + +public class ProgressFilter implements ProgressObservable { + + private final ProgressObservable delegate; + private final double offset; + private final double portion; + + public ProgressFilter(ProgressObservable delegate, double offset, double portion) { + this.delegate = delegate; + this.offset = offset; + this.portion = portion; + } + + @Override + public double getProgress() { + return offset + portion * Math.max(0, delegate.getProgress()); + } + + @Override + public String getStatus() { + return delegate.getStatus(); + } + + public static ProgressObservable between(ProgressObservable delegate, double from, double to) { + return new ProgressFilter(delegate, from, to - from); + } + +} diff --git a/launcher/src/main/java/com/skcraft/concurrency/ProgressObservable.java b/launcher/src/main/java/com/skcraft/concurrency/ProgressObservable.java new file mode 100644 index 0000000..2179e6a --- /dev/null +++ b/launcher/src/main/java/com/skcraft/concurrency/ProgressObservable.java @@ -0,0 +1,30 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.concurrency; + +/** + * Implementations of this interface can provide information on the progress + * of a task. + */ +public interface ProgressObservable { + + /** + * Get the progress value as a number between 0 and 1 (inclusive), or -1 + * if progress information is unavailable. + * + * @return the progress value + */ + double getProgress(); + + /** + * Get the current status text. + * + * @return the status text, or null if unavailable + */ + String getStatus(); + +} diff --git a/launcher/src/main/java/com/skcraft/concurrency/SettableProgress.java b/launcher/src/main/java/com/skcraft/concurrency/SettableProgress.java new file mode 100644 index 0000000..ce8f20b --- /dev/null +++ b/launcher/src/main/java/com/skcraft/concurrency/SettableProgress.java @@ -0,0 +1,46 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.concurrency; + +public class SettableProgress implements ProgressObservable { + + private ProgressObservable delegate; + private String status = ""; + private double progress = -1; + + public SettableProgress(String status, double progress) { + this.status = status; + this.progress = progress; + } + + public SettableProgress(ProgressObservable observable) { + this.delegate = observable; + } + + public synchronized void observe(ProgressObservable observable) { + delegate = observable; + } + + public synchronized void set(String status, double progress) { + delegate = null; + this.progress = progress; + this.status = status; + } + + @Override + public double getProgress() { + ProgressObservable delegate = this.delegate; + return delegate != null ? delegate.getProgress() : progress; + } + + @Override + public String getStatus() { + ProgressObservable delegate = this.delegate; + return delegate != null ? delegate.getStatus() : status; + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/AssetsRoot.java b/launcher/src/main/java/com/skcraft/launcher/AssetsRoot.java new file mode 100644 index 0000000..247188f --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/AssetsRoot.java @@ -0,0 +1,155 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher; + +import com.skcraft.concurrency.ProgressObservable; +import com.skcraft.launcher.model.minecraft.Asset; +import com.skcraft.launcher.model.minecraft.AssetsIndex; +import com.skcraft.launcher.model.minecraft.VersionManifest; +import com.skcraft.launcher.persistence.Persistence; +import lombok.Getter; +import lombok.NonNull; +import lombok.extern.java.Log; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Map; +import java.util.logging.Level; + +import static com.skcraft.launcher.util.SharedLocale.tr; + +/** + * Represents a directory that stores assets for Minecraft. The class has + * various methods that abstract operations involving the assets (such + * as getting the path to a certain object). + */ +@Log +public class AssetsRoot { + + @Getter + private final File dir; + + /** + * Create a new instance. + * + * @param dir the directory to the assets folder + */ + public AssetsRoot(@NonNull File dir) { + this.dir = dir; + } + + /** + * Get the path to the index .json file for a version manfiest. + * + * @param versionManifest the version manifest + * @return the file, which may not exist + */ + public File getIndexPath(VersionManifest versionManifest) { + return new File(dir, "indexes/" + versionManifest.getAssetId() + ".json"); + } + + /** + * Get the local path for a given asset. + * + * @param asset the asset + * @return the file, which may not exist + */ + public File getObjectPath(Asset asset) { + String hash = asset.getHash(); + return new File(dir, "objects/" + hash.substring(0, 2) + "/" + hash); + } + + /** + * Create an instance of the assets tree builder, which copies the indexed + * assets (identified by hashes) into a directory where the assets + * have been renamed and moved to their real names and locations + * (i.e. sounds/whatever.ogg). + * + * @param versionManifest the version manifest + * @return the builder + * @throws LauncherException + */ + public AssetsTreeBuilder createAssetsBuilder(@NonNull VersionManifest versionManifest) throws LauncherException { + String indexId = versionManifest.getAssetId(); + File path = getIndexPath(versionManifest); + AssetsIndex index = Persistence.read(path, AssetsIndex.class, true); + if (index == null || index.getObjects() == null) { + throw new LauncherException("Missing index at " + path, tr("assets.missingIndex", path.getAbsolutePath())); + } + File treeDir = new File(dir, "virtual/" + indexId); + treeDir.mkdirs(); + return new AssetsTreeBuilder(index, treeDir); + } + + public class AssetsTreeBuilder implements ProgressObservable { + private final AssetsIndex index; + private final File destDir; + private final int count; + private int processed = 0; + + public AssetsTreeBuilder(AssetsIndex index, File destDir) { + this.index = index; + this.destDir = destDir; + count = index.getObjects().size(); + } + + public File build() throws IOException, LauncherException { + AssetsRoot.log.info("Building asset virtual tree at '" + destDir.getAbsolutePath() + "'..."); + + boolean supportsLinks = true; + for (Map.Entry entry : index.getObjects().entrySet()) { + File objectPath = getObjectPath(entry.getValue()); + File virtualPath = new File(destDir, entry.getKey()); + virtualPath.getParentFile().mkdirs(); + if (!virtualPath.exists()) { + log.log(Level.INFO, "Copying {0} to {1}...", new Object[] { + objectPath.getAbsolutePath(), virtualPath.getAbsolutePath()}); + + if (!objectPath.exists()) { + String message = tr("assets.missingObject", objectPath.getAbsolutePath()); + throw new LauncherException("Missing object " + objectPath.getAbsolutePath(), message); + } + + if (supportsLinks) { + try { + Files.createLink(virtualPath.toPath(), objectPath.toPath()); + } catch (UnsupportedOperationException e) { + supportsLinks = false; + } + } + + if (!supportsLinks) { + Files.copy(objectPath.toPath(), virtualPath.toPath()); + } + } + processed++; + } + + return destDir; + } + + @Override + public double getProgress() { + if (count == 0) { + return -1; + } else { + return processed / (double) count; + } + } + + @Override + public String getStatus() { + if (count == 0) { + return tr("assets.expanding1", count, count - processed); + } else { + return tr("assets.expandingN", count, count - processed); + } + } + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/Configuration.java b/launcher/src/main/java/com/skcraft/launcher/Configuration.java new file mode 100644 index 0000000..8365feb --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/Configuration.java @@ -0,0 +1,69 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.skcraft.launcher.launch.runtime.JavaRuntime; +import com.skcraft.launcher.launch.runtime.JavaRuntimeFinder; +import lombok.Data; + +/** + * The configuration for the launcher. + *

+ * Default values are stored as field values. Note that if a default + * value is changed after the launcher has been deployed, it may not take effect + * for users who have already used the launcher because the old default + * values would have been written to disk. + */ +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class Configuration { + + private boolean offlineEnabled = false; + private JavaRuntime javaRuntime; + private String jvmArgs; + private int minMemory = 1024; + private int maxMemory = 0; // Updated in Launcher + private int permGen = 256; + private int windowWidth = 854; + private int windowHeight = 480; + private boolean proxyEnabled = false; + private String proxyHost = "localhost"; + private int proxyPort = 8080; + private String proxyUsername; + private String proxyPassword; + private String gameKey; + private boolean serverEnabled = false; + private String serverHost; + private int serverPort = 25565; + + @Override + public boolean equals(Object o) { + return super.equals(o); + } + + @Override + public int hashCode() { + return super.hashCode(); + } + + /** + * Backwards compatibility for old configs with the misspelling. + */ + 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/DefaultLauncherSupplier.java b/launcher/src/main/java/com/skcraft/launcher/DefaultLauncherSupplier.java new file mode 100644 index 0000000..181da15 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/DefaultLauncherSupplier.java @@ -0,0 +1,27 @@ +/* + * SKCraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher; + +import com.google.common.base.Supplier; +import com.skcraft.launcher.dialog.LauncherFrame; + +import java.awt.*; + +public class DefaultLauncherSupplier implements Supplier { + + private final Launcher launcher; + + public DefaultLauncherSupplier(Launcher launcher) { + this.launcher = launcher; + } + + @Override + public Window get() { + return new LauncherFrame(launcher); + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/Instance.java b/launcher/src/main/java/com/skcraft/launcher/Instance.java new file mode 100644 index 0000000..dd63900 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/Instance.java @@ -0,0 +1,172 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.io.Files; +import com.skcraft.launcher.launch.JavaProcessBuilder; +import com.skcraft.launcher.model.modpack.LaunchModifier; +import lombok.Data; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.Date; + +/** + * An instance is a profile that represents one particular installation + * of the game, with separate files and so on. + */ +@Data +public class Instance implements Comparable { + + private String title; + private String name; + private String version; + private boolean updatePending; + private boolean installed; + private Date lastAccessed; + @JsonProperty("launch") + private LaunchModifier launchModifier; + private InstanceSettings settings = new InstanceSettings(); + + @JsonIgnore private File dir; + @JsonIgnore private URL manifestURL; + @JsonIgnore private int priority; + @JsonIgnore private boolean selected; + @JsonIgnore private boolean local; + + /** + * Get the tile of the instance, which might be the same as the + * instance name if no title is set. + * + * @return a title + */ + public String getTitle() { + return title != null ? title : name; + } + + /** + * Update the given process builder with launch settings that are + * specific to this instance. + * + * @param builder the process builder + */ + public void modify(JavaProcessBuilder builder) { + if (launchModifier != null) { + launchModifier.modify(builder); + } + } + + /** + * Get the instance directory, creating it if possible. + * + * @return the directory + * @see #getContentDir() where game files are stored + */ + public File getDir() { + try { + Files.createParentDirs(dir); + dir.mkdir(); + } catch (IOException ignored) { + } + return dir; + } + + /** + * Get the file for the directory where Minecraft's game files are + * stored, including user files (screenshots, etc.). + * + * @return the content directory, which may not exist + */ + @JsonIgnore + public File getContentDir() { + File dir = new File(this.dir, "minecraft"); + try { + Files.createParentDirs(dir); + dir.mkdir(); + } catch (IOException ignored) { + } + return dir; + } + + /** + * Get the file for the package manifest. + * + * @return the manifest path, which may not exist + */ + @JsonIgnore + public File getManifestPath() { + return new File(getDir(), "manifest.json"); + } + + /** + * Get the file for the Minecraft version manfiest file. + * + * @return the version path, which may not exist + */ + @JsonIgnore + public File getVersionPath() { + return new File(getDir(), "version.json"); + } + + /** + * Get the file for the custom JAR file. + * + * @return the JAR file, which may not exist + */ + @JsonIgnore + public File getCustomJarPath() { + return new File(getContentDir(), "custom_jar.jar"); + } + + @Override + public String toString() { + return name; + } + + @Override + public int hashCode() { + return super.hashCode(); + } + + @Override + public boolean equals(Object other) { + return super.equals(other); + } + + @Override + public int compareTo(Instance o) { + if (isLocal() && !o.isLocal()) { + return -1; + } else if (!isLocal() && o.isLocal()) { + return 1; + } else if (isLocal() && o.isLocal()) { + Date otherDate = o.getLastAccessed(); + + if (otherDate == null && lastAccessed == null) { + return 0; + } else if (otherDate == null) { + return -1; + } else if (lastAccessed == null) { + return 1; + } else { + return -lastAccessed.compareTo(otherDate); + } + } else { + if (priority > o.priority) { + return -1; + } else if (priority < o.priority) { + return 1; + } else { + return 0; + } + } + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/InstanceList.java b/launcher/src/main/java/com/skcraft/launcher/InstanceList.java new file mode 100644 index 0000000..8134b99 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/InstanceList.java @@ -0,0 +1,215 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher; + +import com.skcraft.concurrency.DefaultProgress; +import com.skcraft.concurrency.ProgressObservable; +import com.skcraft.launcher.model.modpack.ManifestInfo; +import com.skcraft.launcher.model.modpack.PackageList; +import com.skcraft.launcher.persistence.Persistence; +import com.skcraft.launcher.util.HttpRequest; +import com.skcraft.launcher.util.SharedLocale; +import lombok.Getter; +import lombok.NonNull; +import lombok.extern.java.Log; +import org.apache.commons.io.filefilter.DirectoryFileFilter; + +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Callable; + +import static com.skcraft.launcher.LauncherUtils.concat; + +/** + * Stores the list of instances. + */ +@Log +public class InstanceList { + + private final Launcher launcher; + @Getter private final List instances = new ArrayList(); + + /** + * Create a new instance list. + * + * @param launcher the launcher + */ + public InstanceList(@NonNull Launcher launcher) { + this.launcher = launcher; + } + + /** + * Get the instance at a particular index. + * + * @param index the index + * @return the instance + */ + public synchronized Instance get(int index) { + return instances.get(index); + } + + /** + * Get the number of instances. + * + * @return the number of instances + */ + public synchronized int size() { + return instances.size(); + } + + /** + * Create a worker that loads the list of instances from disk and from + * the remote list of packages. + * + * @return the worker + */ + public Enumerator createEnumerator() { + return new Enumerator(); + } + + /** + * Get a list of selected instances. + * + * @return a list of instances + */ + public synchronized List getSelected() { + List selected = new ArrayList(); + for (Instance instance : instances) { + if (instance.isSelected()) { + selected.add(instance); + } + } + + return selected; + } + + /** + * Sort the list of instances. + */ + public synchronized void sort() { + Collections.sort(instances); + } + + public final class Enumerator implements Callable, ProgressObservable { + private ProgressObservable progress = new DefaultProgress(-1, null); + + private Enumerator() { + } + + @Override + public InstanceList call() throws Exception { + log.info("Enumerating instance list..."); + progress = new DefaultProgress(0, SharedLocale.tr("instanceLoader.loadingLocal")); + + List local = new ArrayList(); + List remote = new ArrayList(); + + File[] dirs = launcher.getInstancesDir().listFiles((FileFilter) DirectoryFileFilter.INSTANCE); + if (dirs != null) { + for (File dir : dirs) { + File file = new File(dir, "instance.json"); + Instance instance = Persistence.load(file, Instance.class); + instance.setDir(dir); + instance.setName(dir.getName()); + instance.setSelected(true); + instance.setLocal(true); + local.add(instance); + + log.info(instance.getName() + " local instance found at " + dir.getAbsolutePath()); + } + } + + progress = new DefaultProgress(0.3, SharedLocale.tr("instanceLoader.checkingRemote")); + + try { + URL packagesURL = launcher.getPackagesURL(); + + PackageList packages = HttpRequest + .get(packagesURL) + .execute() + .expectResponseCode(200) + .returnContent() + .asJson(PackageList.class); + + if (packages.getMinimumVersion() > Launcher.PROTOCOL_VERSION) { + throw new LauncherException("Update required", SharedLocale.tr("errors.updateRequiredError")); + } + + for (ManifestInfo manifest : packages.getPackages()) { + boolean foundLocal = false; + + for (Instance instance : local) { + if (instance.getName().equalsIgnoreCase(manifest.getName())) { + foundLocal = true; + + instance.setTitle(manifest.getTitle()); + instance.setPriority(manifest.getPriority()); + URL url = concat(packagesURL, manifest.getLocation()); + instance.setManifestURL(url); + + log.info("(" + instance.getName() + ").setManifestURL(" + url + ")"); + + // Check if an update is required + if (instance.getVersion() == null || !instance.getVersion().equals(manifest.getVersion())) { + instance.setUpdatePending(true); + instance.setVersion(manifest.getVersion()); + Persistence.commitAndForget(instance); + log.info(instance.getName() + " requires an update to " + manifest.getVersion()); + } + } + } + + if (!foundLocal) { + File dir = new File(launcher.getInstancesDir(), manifest.getName()); + File file = new File(dir, "instance.json"); + Instance instance = Persistence.load(file, Instance.class); + instance.setDir(dir); + instance.setTitle(manifest.getTitle()); + instance.setName(manifest.getName()); + instance.setVersion(manifest.getVersion()); + instance.setPriority(manifest.getPriority()); + instance.setSelected(false); + instance.setManifestURL(concat(packagesURL, manifest.getLocation())); + instance.setUpdatePending(true); + instance.setLocal(false); + remote.add(instance); + + log.info("Available remote instance: '" + instance.getName() + + "' at version " + instance.getVersion()); + } + } + } catch (IOException e) { + throw new IOException("The list of modpacks could not be downloaded.", e); + } finally { + synchronized (InstanceList.this) { + instances.clear(); + instances.addAll(local); + instances.addAll(remote); + + log.info(instances.size() + " instance(s) enumerated."); + } + } + + return InstanceList.this; + } + + @Override + public double getProgress() { + return -1; + } + + @Override + public String getStatus() { + return progress.getStatus(); + } + } +} 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 0000000..a320361 --- /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.MemorySettings; +import com.skcraft.launcher.launch.runtime.JavaRuntime; +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/InstanceTasks.java b/launcher/src/main/java/com/skcraft/launcher/InstanceTasks.java new file mode 100644 index 0000000..c46e79c --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/InstanceTasks.java @@ -0,0 +1,66 @@ +/* + * SKCraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher; + +import com.skcraft.concurrency.ObservableFuture; +import com.skcraft.launcher.dialog.ProgressDialog; +import com.skcraft.launcher.swing.SwingHelper; +import com.skcraft.launcher.update.HardResetter; +import com.skcraft.launcher.update.Remover; +import com.skcraft.launcher.util.SharedLocale; + +import java.awt.*; + +import static com.skcraft.launcher.util.SharedLocale.tr; + +public class InstanceTasks { + + private final Launcher launcher; + + public InstanceTasks(Launcher launcher) { + this.launcher = launcher; + } + + public ObservableFuture delete(Window window, Instance instance) { + // Execute the deleter + Remover resetter = new Remover(instance); + ObservableFuture future = new ObservableFuture( + launcher.getExecutor().submit(resetter), resetter); + + // Show progress + ProgressDialog.showProgress( + window, future, SharedLocale.tr("instance.deletingTitle"), tr("instance.deletingStatus", instance.getTitle())); + SwingHelper.addErrorDialogCallback(window, future); + + return future; + } + + public ObservableFuture hardUpdate(Window window, Instance instance) { + // Execute the resetter + HardResetter resetter = new HardResetter(instance); + ObservableFuture future = new ObservableFuture( + launcher.getExecutor().submit(resetter), resetter); + + // Show progress + ProgressDialog.showProgress(window, future, SharedLocale.tr("instance.resettingTitle"), + tr("instance.resettingStatus", instance.getTitle())); + SwingHelper.addErrorDialogCallback(window, future); + + return future; + } + + public ObservableFuture reloadInstances(Window window) { + InstanceList.Enumerator loader = launcher.getInstances().createEnumerator(); + ObservableFuture future = new ObservableFuture(launcher.getExecutor().submit(loader), loader); + + ProgressDialog.showProgress(window, future, SharedLocale.tr("launcher.checkingTitle"), SharedLocale.tr("launcher.checkingStatus")); + SwingHelper.addErrorDialogCallback(window, future); + + return future; + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/Launcher.java b/launcher/src/main/java/com/skcraft/launcher/Launcher.java new file mode 100644 index 0000000..baef3da --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/Launcher.java @@ -0,0 +1,471 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher; + +import com.beust.jcommander.JCommander; +import com.beust.jcommander.ParameterException; +import com.google.common.base.Strings; +import com.google.common.base.Supplier; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import com.skcraft.launcher.auth.*; +import com.skcraft.launcher.launch.LaunchSupervisor; +import com.skcraft.launcher.model.minecraft.Library; +import com.skcraft.launcher.model.minecraft.VersionManifest; +import com.skcraft.launcher.persistence.Persistence; +import com.skcraft.launcher.swing.SwingHelper; +import com.skcraft.launcher.update.UpdateManager; +import com.skcraft.launcher.util.Environment; +import com.skcraft.launcher.util.HttpRequest; +import com.skcraft.launcher.util.SharedLocale; +import com.skcraft.launcher.util.SimpleLogFormatter; +import com.sun.management.OperatingSystemMXBean; +import lombok.Getter; +import lombok.NonNull; +import lombok.Setter; +import lombok.extern.java.Log; +import org.apache.commons.io.FileUtils; + +import javax.swing.*; +import java.awt.*; +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.lang.management.ManagementFactory; +import java.net.URL; +import java.net.URLEncoder; +import java.util.Locale; +import java.util.Properties; +import java.util.concurrent.Executors; +import java.util.logging.Level; + +import static com.skcraft.launcher.util.SharedLocale.tr; + +/** + * The main entry point for the launcher. + */ +@Log +public final class Launcher { + + public static final int PROTOCOL_VERSION = 3; + + @Getter + private final ListeningExecutorService executor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool()); + @Getter @Setter private Supplier mainWindowSupplier = new DefaultLauncherSupplier(this); + @Getter private final File baseDir; + @Getter private final Properties properties; + @Getter private final InstanceList instances; + @Getter private final Configuration config; + @Getter private final AccountList accounts; + @Getter private final AssetsRoot assets; + @Getter private final LaunchSupervisor launchSupervisor = new LaunchSupervisor(this); + @Getter private final UpdateManager updateManager = new UpdateManager(this); + @Getter private final InstanceTasks instanceTasks = new InstanceTasks(this); + private final Environment env = Environment.getInstance(); + + /** + * Create a new launcher instance with the given base directory. + * + * @param baseDir the base directory + * @throws java.io.IOException on load error + */ + public Launcher(@NonNull File baseDir) throws IOException { + this(baseDir, baseDir); + } + + /** + * Create a new launcher instance with the given base and configuration + * directories. + * + * @param baseDir the base directory + * @param configDir the config directory + * @throws java.io.IOException on load error + */ + public Launcher(@NonNull File baseDir, @NonNull File configDir) throws IOException { + SharedLocale.loadBundle("com.skcraft.launcher.lang.Launcher", Locale.getDefault()); + + this.baseDir = baseDir.getAbsoluteFile(); + this.properties = LauncherUtils.loadProperties(Launcher.class, "launcher.properties", "com.skcraft.launcher.propertiesFile"); + this.instances = new InstanceList(this); + this.assets = new AssetsRoot(new File(baseDir, "assets")); + this.config = Persistence.load(new File(configDir, "config.json"), Configuration.class); + this.accounts = Persistence.load(new File(configDir, "accounts.dat"), AccountList.class); + + setDefaultConfig(); + + executor.submit(new Runnable() { + @Override + public void run() { + cleanupExtractDir(); + } + }); + + updateManager.checkForUpdate(null); + } + + /** + * Updates any incorrect / unset configuration settings with defaults. + */ + public void setDefaultConfig() { + double configMax = config.getMaxMemory() / 1024.0; + double suggestedMax = 2; + double available = Double.MAX_VALUE; + + try { + OperatingSystemMXBean bean = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean(); + available = bean.getTotalPhysicalMemorySize() / 1024.0 / 1024.0 / 1024.0; + if (available <= 6) { + suggestedMax = available * 0.48; + } else { + suggestedMax = 4; + } + } catch (Exception ignored) { + } + + if (config.getMaxMemory() <= 0 || configMax >= available - 1) { + config.setMaxMemory((int) (suggestedMax * 1024)); + } + } + + /** + * Get the launcher title. + * + * @return The launcher title. + */ + public String getTitle() { + return tr("launcher.appTitle"); + } + + /** + * Get the launcher version. + * + * @return the launcher version + */ + public String getVersion() { + String version = getProperties().getProperty("version"); + if (version.equals("${project.version}")) { + return "1.0.0-SNAPSHOT"; + } + return version; + } + + /** + * Get the Yggdrasil login service. + * + * @return the Yggdrasil (legacy) login service + */ + public YggdrasilLoginService getYggdrasil() { + return new YggdrasilLoginService(HttpRequest.url(getProperties().getProperty("yggdrasilAuthUrl")), accounts.getClientId()); + } + + /** + * Get the Microsoft login service. + * + * @return the Microsoft (current) login service + */ + public MicrosoftLoginService getMicrosoftLogin() { + return new MicrosoftLoginService(getProperties().getProperty("microsoftClientId")); + } + + public LoginService getLoginService(UserType type) { + if (type == UserType.MICROSOFT) { + return getMicrosoftLogin(); + } else { + return getYggdrasil(); + } + } + + /** + * Get the directory containing the instances. + * + * @return the instances dir + */ + public File getInstancesDir() { + return new File(getBaseDir(), "instances"); + } + + /** + * Get the directory to store temporary files. + * + * @return the temporary directory + */ + public File getTemporaryDir() { + return new File(getBaseDir(), "temp"); + } + + /** + * Get the directory to store temporary install files. + * + * @return the temporary install directory + */ + public File getInstallerDir() { + return new File(getTemporaryDir(), "install"); + } + + /** + * Get the directory to store temporarily extracted files. + * + * @return the directory + */ + private File getExtractDir() { + return new File(getTemporaryDir(), "extract"); + } + + /** + * Delete old extracted files. + */ + public void cleanupExtractDir() { + log.info("Cleaning up temporary extracted files directory..."); + + final long now = System.currentTimeMillis(); + + File[] dirs = getExtractDir().listFiles(new FileFilter() { + @Override + public boolean accept(File pathname) { + try { + long time = Long.parseLong(pathname.getName()); + return (now - time) > (1000 * 60 * 60); + } catch (NumberFormatException e) { + return false; + } + } + }); + + if (dirs != null) { + for (File dir : dirs) { + log.info("Removing " + dir.getAbsolutePath() + "..."); + try { + FileUtils.deleteDirectory(dir); + } catch (IOException e) { + log.log(Level.WARNING, "Failed to delete " + dir.getAbsolutePath(), e); + } + } + } + } + + /** + * Create a new temporary directory to extract files to. + * + * @return the directory path + */ + public File createExtractDir() { + File dir = new File(getExtractDir(), String.valueOf(System.currentTimeMillis())); + dir.mkdirs(); + log.info("Created temporary directory " + dir.getAbsolutePath()); + return dir; + } + + /** + * Get the directory to store the launcher binaries. + * + * @return the libraries directory + */ + public File getLauncherBinariesDir() { + return new File(getBaseDir(), "launcher"); + } + + /** + * Get the directory to store common data files. + * + * @return the common data directory + */ + public File getCommonDataDir() { + return getBaseDir(); + } + + /** + * Get the directory to store libraries. + * + * @return the libraries directory + */ + public File getLibrariesDir() { + return new File(getCommonDataDir(), "libraries"); + } + + /** + * Fetch a library file. + * @param library Library to fetch + * @return File pointing to the library on disk. + */ + public File getLibraryFile(Library library) { + return new File(getLibrariesDir(), library.getPath(env)); + } + + /** + * Get the directory to store versions. + * + * @return the versions directory + */ + public File getVersionsDir() { + return new File(getCommonDataDir(), "versions"); + } + + /** + * Get the directory to store a version. + * + * @param version the version + * @return the directory + */ + public File getVersionDir(String version) { + return new File(getVersionsDir(), version); + } + + /** + * Get the path to the JAR for the given version manifest. + * + * @param versionManifest the version manifest + * @return the path + */ + public File getJarPath(VersionManifest versionManifest) { + return new File(getVersionDir(versionManifest.getId()), versionManifest.getId() + ".jar"); + } + + /** + * Get the news URL. + * + * @return the news URL + */ + public URL getNewsURL() { + try { + return HttpRequest.url( + String.format(getProperties().getProperty("newsUrl"), + URLEncoder.encode(getVersion(), "UTF-8"))); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + + /** + * Get the packages URL. + * + * @return the packages URL + */ + public URL getPackagesURL() { + try { + String key = Strings.nullToEmpty(getConfig().getGameKey()); + return HttpRequest.url( + String.format(getProperties().getProperty("packageListUrl"), + URLEncoder.encode(key, "UTF-8"))); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + + /** + * Convenient method to fetch a property. + * + * @param key the key + * @return the property + */ + public String prop(String key) { + return getProperties().getProperty(key); + } + + /** + * Convenient method to fetch a property. + * + * @param key the key + * @param args formatting arguments + * @return the property + */ + public String prop(String key, String... args) { + return String.format(getProperties().getProperty(key), (Object[]) args); + } + + /** + * Convenient method to fetch a property. + * + * @param key the key + * @return the property + */ + public URL propUrl(String key) { + return HttpRequest.url(prop(key)); + } + + /** + * Convenient method to fetch a property. + * + * @param key the key + * @param args formatting arguments + * @return the property + */ + public URL propUrl(String key, String... args) { + return HttpRequest.url(prop(key, args)); + } + + /** + * Show the launcher. + */ + public Window showLauncherWindow() { + Window window = mainWindowSupplier.get(); + window.setVisible(true); + + return window; + } + + /** + * Create a new launcher from arguments. + * + * @param args the arguments + * @return the launcher + * @throws ParameterException thrown on a bad parameter + * @throws IOException throw on an I/O error + */ + public static Launcher createFromArguments(String[] args) throws ParameterException, IOException { + LauncherArguments options = new LauncherArguments(); + new JCommander(options).parse(args); + + Integer bsVersion = options.getBootstrapVersion(); + log.info(bsVersion != null ? "Bootstrap version " + bsVersion + " detected" : "Not bootstrapped"); + + File dir = options.getDir(); + if (dir != null) { + dir = dir.getAbsoluteFile(); + log.info("Using given base directory " + dir.getAbsolutePath()); + } else { + dir = new File("").getAbsoluteFile(); + log.info("Using current directory " + dir.getAbsolutePath()); + } + + return new Launcher(dir); + } + + /** + * Setup loggers and perform initialization. + */ + public static void setupLogger() { + SimpleLogFormatter.configureGlobalLogger(); + } + + /** + * Bootstrap. + * + * @param args args + */ + public static void main(final String[] args) { + setupLogger(); + + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + try { + Launcher launcher = createFromArguments(args); + SwingHelper.setSwingProperties(tr("launcher.appTitle", launcher.getVersion())); + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + launcher.showLauncherWindow(); + } catch (Throwable t) { + log.log(Level.WARNING, "Load failure", t); + SwingHelper.showErrorDialog(null, "Uh oh! The updater couldn't be opened because a " + + "problem was encountered.", "Launcher error", t); + } + } + }); + + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/LauncherArguments.java b/launcher/src/main/java/com/skcraft/launcher/LauncherArguments.java new file mode 100644 index 0000000..2b340e0 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/LauncherArguments.java @@ -0,0 +1,29 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher; + +import com.beust.jcommander.Parameter; +import lombok.Data; + +import java.io.File; + +/** + * The command line arguments that the launcher accepts. + */ +@Data +public class LauncherArguments { + + @Parameter(names = "--dir") + private File dir; + + @Parameter(names = "--bootstrap-version") + private Integer bootstrapVersion; + + @Parameter(names = "--portable") + private boolean portable; + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/LauncherException.java b/launcher/src/main/java/com/skcraft/launcher/LauncherException.java new file mode 100644 index 0000000..dea9d41 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/LauncherException.java @@ -0,0 +1,30 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher; + +/** + * A human-readable error wrapper. + */ +public class LauncherException extends Exception { + + private final String localizedMessage; + + public LauncherException(String message, String localizedMessage) { + super(message); + this.localizedMessage = localizedMessage; + } + + public LauncherException(Throwable cause, String localizedMessage) { + super(cause.getMessage(), cause); + this.localizedMessage = localizedMessage; + } + + @Override + public String getLocalizedMessage() { + return localizedMessage; + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/LauncherUtils.java b/launcher/src/main/java/com/skcraft/launcher/LauncherUtils.java new file mode 100644 index 0000000..6520ef7 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/LauncherUtils.java @@ -0,0 +1,113 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher; + +import com.google.common.io.Closer; +import lombok.extern.java.Log; + +import java.io.*; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.List; +import java.util.Properties; +import java.util.regex.Pattern; + +@Log +public final class LauncherUtils { + + private static final Pattern absoluteUrlPattern = Pattern.compile("^[A-Za-z0-9\\-]+://.*$"); + + private LauncherUtils() { + } + + public static String getStackTrace(Throwable t) { + Writer result = new StringWriter(); + PrintWriter printWriter = new PrintWriter(result); + t.printStackTrace(printWriter); + return result.toString(); + } + + public static void checkInterrupted() throws InterruptedException { + if (Thread.interrupted()) { + throw new InterruptedException(); + } + } + + public static Properties loadProperties(Class clazz, String name, String extraProperty) throws IOException { + Closer closer = Closer.create(); + Properties prop = new Properties(); + try { + InputStream in = closer.register(clazz.getResourceAsStream(name)); + if (in != null) { + prop.load(in); + String extraPath = System.getProperty(extraProperty); + if (extraPath != null) { + log.info("Loading extra properties for " + + clazz.getCanonicalName() + ":" + name + " from " + extraPath + "..."); + in = closer.register(new BufferedInputStream(closer.register(new FileInputStream(extraPath)))); + prop.load(in); + } + } else { + throw new FileNotFoundException(); + } + } finally { + closer.close(); + } + + return prop; + } + + public static URL concat(URL baseUrl, String url) throws MalformedURLException { + if (absoluteUrlPattern.matcher(url).matches()) { + return new URL(url); + } + + int lastSlash = baseUrl.toExternalForm().lastIndexOf("/"); + if (lastSlash == -1) { + return new URL(url); + } + + int firstSlash = url.indexOf("/"); + if (firstSlash == 0) { + boolean portSet = (baseUrl.getDefaultPort() == baseUrl.getPort() || + baseUrl.getPort() == -1); + String port = portSet ? "" : ":" + baseUrl.getPort(); + return new URL(baseUrl.getProtocol() + "://" + baseUrl.getHost() + + port + url); + } else { + return new URL(baseUrl.toExternalForm().substring(0, lastSlash + 1) + url); + } + } + + + + public static void interruptibleDelete(File file, List failures) throws IOException, InterruptedException { + checkInterrupted(); + + if (!file.exists()) { + throw new FileNotFoundException("Does not exist: " + file); + } + + if (file.isDirectory()) { + File[] files = file.listFiles(); + + if (files == null) { + throw new IOException("Failed to list contents of " + file.getAbsolutePath()); + } + + for (File f : files) { + interruptibleDelete(f, failures); + } + } + + if (!file.delete()) { + log.warning("Failed to delete " + file.getAbsolutePath()); + failures.add(file); + } + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/AccountList.java b/launcher/src/main/java/com/skcraft/launcher/auth/AccountList.java new file mode 100644 index 0000000..969caeb --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/auth/AccountList.java @@ -0,0 +1,78 @@ +package com.skcraft.launcher.auth; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.google.common.collect.Lists; +import com.skcraft.launcher.dialog.component.ListListenerReducer; +import com.skcraft.launcher.persistence.Scrambled; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.apache.commons.lang.RandomStringUtils; + +import javax.swing.*; +import javax.swing.event.ListDataEvent; +import javax.swing.event.ListDataListener; +import java.util.List; + +/** + * Persisted account list + */ +@Scrambled("ACCOUNT_LIST_NOT_SECURITY!") +@Getter +@Setter +@ToString +@JsonIgnoreProperties(ignoreUnknown = true) +public class AccountList implements ListModel { + private List accounts = Lists.newArrayList(); + private String clientId = RandomStringUtils.randomAlphanumeric(24); + + @JsonIgnore private final ListListenerReducer listeners = new ListListenerReducer(); + + public synchronized void add(SavedSession session) { + accounts.add(session); + + int index = accounts.size() - 1; + listeners.intervalAdded(new ListDataEvent(this, ListDataEvent.INTERVAL_ADDED, index, index)); + } + + public synchronized void remove(SavedSession session) { + int index = accounts.indexOf(session); + + if (index > -1) { + accounts.remove(index); + listeners.intervalRemoved(new ListDataEvent(this, ListDataEvent.INTERVAL_REMOVED, index, index)); + } + } + + public synchronized void update(SavedSession newSavedSession) { + int index = accounts.indexOf(newSavedSession); + + if (index > -1) { + accounts.set(index, newSavedSession); + listeners.contentsChanged(new ListDataEvent(this, ListDataEvent.CONTENTS_CHANGED, index, index)); + } else { + this.add(newSavedSession); + } + } + + @Override + public int getSize() { + return accounts.size(); + } + + @Override + public SavedSession getElementAt(int index) { + return accounts.get(index); + } + + @Override + public void addListDataListener(ListDataListener l) { + listeners.addListDataListener(l); + } + + @Override + public void removeListDataListener(ListDataListener l) { + listeners.removeListDataListener(l); + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/AuthenticationException.java b/launcher/src/main/java/com/skcraft/launcher/auth/AuthenticationException.java new file mode 100644 index 0000000..6290d74 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/auth/AuthenticationException.java @@ -0,0 +1,35 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.auth; + +import com.skcraft.launcher.LauncherException; +import lombok.Getter; + +/** + * Thrown on authentication error. + */ +public class AuthenticationException extends LauncherException { + @Getter + private boolean invalidatedSession = false; + + public AuthenticationException(String message, String localizedMessage) { + super(message, localizedMessage); + } + + public AuthenticationException(String message) { + super(message, message); + } + + public AuthenticationException(String message, boolean invalidatedSession) { + super(message, message); + this.invalidatedSession = invalidatedSession; + } + + public AuthenticationException(Throwable cause, String localizedMessage) { + super(cause, localizedMessage); + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/LoginService.java b/launcher/src/main/java/com/skcraft/launcher/auth/LoginService.java new file mode 100644 index 0000000..f6da3d2 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/auth/LoginService.java @@ -0,0 +1,28 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.auth; + +import java.io.IOException; + +/** + * A service for creating authenticated sessions. + */ +public interface LoginService { + + /** + * Attempt to restore a saved session into an active session. + * + * @param savedSession Session to restore + * @return An authenticated session, which corresponds to a Minecraft account + * @throws IOException thrown on I/O error + * @throws InterruptedException thrown if interrupted + * @throws AuthenticationException thrown on an authentication error + */ + Session restore(SavedSession savedSession) + throws IOException, InterruptedException, AuthenticationException; + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/MicrosoftLoginService.java b/launcher/src/main/java/com/skcraft/launcher/auth/MicrosoftLoginService.java new file mode 100644 index 0000000..0c46177 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/auth/MicrosoftLoginService.java @@ -0,0 +1,177 @@ +package com.skcraft.launcher.auth; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.skcraft.launcher.auth.microsoft.MicrosoftWebAuthorizer; +import com.skcraft.launcher.auth.microsoft.MinecraftServicesAuthorizer; +import com.skcraft.launcher.auth.microsoft.OauthResult; +import com.skcraft.launcher.auth.microsoft.XboxTokenAuthorizer; +import com.skcraft.launcher.auth.microsoft.model.McAuthResponse; +import com.skcraft.launcher.auth.microsoft.model.McProfileResponse; +import com.skcraft.launcher.auth.microsoft.model.TokenResponse; +import com.skcraft.launcher.auth.microsoft.model.XboxAuthorization; +import com.skcraft.launcher.auth.skin.MinecraftSkinService; +import com.skcraft.launcher.util.HttpRequest; +import lombok.Data; +import lombok.RequiredArgsConstructor; + +import java.io.IOException; +import java.net.URL; +import java.util.Collections; +import java.util.Map; +import java.util.function.Consumer; + +import static com.skcraft.launcher.util.HttpRequest.url; + +@RequiredArgsConstructor +public class MicrosoftLoginService implements LoginService { + private static final URL MS_TOKEN_URL = url("https://login.live.com/oauth20_token.srf"); + + private final String clientId; + + /** + * Trigger a full login sequence with the Microsoft authenticator. + * + * @param oauthDone Callback called when OAuth is complete and automatic login is about to begin. + * @return Valid {@link Session} instance representing the logged-in player. + * @throws IOException if any I/O error occurs. + * @throws InterruptedException if the current thread is interrupted + * @throws AuthenticationException if authentication fails in any way, this is thrown with a human-useful message. + */ + public Session login(Receiver oauthDone) throws IOException, InterruptedException, AuthenticationException { + MicrosoftWebAuthorizer authorizer = new MicrosoftWebAuthorizer(clientId); + OauthResult auth = authorizer.authorize(); + + if (auth.isError()) { + OauthResult.Error error = (OauthResult.Error) auth; + throw new AuthenticationException(error.getErrorMessage()); + } + + TokenResponse response = exchangeToken(form -> { + form.add("grant_type", "authorization_code"); + form.add("redirect_uri", authorizer.getRedirectUri()); + form.add("code", ((OauthResult.Success) auth).getAuthCode()); + }); + + oauthDone.tell(); + Profile session = performLogin(response.getAccessToken(), null); + session.setRefreshToken(response.getRefreshToken()); + + return session; + } + + @Override + public Session restore(SavedSession savedSession) + throws IOException, InterruptedException, AuthenticationException { + TokenResponse response = exchangeToken(form -> { + form.add("grant_type", "refresh_token"); + form.add("refresh_token", savedSession.getRefreshToken()); + }); + + Profile session = performLogin(response.getAccessToken(), savedSession); + session.setRefreshToken(response.getRefreshToken()); + + return session; + } + + private TokenResponse exchangeToken(Consumer formConsumer) + throws IOException, InterruptedException, AuthenticationException { + HttpRequest.Form form = HttpRequest.Form.form(); + form.add("client_id", clientId); + formConsumer.accept(form); + + return HttpRequest.post(MS_TOKEN_URL) + .bodyForm(form) + .execute() + .expectResponseCodeOr(200, (req) -> { + TokenError error = req.returnContent().asJson(TokenError.class); + + return new AuthenticationException(error.errorDescription, true); + }) + .returnContent() + .asJson(TokenResponse.class); + } + + private Profile performLogin(String microsoftToken, SavedSession previous) + throws IOException, InterruptedException, AuthenticationException { + XboxAuthorization xboxAuthorization = XboxTokenAuthorizer.authorizeWithXbox(microsoftToken); + McAuthResponse auth = MinecraftServicesAuthorizer.authorizeWithMinecraft(xboxAuthorization); + McProfileResponse profile = MinecraftServicesAuthorizer.getUserProfile(auth); + + Profile session = new Profile(auth, profile); + if (previous != null && previous.getAvatarImage() != null) { + session.setAvatarImage(previous.getAvatarImage()); + } else { + session.setAvatarImage(MinecraftSkinService.fetchSkinHead(profile)); + } + + return session; + } + + @Data + public static class Profile implements Session { + private final McAuthResponse auth; + private final McProfileResponse profile; + private final Map userProperties = Collections.emptyMap(); + private String refreshToken; + private byte[] avatarImage; + + @Override + public String getUuid() { + return profile.getUuid(); + } + + @Override + public String getName() { + return profile.getName(); + } + + @Override + public String getAccessToken() { + return auth.getAccessToken(); + } + + @Override + public String getSessionToken() { + return String.format("token:%s:%s", getAccessToken(), getUuid()); + } + + @Override + public UserType getUserType() { + return UserType.MICROSOFT; + } + + @Override + public boolean isOnline() { + return true; + } + + @Override + public SavedSession toSavedSession() { + SavedSession savedSession = new SavedSession(); + + savedSession.setType(getUserType()); + savedSession.setUsername(getName()); + savedSession.setUuid(getUuid()); + savedSession.setAccessToken(getAccessToken()); + savedSession.setRefreshToken(getRefreshToken()); + savedSession.setAvatarImage(getAvatarImage()); + + return savedSession; + } + } + + @Data + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + @JsonIgnoreProperties(ignoreUnknown = true) + private static class TokenError { + private String error; + private String errorDescription; + } + + @FunctionalInterface + public interface Receiver { + void tell(); + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/OfflineSession.java b/launcher/src/main/java/com/skcraft/launcher/auth/OfflineSession.java new file mode 100644 index 0000000..cb2520a --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/auth/OfflineSession.java @@ -0,0 +1,70 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.auth; + +import lombok.Getter; +import lombok.NonNull; + +import java.util.Collections; +import java.util.Map; +import java.util.UUID; + +/** + * An offline session. + */ +public class OfflineSession implements Session { + + private static Map dummyProperties = Collections.emptyMap(); + + @Getter + private final String name; + + /** + * Create a new offline session using the given player name. + * + * @param name the player name + */ + public OfflineSession(@NonNull String name) { + this.name = name; + } + + @Override + public String getUuid() { + return (new UUID(0, 0)).toString(); + } + + @Override + public String getAccessToken() { + return "0"; + } + + @Override + public Map getUserProperties() { + return dummyProperties; + } + + @Override + public String getSessionToken() { + return "-"; + } + + @Override + public UserType getUserType() { + return UserType.LEGACY; + } + + @Override + public byte[] getAvatarImage() { + return null; + } + + @Override + public boolean isOnline() { + return false; + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/SavedSession.java b/launcher/src/main/java/com/skcraft/launcher/auth/SavedSession.java new file mode 100644 index 0000000..9e12f1b --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/auth/SavedSession.java @@ -0,0 +1,37 @@ +package com.skcraft.launcher.auth; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; +import org.apache.commons.lang.builder.HashCodeBuilder; + +/** + * Represents a session saved to disk. + */ +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class SavedSession { + private UserType type; + private String uuid; + private String username; + private String accessToken; + private String refreshToken; + private byte[] avatarImage; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + + if (o == null || getClass() != o.getClass()) return false; + + SavedSession that = (SavedSession) o; + + return getUuid().equals(that.getUuid()); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37) + .append(uuid) + .toHashCode(); + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/Session.java b/launcher/src/main/java/com/skcraft/launcher/auth/Session.java new file mode 100644 index 0000000..6535c14 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/auth/Session.java @@ -0,0 +1,90 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.auth; + +import java.util.Map; + +/** + * Represents an authenticated (or virtual) session. + */ +public interface Session { + + /** + * Get the user's UUID. + * + * @return the user's UUID + */ + String getUuid(); + + /** + * Get the user's game username. + * + * @return the username + */ + String getName(); + + /** + * Get the access token. + * + * @return the access token + */ + String getAccessToken(); + + /** + * Get a map of user properties. + * + * @return the map of user properties + */ + Map getUserProperties(); + + /** + * Get the session token string, which is in the form of + * token:accessToken:uuid for authenticated players, and + * simply - for offline players. + * + * @return the session token + */ + String getSessionToken(); + + /** + * Get the user type. + * + * @return the user type + */ + UserType getUserType(); + + /** + * Get the user's avatar + * + * @return User's avatar as a base64 string. + */ + byte[] getAvatarImage(); + + /** + * Return true if the user is in an online session. + * + * @return true if online + */ + boolean isOnline(); + + /** + * Convert this session to a saved session + * @return Saved session that represents this active session + */ + default SavedSession toSavedSession() { + SavedSession savedSession = new SavedSession(); + + savedSession.setType(getUserType()); + savedSession.setUsername(getName()); + savedSession.setUuid(getUuid()); + savedSession.setAccessToken(getAccessToken()); + savedSession.setAvatarImage(getAvatarImage()); + + return savedSession; + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/UserType.java b/launcher/src/main/java/com/skcraft/launcher/auth/UserType.java new file mode 100644 index 0000000..a697886 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/auth/UserType.java @@ -0,0 +1,40 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.auth; + +/** + * Represents the type of user for the account. + */ +public enum UserType { + /** + * Legacy accounts login with an account username. + */ + LEGACY("legacy"), + /** + * Mojang accounts login with an email address. + */ + MOJANG("mojang"), + /** + * Microsoft accounts login via OAuth. + */ + MICROSOFT("msa"); + + private final String id; + + UserType(String id) { + this.id = id; + } + + /** + * Return the account type string as the game understands it + * + * @return the account type ID for passing to the game + */ + public String getId() { + return this.id; + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/YggdrasilLoginService.java b/launcher/src/main/java/com/skcraft/launcher/auth/YggdrasilLoginService.java new file mode 100644 index 0000000..0ea50c1 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/auth/YggdrasilLoginService.java @@ -0,0 +1,146 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.auth; + +import com.fasterxml.jackson.annotation.*; +import com.skcraft.launcher.util.HttpRequest; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +import java.io.IOException; +import java.net.URL; +import java.util.Collections; +import java.util.Map; + +/** + * Creates authenticated sessions using the Mojang Yggdrasil login protocol. + */ +@RequiredArgsConstructor +public class YggdrasilLoginService implements LoginService { + + private final URL authUrl; + private final String clientId; + + public Session login(String id, String password) + throws IOException, InterruptedException, AuthenticationException { + AuthenticatePayload payload = new AuthenticatePayload(new Agent("Minecraft"), id, password, clientId); + + return call(this.authUrl, payload, null); + } + + @Override + public Session restore(SavedSession savedSession) + throws IOException, InterruptedException, AuthenticationException { + RefreshPayload payload = new RefreshPayload(savedSession.getAccessToken(), clientId); + + return call(new URL(this.authUrl, "/refresh"), payload, savedSession); + } + + private Session call(URL url, Object payload, SavedSession previous) + throws IOException, InterruptedException, AuthenticationException { + HttpRequest req = HttpRequest + .post(url) + .bodyJson(payload) + .execute(); + + if (req.getResponseCode() != 200) { + ErrorResponse error = req.returnContent().asJson(ErrorResponse.class); + + throw new AuthenticationException(error.getErrorMessage(), true); + } else { + AuthenticateResponse response = req.returnContent().asJson(AuthenticateResponse.class); + Profile profile = response.getSelectedProfile(); + + if (profile == null) return null; // Minecraft not owned + + if (previous != null && previous.getAvatarImage() != null) { + profile.setAvatarImage(previous.getAvatarImage()); + } + + // DEPRECEATION: minecraft services API no longer accepts yggdrasil tokens + // login still works though. until it doesn't, this class will remain + + return profile; + } + } + + @Data + private static class Agent { + private final String name; + private final int version = 1; + } + + @Data + private static class AuthenticatePayload { + private final Agent agent; + private final String username; + private final String password; + private final String clientToken; + } + + @Data + private static class RefreshPayload { + private final String accessToken; + private final String clientToken; + private boolean requestUser = true; + } + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + private static class AuthenticateResponse { + private String accessToken; + private String clientToken; + @JsonManagedReference private Profile selectedProfile; + } + + @Data + private static class ErrorResponse { + private String error; + private String errorMessage; + private String cause; + } + + /** + * Return in the list of available profiles. + */ + @Data + @ToString(exclude = "response") + @JsonIgnoreProperties(ignoreUnknown = true) + private static class Profile implements Session { + @JsonProperty("id") private String uuid; + private String name; + private boolean legacy; + private byte[] avatarImage; + @JsonIgnore private final Map userProperties = Collections.emptyMap(); + @JsonBackReference private AuthenticateResponse response; + + @Override + @JsonIgnore + public String getSessionToken() { + return String.format("token:%s:%s", getAccessToken(), getUuid()); + } + + @Override + @JsonIgnore + public String getAccessToken() { + return response.getAccessToken(); + } + + @Override + @JsonIgnore + public UserType getUserType() { + return legacy ? UserType.LEGACY : UserType.MOJANG; + } + + @Override + public boolean isOnline() { + return true; + } + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/MicrosoftWebAuthorizer.java b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/MicrosoftWebAuthorizer.java new file mode 100644 index 0000000..a4d7953 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/MicrosoftWebAuthorizer.java @@ -0,0 +1,59 @@ +package com.skcraft.launcher.auth.microsoft; + +import com.skcraft.launcher.auth.AuthenticationException; +import com.skcraft.launcher.swing.SwingHelper; +import com.skcraft.launcher.util.HttpRequest; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.awt.*; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; + +/** + * Handles the Microsoft leg of OAuth authorization. + */ +@RequiredArgsConstructor +public class MicrosoftWebAuthorizer { + private final String clientId; + @Getter private String redirectUri; + + public OauthResult authorize() throws IOException, AuthenticationException, InterruptedException { + if (Desktop.isDesktopSupported()) { + // Interactive auth + return authorizeInteractive(); + } else { + // TODO Device code auth + return null; + } + } + + private OauthResult authorizeInteractive() throws IOException, AuthenticationException, InterruptedException { + OauthHttpHandler httpHandler = new OauthHttpHandler(); + SwingHelper.openURL(generateInteractiveUrl(httpHandler.getPort())); + + return httpHandler.await(); + } + + private URI generateInteractiveUrl(int port) throws AuthenticationException { + redirectUri = "http://localhost:" + port; + + URI interactive; + try { + HttpRequest.Form query = HttpRequest.Form.form(); + query.add("client_id", clientId); + query.add("scope", "XboxLive.signin XboxLive.offline_access"); + query.add("response_type", "code"); + query.add("redirect_uri", redirectUri); + query.add("prompt", "select_account"); + + interactive = new URI("https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?" + + query.toString()); + } catch (URISyntaxException e) { + throw new AuthenticationException(e, "Failed to generate OAuth URL"); + } + + return interactive; + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/MinecraftServicesAuthorizer.java b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/MinecraftServicesAuthorizer.java new file mode 100644 index 0000000..a64a67d --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/MinecraftServicesAuthorizer.java @@ -0,0 +1,59 @@ +package com.skcraft.launcher.auth.microsoft; + +import com.skcraft.launcher.auth.AuthenticationException; +import com.skcraft.launcher.auth.microsoft.model.*; +import com.skcraft.launcher.util.HttpRequest; +import com.skcraft.launcher.util.SharedLocale; + +import java.io.IOException; +import java.net.URL; + +import static com.skcraft.launcher.util.HttpRequest.url; + +public class MinecraftServicesAuthorizer { + private static final URL MC_SERVICES_LOGIN = url("https://api.minecraftservices.com/authentication/login_with_xbox"); + private static final URL MC_SERVICES_PROFILE = url("https://api.minecraftservices.com/minecraft/profile"); + + public static McAuthResponse authorizeWithMinecraft(XboxAuthorization auth) throws IOException, InterruptedException { + McAuthRequest request = new McAuthRequest("XBL3.0 x=" + auth.getCombinedToken()); + + return HttpRequest.post(MC_SERVICES_LOGIN) + .bodyJson(request) + .header("Accept", "application/json") + .execute() + .expectResponseCode(200) + .returnContent() + .asJson(McAuthResponse.class); + } + + public static McProfileResponse getUserProfile(McAuthResponse auth) + throws IOException, InterruptedException, AuthenticationException { + return getUserProfile(auth.getAuthorization()); + } + + public static McProfileResponse getUserProfile(String authorization) + throws IOException, InterruptedException, AuthenticationException { + return HttpRequest.get(MC_SERVICES_PROFILE) + .header("Authorization", authorization) + .execute() + .expectResponseCodeOr(200, req -> { + HttpRequest.BufferedResponse content = req.returnContent(); + if (content.asBytes().length == 0) { + return new AuthenticationException("Got empty response from Minecraft services", + SharedLocale.tr("login.minecraft.error", req.getResponseCode())); + } + + McServicesError error = content.asJson(McServicesError.class); + + if (error.getError().equals("NOT_FOUND")) { + return new AuthenticationException("No Minecraft profile", + SharedLocale.tr("login.minecraftNotOwnedError")); + } + + return new AuthenticationException(error.getErrorMessage(), + SharedLocale.tr("login.minecraft.error", error.getErrorMessage())); + }) + .returnContent() + .asJson(McProfileResponse.class); + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/OauthHttpHandler.java b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/OauthHttpHandler.java new file mode 100644 index 0000000..34fd9bf --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/OauthHttpHandler.java @@ -0,0 +1,86 @@ +package com.skcraft.launcher.auth.microsoft; + +import com.google.common.base.Charsets; +import com.google.common.base.Splitter; +import com.skcraft.launcher.Launcher; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; +import lombok.extern.java.Log; +import org.apache.commons.io.IOUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.net.InetSocketAddress; +import java.util.Base64; +import java.util.Map; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +@Log +public class OauthHttpHandler { + private Executor executor = Executors.newCachedThreadPool(); + private HttpServer server; + private OauthResult result; + + public OauthHttpHandler() throws IOException { + server = HttpServer.create(new InetSocketAddress("localhost", 0), 0); + + server.createContext("/", new Handler()); + server.setExecutor(executor); + server.start(); + } + + public int getPort() { + return server.getAddress().getPort(); + } + + public OauthResult await() throws InterruptedException { + synchronized (this) { + this.wait(); + } + + server.stop(3); + + return result; + } + + private class Handler implements HttpHandler { + @Override + public void handle(HttpExchange httpExchange) throws IOException { + String query = httpExchange.getRequestURI().getQuery(); + Map qs = Splitter.on('&').withKeyValueSeparator('=').split(query); + if (qs.get("error") != null) { + result = new OauthResult.Error(qs.get("error_description")); + } else { + result = new OauthResult.Success(qs.get("code")); + } + + synchronized (OauthHttpHandler.this) { + OauthHttpHandler.this.notifyAll(); + } + + byte[] response; + InputStream is = Launcher.class.getResourceAsStream("login.html"); + if (is != null) { + response = IOUtils.toByteArray(is); + } else { + response = "Unable to fetch resource login.html".getBytes(Charsets.UTF_8); + } + + InputStream iconStream = Launcher.class.getResourceAsStream("icon.png"); + if (iconStream != null) { + byte[] iconBytes = IOUtils.toByteArray(iconStream); + String encodedIcon = Base64.getEncoder().encodeToString(iconBytes); + response = String.format(new String(response), encodedIcon).getBytes(Charsets.UTF_8); + } else { + log.warning("Unable to fetch resource icon.png"); + } + + httpExchange.sendResponseHeaders(200, response.length); + httpExchange.getResponseBody().write(response); + httpExchange.getResponseBody().flush(); + httpExchange.getResponseBody().close(); + } + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/OauthResult.java b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/OauthResult.java new file mode 100644 index 0000000..28c8001 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/OauthResult.java @@ -0,0 +1,28 @@ +package com.skcraft.launcher.auth.microsoft; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +public interface OauthResult { + boolean isError(); + + @RequiredArgsConstructor + class Success implements OauthResult { + @Getter private final String authCode; + + @Override + public boolean isError() { + return false; + } + } + + @RequiredArgsConstructor + class Error implements OauthResult { + @Getter private final String errorMessage; + + @Override + public boolean isError() { + return true; + } + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/XboxTokenAuthorizer.java b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/XboxTokenAuthorizer.java new file mode 100644 index 0000000..8798b87 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/XboxTokenAuthorizer.java @@ -0,0 +1,65 @@ +package com.skcraft.launcher.auth.microsoft; + +import com.skcraft.launcher.auth.AuthenticationException; +import com.skcraft.launcher.auth.microsoft.model.*; +import com.skcraft.launcher.util.HttpRequest; +import com.skcraft.launcher.util.SharedLocale; + +import java.io.IOException; +import java.net.URL; + +import static com.skcraft.launcher.util.HttpRequest.url; + +public class XboxTokenAuthorizer { + private static final URL XBL_AUTHENTICATE_URL = url("https://user.auth.xboxlive.com/user/authenticate"); + private static final URL XSTS_AUTHENTICATE_URL = url("https://xsts.auth.xboxlive.com/xsts/authorize"); + + public static XboxAuthorization authorizeWithXbox(String accessToken) + throws IOException, InterruptedException, AuthenticationException { + XboxAuthRequest xblPayload = + new XboxAuthRequest<>(new XblAuthProperties("d=" + accessToken)); + + XboxAuthResponse xblResponse = HttpRequest.post(XBL_AUTHENTICATE_URL) + .bodyJson(xblPayload) + .header("Accept", "application/json") + .execute() + .expectResponseCodeOr(200, (req) -> + new AuthenticationException("Error authenticating with Xbox Live", + SharedLocale.tr("login.xbox.generic"))) + .returnContent() + .asJson(XboxAuthResponse.class); + + XboxAuthRequest xstsPayload = + new XboxAuthRequest<>(new XstsAuthProperties(xblResponse.getToken())); + xstsPayload.setRelyingParty("rp://api.minecraftservices.com/"); + + XboxAuthResponse xstsResponse = HttpRequest.post(XSTS_AUTHENTICATE_URL) + .bodyJson(xstsPayload) + .header("Accept", "application/json") + .execute() + .expectResponseCodeOr(200, (req) -> { + XstsError xstsError = req.returnContent().asJson(XstsError.class); + + return new AuthenticationException(xstsError.getMessage(), getErrorMessage(xstsError)); + }) + .returnContent() + .asJson(XboxAuthResponse.class); + + return new XboxAuthorization(xstsResponse.getToken(), xstsResponse.getUhs()); + } + + private static String getErrorMessage(XstsError xstsError) { + long xboxErrorCode = xstsError.getXErr(); + if (xboxErrorCode == 2148916233L) { + return SharedLocale.tr("login.xbox.noXboxAccount"); + } + if (xboxErrorCode == 2148916238L) { + return SharedLocale.tr("login.xbox.isChild"); + } + if (!xstsError.getMessage().isEmpty()) { + return SharedLocale.tr("login.xbox.errorMessage", xstsError.getMessage()); + } + + return SharedLocale.tr("login.xbox.unknown", xboxErrorCode); + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/McAuthRequest.java b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/McAuthRequest.java new file mode 100644 index 0000000..505da69 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/McAuthRequest.java @@ -0,0 +1,8 @@ +package com.skcraft.launcher.auth.microsoft.model; + +import lombok.Data; + +@Data +public class McAuthRequest { + private final String identityToken; +} diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/McAuthResponse.java b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/McAuthResponse.java new file mode 100644 index 0000000..c047d12 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/McAuthResponse.java @@ -0,0 +1,21 @@ +package com.skcraft.launcher.auth.microsoft.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.Data; + +@Data +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +@JsonIgnoreProperties(ignoreUnknown = true) +public class McAuthResponse { + private String accessToken; + private String tokenType; + private int expiresIn; + + @JsonIgnore + public String getAuthorization() { + return String.format("%s %s", tokenType, accessToken); + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/McProfileResponse.java b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/McProfileResponse.java new file mode 100644 index 0000000..b442cbb --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/McProfileResponse.java @@ -0,0 +1,28 @@ +package com.skcraft.launcher.auth.microsoft.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.Lists; +import lombok.Data; + +import java.util.List; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class McProfileResponse { + @JsonProperty("id") private String uuid; + private String name; + private List skins = Lists.newArrayList(); + + public Skin getActiveSkin() { + return skins.stream().filter(skin -> skin.getState().equals("ACTIVE")).findFirst().orElse(null); + } + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Skin { + private String state; + private String url; + private String variant; + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/McServicesError.java b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/McServicesError.java new file mode 100644 index 0000000..034f05d --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/McServicesError.java @@ -0,0 +1,11 @@ +package com.skcraft.launcher.auth.microsoft.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class McServicesError { + private String error; + private String errorMessage; +} diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/TokenResponse.java b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/TokenResponse.java new file mode 100644 index 0000000..2348afc --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/TokenResponse.java @@ -0,0 +1,15 @@ +package com.skcraft.launcher.auth.microsoft.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.Data; + +@Data +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +@JsonIgnoreProperties(ignoreUnknown = true) +public class TokenResponse { + private String tokenType; + private String accessToken; + private String refreshToken; +} diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XblAuthProperties.java b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XblAuthProperties.java new file mode 100644 index 0000000..9891875 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XblAuthProperties.java @@ -0,0 +1,15 @@ +package com.skcraft.launcher.auth.microsoft.model; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.Data; +import lombok.NonNull; + +@Data +@JsonNaming(PropertyNamingStrategies.UpperCamelCaseStrategy.class) +public class XblAuthProperties { + private String authMethod = "RPS"; + private String siteName = "user.auth.xboxlive.com"; + @NonNull + private String rpsTicket; +} diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XboxAuthRequest.java b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XboxAuthRequest.java new file mode 100644 index 0000000..3888357 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XboxAuthRequest.java @@ -0,0 +1,14 @@ +package com.skcraft.launcher.auth.microsoft.model; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.Data; +import lombok.NonNull; + +@Data +@JsonNaming(PropertyNamingStrategies.UpperCamelCaseStrategy.class) +public class XboxAuthRequest { + @NonNull private T properties; + private String relyingParty = "http://auth.xboxlive.com"; + private String tokenType = "JWT"; +} diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XboxAuthResponse.java b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XboxAuthResponse.java new file mode 100644 index 0000000..e989e38 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XboxAuthResponse.java @@ -0,0 +1,32 @@ +package com.skcraft.launcher.auth.microsoft.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.Data; + +import java.util.List; + +@Data +@JsonNaming(PropertyNamingStrategies.UpperCamelCaseStrategy.class) +@JsonIgnoreProperties(ignoreUnknown = true) +public class XboxAuthResponse { + private String token; + private DisplayClaims displayClaims; + + @JsonIgnore + public String getUhs() { + return getDisplayClaims().getXui().get(0).getUhs(); + } + + @Data + public static class DisplayClaims { + private List xui; + } + + @Data + public static class UhsContainer { + private String uhs; + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XboxAuthorization.java b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XboxAuthorization.java new file mode 100644 index 0000000..499a0e6 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XboxAuthorization.java @@ -0,0 +1,13 @@ +package com.skcraft.launcher.auth.microsoft.model; + +import lombok.Data; + +@Data +public class XboxAuthorization { + private final String token; + private final String uhs; + + public String getCombinedToken() { + return String.format("%s;%s", uhs, token); + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XstsAuthProperties.java b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XstsAuthProperties.java new file mode 100644 index 0000000..5d59693 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XstsAuthProperties.java @@ -0,0 +1,19 @@ +package com.skcraft.launcher.auth.microsoft.model; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.Data; + +import java.util.Collections; +import java.util.List; + +@Data +@JsonNaming(PropertyNamingStrategies.UpperCamelCaseStrategy.class) +public class XstsAuthProperties { + private String sandboxId = "RETAIL"; + private List userTokens; + + public XstsAuthProperties(String token) { + this.userTokens = Collections.singletonList(token); + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XstsError.java b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XstsError.java new file mode 100644 index 0000000..aec313f --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XstsError.java @@ -0,0 +1,17 @@ +package com.skcraft.launcher.auth.microsoft.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.Data; + +@Data +@JsonNaming(PropertyNamingStrategies.UpperCamelCaseStrategy.class) +@JsonIgnoreProperties(ignoreUnknown = true) +public class XstsError { + @JsonProperty("XErr") + private long xErr; + private String message; + private String redirect; +} diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/skin/MinecraftSkinService.java b/launcher/src/main/java/com/skcraft/launcher/auth/skin/MinecraftSkinService.java new file mode 100644 index 0000000..831155e --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/auth/skin/MinecraftSkinService.java @@ -0,0 +1,30 @@ +package com.skcraft.launcher.auth.skin; + +import com.skcraft.launcher.auth.microsoft.model.McProfileResponse; +import com.skcraft.launcher.util.HttpRequest; +import lombok.extern.java.Log; + +import java.io.IOException; +import java.util.logging.Level; + +@Log +public class MinecraftSkinService { + static byte[] downloadSkin(String textureUrl) throws IOException, InterruptedException { + return HttpRequest.get(HttpRequest.url(textureUrl)) + .execute() + .expectResponseCode(200) + .returnContent() + .asBytes(); + } + + public static byte[] fetchSkinHead(McProfileResponse profile) throws InterruptedException { + try { + byte[] skin = downloadSkin(profile.getActiveSkin().getUrl()); + + return SkinProcessor.renderHead(skin); + } catch (IOException e) { + log.log(Level.WARNING, "Failed to download or process skin.", e); + return null; + } + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/skin/SkinProcessor.java b/launcher/src/main/java/com/skcraft/launcher/auth/skin/SkinProcessor.java new file mode 100644 index 0000000..7622329 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/auth/skin/SkinProcessor.java @@ -0,0 +1,27 @@ +package com.skcraft.launcher.auth.skin; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +public class SkinProcessor { + public static byte[] renderHead(byte[] skinData) throws IOException { + BufferedImage skin = ImageIO.read(new ByteArrayInputStream(skinData)); + + BufferedImage result = new BufferedImage(32, 32, BufferedImage.TYPE_INT_RGB); + Graphics graphics = result.getGraphics(); + + // Draw bottom head layer + graphics.drawImage(skin, 0, 0, 32, 32, 8, 8, 16, 16, null); + // Draw top head layer + graphics.drawImage(skin, 0, 0, 32, 32, 40, 8, 48, 16, null); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ImageIO.write(result, "png", outputStream); + + return outputStream.toByteArray(); + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/skin/VisageSkinService.java b/launcher/src/main/java/com/skcraft/launcher/auth/skin/VisageSkinService.java new file mode 100644 index 0000000..2c7e770 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/auth/skin/VisageSkinService.java @@ -0,0 +1,29 @@ +package com.skcraft.launcher.auth.skin; + +import com.skcraft.launcher.util.HttpRequest; +import lombok.extern.java.Log; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.util.logging.Level; + +import static com.skcraft.launcher.util.HttpRequest.url; + +@Log +public class VisageSkinService { + @Nullable + public static byte[] fetchSkinHead(String uuid) throws InterruptedException { + String skinUrl = String.format("https://visage.surgeplay.com/face/32/%s.png", uuid); + + try { + return HttpRequest.get(url(skinUrl)) + .execute() + .expectResponseCode(200) + .returnContent() + .asBytes(); + } catch (IOException e) { + log.log(Level.WARNING, "Failed to download or process skin from Visage.", e); + return null; + } + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/dialog/AboutDialog.java b/launcher/src/main/java/com/skcraft/launcher/dialog/AboutDialog.java new file mode 100644 index 0000000..30b4662 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/dialog/AboutDialog.java @@ -0,0 +1,58 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.dialog; + +import com.skcraft.launcher.swing.ActionListeners; +import net.miginfocom.swing.MigLayout; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.KeyEvent; + +public class AboutDialog extends JDialog { + + public AboutDialog(Window parent) { + super(parent, "About", ModalityType.DOCUMENT_MODAL); + + setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + initComponents(); + setResizable(false); + pack(); + setLocationRelativeTo(parent); + } + + private void initComponents() { + JPanel container = new JPanel(); + container.setLayout(new MigLayout("insets dialog")); + + container.add(new JLabel("Licensed under the GNU Lesser General Public License, version 3."), "wrap, gapbottom unrel"); + container.add(new JLabel("You are using SKCraft Launcher, an open-source customizable
" + + "launcher platform that anyone can use."), "wrap, gapbottom unrel"); + container.add(new JLabel("SKCraft does not necessarily endorse the version of
" + + "the launcher that you are using."), "wrap, gapbottom unrel"); + + JButton okButton = new JButton("OK"); + JButton sourceCodeButton = new JButton("Website"); + + container.add(sourceCodeButton, "span, split 3, sizegroup bttn"); + container.add(okButton, "tag ok, sizegroup bttn"); + + add(container, BorderLayout.CENTER); + + getRootPane().setDefaultButton(okButton); + getRootPane().registerKeyboardAction(ActionListeners.dispose(this), KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW); + + okButton.addActionListener(ActionListeners.dispose(this)); + sourceCodeButton.addActionListener(ActionListeners.openURL(this, "https://github.com/SKCraft/Launcher")); + } + + public static void showAboutDialog(Window parent) { + AboutDialog dialog = new AboutDialog(parent); + dialog.setVisible(true); + } +} + diff --git a/launcher/src/main/java/com/skcraft/launcher/dialog/AccountSelectDialog.java b/launcher/src/main/java/com/skcraft/launcher/dialog/AccountSelectDialog.java new file mode 100644 index 0000000..89d0785 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/dialog/AccountSelectDialog.java @@ -0,0 +1,259 @@ +package com.skcraft.launcher.dialog; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.skcraft.concurrency.ObservableFuture; +import com.skcraft.concurrency.ProgressObservable; +import com.skcraft.concurrency.SettableProgress; +import com.skcraft.launcher.Launcher; +import com.skcraft.launcher.auth.*; +import com.skcraft.launcher.persistence.Persistence; +import com.skcraft.launcher.swing.LinedBoxPanel; +import com.skcraft.launcher.swing.SwingHelper; +import com.skcraft.launcher.util.SharedLocale; +import com.skcraft.launcher.util.SwingExecutor; +import lombok.RequiredArgsConstructor; + +import javax.swing.*; +import java.awt.*; +import java.util.concurrent.Callable; + +public class AccountSelectDialog extends JDialog { + private final JList accountList; + private final JButton loginButton = new JButton(SharedLocale.tr("accounts.play")); + private final JButton cancelButton = new JButton(SharedLocale.tr("button.cancel")); + private final JButton addMojangButton = new JButton(SharedLocale.tr("accounts.addMojang")); + private final JButton addMicrosoftButton = new JButton(SharedLocale.tr("accounts.addMicrosoft")); + private final JButton removeSelected = new JButton(SharedLocale.tr("accounts.removeSelected")); + private final JButton offlineButton = new JButton(SharedLocale.tr("login.playOffline")); + private final LinedBoxPanel buttonsPanel = new LinedBoxPanel(true); + + private final Launcher launcher; + private Session selected; + + public AccountSelectDialog(Window owner, Launcher launcher) { + super(owner, ModalityType.DOCUMENT_MODAL); + + this.launcher = launcher; + this.accountList = new JList<>(launcher.getAccounts()); + + setTitle(SharedLocale.tr("accounts.title")); + initComponents(); + setDefaultCloseOperation(DISPOSE_ON_CLOSE); + setMinimumSize(new Dimension(350, 250)); + setResizable(false); + pack(); + setLocationRelativeTo(owner); + } + + private void initComponents() { + setLayout(new BorderLayout()); + + accountList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + accountList.setLayoutOrientation(JList.VERTICAL); + accountList.setVisibleRowCount(0); + accountList.setCellRenderer(new AccountRenderer()); + + JScrollPane accountPane = new JScrollPane(accountList); + accountPane.setPreferredSize(new Dimension(280, 150)); + accountPane.setAlignmentX(CENTER_ALIGNMENT); + + loginButton.setFont(loginButton.getFont().deriveFont(Font.BOLD)); + loginButton.setMargin(new Insets(0, 10, 0, 10)); + + //Start Buttons + buttonsPanel.setBorder(BorderFactory.createEmptyBorder(26, 13, 13, 13)); + if (launcher.getConfig().isOfflineEnabled()) { + buttonsPanel.addElement(offlineButton); + } + buttonsPanel.addGlue(); + buttonsPanel.addElement(cancelButton); + buttonsPanel.addElement(loginButton); + + //Login Buttons + JPanel loginButtonsRow = new JPanel(new BorderLayout(0, 5)); + addMojangButton.setAlignmentX(CENTER_ALIGNMENT); + addMicrosoftButton.setAlignmentX(CENTER_ALIGNMENT); + removeSelected.setAlignmentX(CENTER_ALIGNMENT); + loginButtonsRow.add(addMojangButton, BorderLayout.NORTH); + loginButtonsRow.add(addMicrosoftButton, BorderLayout.CENTER); + loginButtonsRow.add(removeSelected, BorderLayout.SOUTH); + loginButtonsRow.setBorder(BorderFactory.createEmptyBorder(0, 10, 0, 0)); + + JPanel listAndLoginContainer = new JPanel(); + listAndLoginContainer.add(accountPane, BorderLayout.WEST); + listAndLoginContainer.add(loginButtonsRow, BorderLayout.EAST); + listAndLoginContainer.add(Box.createVerticalStrut(5)); + listAndLoginContainer.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + + add(listAndLoginContainer, BorderLayout.CENTER); + add(buttonsPanel, BorderLayout.SOUTH); + + loginButton.addActionListener(ev -> attemptExistingLogin(accountList.getSelectedValue())); + cancelButton.addActionListener(ev -> dispose()); + + addMojangButton.addActionListener(ev -> { + Session newSession = LoginDialog.showLoginRequest(this, launcher); + + if (newSession != null) { + launcher.getAccounts().update(newSession.toSavedSession()); + setResult(newSession); + } + }); + + addMicrosoftButton.addActionListener(ev -> attemptMicrosoftLogin(SharedLocale.tr("login.microsoft.seeBrowser"))); + + offlineButton.addActionListener(ev -> + setResult(new OfflineSession(launcher.getProperties().getProperty("offlinePlayerName")))); + + removeSelected.addActionListener(ev -> { + if (accountList.getSelectedValue() != null) { + boolean confirmed = SwingHelper.confirmDialog(this, SharedLocale.tr("accounts.confirmForget"), + SharedLocale.tr("accounts.confirmForgetTitle")); + + if (confirmed) { + launcher.getAccounts().remove(accountList.getSelectedValue()); + } + } + }); + + accountList.setSelectedIndex(0); + } + + @Override + public void dispose() { + accountList.setModel(new DefaultListModel<>()); + super.dispose(); + } + + public static Session showAccountRequest(Window owner, Launcher launcher) { + AccountSelectDialog dialog = new AccountSelectDialog(owner, launcher); + dialog.setVisible(true); + + if (dialog.selected != null && dialog.selected.isOnline()) { + launcher.getAccounts().update(dialog.selected.toSavedSession()); + } + + Persistence.commitAndForget(launcher.getAccounts()); + + return dialog.selected; + } + + private void setResult(Session result) { + this.selected = result; + dispose(); + } + + private void attemptMicrosoftLogin(String status) { + SettableProgress progress = new SettableProgress(status, -1); + + ListenableFuture future = launcher.getExecutor().submit(() -> { + Session newSession = launcher.getMicrosoftLogin().login(() -> + progress.set(SharedLocale.tr("login.loggingInStatus"), -1)); + + if (newSession != null) { + launcher.getAccounts().update(newSession.toSavedSession()); + setResult(newSession); + } + + return null; + }); + + ProgressDialog.showProgress(this, future, progress, + SharedLocale.tr("login.loggingInTitle"), status); + SwingHelper.addErrorDialogCallback(this, future); + } + + private void attemptExistingLogin(SavedSession session) { + if (session == null) return; + + LoginService loginService = launcher.getLoginService(session.getType()); + RestoreSessionCallable callable = new RestoreSessionCallable(loginService, session); + + ObservableFuture future = new ObservableFuture<>(launcher.getExecutor().submit(callable), callable); + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(Session result) { + setResult(result); + } + + @Override + public void onFailure(Throwable t) { + if (t instanceof AuthenticationException && ((AuthenticationException) t).isInvalidatedSession()) { + // Just need to log in again + relogin(session, t.getLocalizedMessage()); + } else { + SwingHelper.showErrorDialog(AccountSelectDialog.this, t.getLocalizedMessage(), SharedLocale.tr("errorTitle"), t); + } + } + }, SwingExecutor.INSTANCE); + + ProgressDialog.showProgress(this, future, SharedLocale.tr("login.loggingInTitle"), + SharedLocale.tr("login.loggingInStatus")); + } + + /** + * Re-login to an expired session + */ + private void relogin(SavedSession session, String message) { + if (session.getType() == UserType.MICROSOFT) { + this.attemptMicrosoftLogin(message); + } else { + LoginDialog.ReloginDetails details = new LoginDialog.ReloginDetails(session.getUsername(), + SharedLocale.tr("login.relogin", message)); + Session newSession = LoginDialog.showLoginRequest(AccountSelectDialog.this, launcher, details); + + launcher.getAccounts().update(newSession.toSavedSession()); + setResult(newSession); + } + } + + @RequiredArgsConstructor + private static class RestoreSessionCallable implements Callable, ProgressObservable { + private final LoginService service; + private final SavedSession session; + + @Override + public Session call() throws Exception { + return service.restore(session); + } + + @Override + public String getStatus() { + return SharedLocale.tr("accounts.refreshingStatus"); + } + + @Override + public double getProgress() { + return -1; + } + } + + private static class AccountRenderer extends JLabel implements ListCellRenderer { + public AccountRenderer() { + setHorizontalAlignment(LEFT); + } + + @Override + public Component getListCellRendererComponent(JList list, SavedSession value, int index, boolean isSelected, boolean cellHasFocus) { + setText(value.getUsername()); + if (value.getAvatarImage() != null) { + setIcon(new ImageIcon(value.getAvatarImage())); + } else { + setIcon(SwingHelper.createIcon(Launcher.class, "default_skin.png", 32, 32)); + } + + if (isSelected) { + setOpaque(true); + setBackground(list.getSelectionBackground()); + setForeground(list.getSelectionForeground()); + } else { + setOpaque(false); + setForeground(list.getForeground()); + } + + return this; + } + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/dialog/ConfigurationDialog.java b/launcher/src/main/java/com/skcraft/launcher/dialog/ConfigurationDialog.java new file mode 100644 index 0000000..e0025bc --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/dialog/ConfigurationDialog.java @@ -0,0 +1,219 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.dialog; + +import com.skcraft.launcher.Configuration; +import com.skcraft.launcher.Launcher; +import com.skcraft.launcher.dialog.component.BetterComboBox; +import com.skcraft.launcher.launch.runtime.AddJavaRuntime; +import com.skcraft.launcher.launch.runtime.JavaRuntime; +import com.skcraft.launcher.launch.runtime.JavaRuntimeFinder; +import com.skcraft.launcher.persistence.Persistence; +import com.skcraft.launcher.swing.*; +import com.skcraft.launcher.util.SharedLocale; +import lombok.NonNull; + +import javax.swing.*; +import javax.swing.filechooser.FileFilter; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; +import java.util.Arrays; + +/** + * A dialog to modify configuration options. + */ +public class ConfigurationDialog extends JDialog { + + private final Configuration config; + private final ObjectSwingMapper mapper; + + private final JPanel tabContainer = new JPanel(new BorderLayout()); + private final JTabbedPane tabbedPane = new JTabbedPane(); + private final FormPanel javaSettingsPanel = new FormPanel(); + private final JComboBox jvmRuntime = new BetterComboBox<>(); + private final JTextField jvmArgsText = new JTextField(); + private final JSpinner minMemorySpinner = new JSpinner(); + private final JSpinner maxMemorySpinner = new JSpinner(); + private final JSpinner permGenSpinner = new JSpinner(); + private final FormPanel gameSettingsPanel = new FormPanel(); + private final JSpinner widthSpinner = new JSpinner(); + private final JSpinner heightSpinner = new JSpinner(); + private final FormPanel proxySettingsPanel = new FormPanel(); + private final JCheckBox useProxyCheck = new JCheckBox(SharedLocale.tr("options.useProxyCheck")); + private final JTextField proxyHostText = new JTextField(); + private final JSpinner proxyPortText = new JSpinner(); + private final JTextField proxyUsernameText = new JTextField(); + private final JPasswordField proxyPasswordText = new JPasswordField(); + private final FormPanel advancedPanel = new FormPanel(); + private final JTextField gameKeyText = new JTextField(); + private final LinedBoxPanel buttonsPanel = new LinedBoxPanel(true); + private final JButton okButton = new JButton(SharedLocale.tr("button.ok")); + private final JButton cancelButton = new JButton(SharedLocale.tr("button.cancel")); + private final JButton aboutButton = new JButton(SharedLocale.tr("options.about")); + private final JButton logButton = new JButton(SharedLocale.tr("options.launcherConsole")); + + /** + * Create a new configuration dialog. + * + * @param owner the window owner + * @param launcher the launcher + */ + public ConfigurationDialog(Window owner, @NonNull Launcher launcher) { + super(owner, ModalityType.DOCUMENT_MODAL); + + this.config = launcher.getConfig(); + mapper = new ObjectSwingMapper(config); + + JavaRuntime[] javaRuntimes = JavaRuntimeFinder.getAvailableRuntimes().toArray(new JavaRuntime[0]); + DefaultComboBoxModel model = new DefaultComboBoxModel<>(javaRuntimes); + + // Put the runtime from the config in the model if it isn't + boolean configRuntimeFound = Arrays.stream(javaRuntimes).anyMatch(r -> r.equals(config.getJavaRuntime())); + if (!configRuntimeFound && config.getJavaRuntime() != null) { + model.insertElementAt(config.getJavaRuntime(), 0); + } + + jvmRuntime.setModel(model); + jvmRuntime.addItem(AddJavaRuntime.ADD_RUNTIME_SENTINEL); + + jvmRuntime.setSelectedItem(config.getJavaRuntime()); + + setTitle(SharedLocale.tr("options.title")); + initComponents(); // Must be called after jvmRuntime model setup + setDefaultCloseOperation(DISPOSE_ON_CLOSE); + setSize(new Dimension(400, 500)); + setResizable(false); + setLocationRelativeTo(owner); + + mapper.map(jvmArgsText, "jvmArgs"); + mapper.map(minMemorySpinner, "minMemory"); + mapper.map(maxMemorySpinner, "maxMemory"); + mapper.map(permGenSpinner, "permGen"); + mapper.map(widthSpinner, "windowWidth"); + mapper.map(heightSpinner, "windowHeight"); + mapper.map(useProxyCheck, "proxyEnabled"); + mapper.map(proxyHostText, "proxyHost"); + mapper.map(proxyPortText, "proxyPort"); + mapper.map(proxyUsernameText, "proxyUsername"); + mapper.map(proxyPasswordText, "proxyPassword"); + mapper.map(gameKeyText, "gameKey"); + + mapper.copyFromObject(); + } + + private void initComponents() { + 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"))); + javaSettingsPanel.addRow(new JLabel(SharedLocale.tr("options.minMemory")), minMemorySpinner); + javaSettingsPanel.addRow(new JLabel(SharedLocale.tr("options.maxMemory")), maxMemorySpinner); + javaSettingsPanel.addRow(new JLabel(SharedLocale.tr("options.permGen")), permGenSpinner); + SwingHelper.removeOpaqueness(javaSettingsPanel); + tabbedPane.addTab(SharedLocale.tr("options.javaTab"), SwingHelper.alignTabbedPane(javaSettingsPanel)); + + gameSettingsPanel.addRow(new JLabel(SharedLocale.tr("options.windowWidth")), widthSpinner); + gameSettingsPanel.addRow(new JLabel(SharedLocale.tr("options.windowHeight")), heightSpinner); + SwingHelper.removeOpaqueness(gameSettingsPanel); + tabbedPane.addTab(SharedLocale.tr("options.minecraftTab"), SwingHelper.alignTabbedPane(gameSettingsPanel)); + + proxySettingsPanel.addRow(useProxyCheck); + proxySettingsPanel.addRow(new JLabel(SharedLocale.tr("options.proxyHost")), proxyHostText); + proxySettingsPanel.addRow(new JLabel(SharedLocale.tr("options.proxyPort")), proxyPortText); + proxySettingsPanel.addRow(new JLabel(SharedLocale.tr("options.proxyUsername")), proxyUsernameText); + proxySettingsPanel.addRow(new JLabel(SharedLocale.tr("options.proxyPassword")), proxyPasswordText); + SwingHelper.removeOpaqueness(proxySettingsPanel); + tabbedPane.addTab(SharedLocale.tr("options.proxyTab"), SwingHelper.alignTabbedPane(proxySettingsPanel)); + + advancedPanel.addRow(new JLabel(SharedLocale.tr("options.gameKey")), gameKeyText); + SwingHelper.removeOpaqueness(advancedPanel); + tabbedPane.addTab(SharedLocale.tr("options.advancedTab"), SwingHelper.alignTabbedPane(advancedPanel)); + + buttonsPanel.addElement(logButton); + buttonsPanel.addElement(aboutButton); + buttonsPanel.addGlue(); + buttonsPanel.addElement(okButton); + buttonsPanel.addElement(cancelButton); + + tabContainer.add(tabbedPane, BorderLayout.CENTER); + tabContainer.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + add(tabContainer, BorderLayout.CENTER); + add(buttonsPanel, BorderLayout.SOUTH); + + SwingHelper.equalWidth(okButton, cancelButton); + + cancelButton.addActionListener(ActionListeners.dispose(this)); + + aboutButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + AboutDialog.showAboutDialog(ConfigurationDialog.this); + } + }); + + okButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + save(); + } + }); + + logButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + ConsoleFrame.showMessages(); + } + }); + + jvmRuntime.addActionListener(e -> { + // A little fun hack... + if (jvmRuntime.getSelectedItem() == AddJavaRuntime.ADD_RUNTIME_SENTINEL) { + jvmRuntime.setSelectedItem(null); + jvmRuntime.setPopupVisible(false); + + JFileChooser chooser = new JFileChooser(); + chooser.setFileSelectionMode(JFileChooser.FILES_ONLY); + chooser.setFileFilter(new JavaRuntimeFileFilter()); + chooser.setDialogTitle("Choose a Java executable"); + + int result = chooser.showOpenDialog(this); + if (result == JFileChooser.APPROVE_OPTION) { + JavaRuntime runtime = JavaRuntimeFinder.getRuntimeFromPath(chooser.getSelectedFile().getAbsolutePath()); + + MutableComboBoxModel model = (MutableComboBoxModel) jvmRuntime.getModel(); + model.insertElementAt(runtime, 0); + jvmRuntime.setSelectedItem(runtime); + } + } + }); + } + + /** + * Save the configuration and close the dialog. + */ + public void save() { + mapper.copyFromSwing(); + config.setJavaRuntime((JavaRuntime) jvmRuntime.getSelectedItem()); + + Persistence.commitAndForget(config); + dispose(); + } + + static class JavaRuntimeFileFilter extends FileFilter { + @Override + public boolean accept(File f) { + return f.isDirectory() || f.getName().startsWith("java") && f.canExecute(); + } + + @Override + public String getDescription() { + return "Java runtime executables"; + } + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/dialog/ConsoleFrame.java b/launcher/src/main/java/com/skcraft/launcher/dialog/ConsoleFrame.java new file mode 100644 index 0000000..bdd5dd1 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/dialog/ConsoleFrame.java @@ -0,0 +1,172 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.dialog; + +import com.skcraft.launcher.Launcher; +import com.skcraft.launcher.swing.LinedBoxPanel; +import com.skcraft.launcher.swing.MessageLog; +import com.skcraft.launcher.swing.SwingHelper; +import com.skcraft.launcher.util.PastebinPoster; +import com.skcraft.launcher.util.SharedLocale; +import lombok.Getter; +import lombok.NonNull; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; + +import static com.skcraft.launcher.util.SharedLocale.tr; + +/** + * A frame capable of showing messages. + */ +public class ConsoleFrame extends JFrame { + + private static ConsoleFrame globalFrame; + + @Getter private final Image trayRunningIcon; + @Getter private final Image trayClosedIcon; + + @Getter private final MessageLog messageLog; + @Getter private LinedBoxPanel buttonsPanel; + + private boolean registeredGlobalLog = false; + + /** + * Construct the frame. + * + * @param numLines number of lines to show at a time + * @param colorEnabled true to enable a colored console + */ + public ConsoleFrame(int numLines, boolean colorEnabled) { + this(SharedLocale.tr("console.title"), numLines, colorEnabled); + } + + /** + * Construct the frame. + * + * @param title the title of the window + * @param numLines number of lines to show at a time + * @param colorEnabled true to enable a colored console + */ + public ConsoleFrame(@NonNull String title, int numLines, boolean colorEnabled) { + messageLog = new MessageLog(numLines, colorEnabled); + trayRunningIcon = SwingHelper.createImage(Launcher.class, "tray_ok.png"); + trayClosedIcon = SwingHelper.createImage(Launcher.class, "tray_closed.png"); + + setTitle(title); + setIconImage(trayRunningIcon); + + setSize(new Dimension(650, 400)); + initComponents(); + + setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); + addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent event) { + performClose(); + } + }); + } + + /** + * Add components to the frame. + */ + private void initComponents() { + JButton pastebinButton = new JButton(SharedLocale.tr("console.uploadLog")); + JButton clearLogButton = new JButton(SharedLocale.tr("console.clearLog")); + buttonsPanel = new LinedBoxPanel(true); + + buttonsPanel.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); + buttonsPanel.addElement(pastebinButton); + buttonsPanel.addElement(clearLogButton); + + add(buttonsPanel, BorderLayout.NORTH); + add(messageLog, BorderLayout.CENTER); + clearLogButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + messageLog.clear(); + } + }); + + pastebinButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + pastebinLog(); + } + }); + } + + /** + * Register the global logger if it hasn't been registered. + */ + private void registerLoggerHandler() { + if (!registeredGlobalLog) { + getMessageLog().registerLoggerHandler(); + registeredGlobalLog = true; + } + } + + /** + * Attempt to perform window close. + */ + protected void performClose() { + messageLog.detachGlobalHandler(); + messageLog.clear(); + registeredGlobalLog = false; + dispose(); + } + + /** + * Send the contents of the message log to a pastebin. + */ + private void pastebinLog() { + String text = messageLog.getPastableText(); + // Not really bytes! + messageLog.log(tr("console.pasteUploading", text.length()), messageLog.asHighlighted()); + + PastebinPoster.paste(text, new PastebinPoster.PasteCallback() { + @Override + public void handleSuccess(String url) { + messageLog.log(tr("console.pasteUploaded", url), messageLog.asHighlighted()); + SwingHelper.openURL(url, messageLog); + } + + @Override + public void handleError(String err) { + messageLog.log(tr("console.pasteFailed", err), messageLog.asError()); + } + }); + } + + public static void showMessages() { + ConsoleFrame frame = globalFrame; + if (frame == null) { + frame = new ConsoleFrame(10000, false); + globalFrame = frame; + frame.setTitle(SharedLocale.tr("console.launcherConsoleTitle")); + frame.registerLoggerHandler(); + frame.setVisible(true); + } else { + frame.setVisible(true); + frame.registerLoggerHandler(); + frame.requestFocus(); + } + } + + public static void hideMessages() { + ConsoleFrame frame = globalFrame; + if (frame != null) { + frame.setVisible(false); + } + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/dialog/FeatureSelectionDialog.java b/launcher/src/main/java/com/skcraft/launcher/dialog/FeatureSelectionDialog.java new file mode 100644 index 0000000..08d40a4 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/dialog/FeatureSelectionDialog.java @@ -0,0 +1,109 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.dialog; + +import com.skcraft.launcher.model.modpack.Feature; +import com.skcraft.launcher.swing.*; +import com.skcraft.launcher.util.SharedLocale; +import lombok.NonNull; + +import javax.swing.*; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.List; + +import static javax.swing.BorderFactory.createEmptyBorder; + +public class FeatureSelectionDialog extends JDialog { + + private final List features; + private final JPanel container = new JPanel(new BorderLayout()); + private final JTextArea descText = new JTextArea(SharedLocale.tr("features.selectForInfo")); + private final JScrollPane descScroll = new JScrollPane(descText); + private final CheckboxTable componentsTable = new CheckboxTable(); + private final JScrollPane componentsScroll = new JScrollPane(componentsTable); + private final JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, componentsScroll, descScroll); + private final LinedBoxPanel buttonsPanel = new LinedBoxPanel(true); + private final JButton installButton = new JButton(SharedLocale.tr("features.install")); + private final Object receiver; + + public FeatureSelectionDialog(Window owner, @NonNull List features, Object receiver) { + super(owner, ModalityType.DOCUMENT_MODAL); + + this.features = features; + this.receiver = receiver; + + setTitle(SharedLocale.tr("features.title")); + initComponents(); + setDefaultCloseOperation(DISPOSE_ON_CLOSE); + setSize(new Dimension(500, 400)); + setResizable(false); + setLocationRelativeTo(owner); + } + + private void initComponents() { + componentsTable.setModel(new FeatureTableModel(features)); + + descScroll.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS); + + descText.setFont(new JLabel().getFont()); + descText.setEditable(false); + descText.setWrapStyleWord(true); + descText.setLineWrap(true); + SwingHelper.removeOpaqueness(descText); + descText.setComponentPopupMenu(TextFieldPopupMenu.INSTANCE); + + splitPane.setDividerLocation(300); + splitPane.setDividerSize(6); + SwingHelper.flattenJSplitPane(splitPane); + + container.setBorder(createEmptyBorder(12, 12, 12, 12)); + container.add(splitPane, BorderLayout.CENTER); + + buttonsPanel.addGlue(); + buttonsPanel.addElement(installButton); + + JLabel descLabel = new JLabel(SharedLocale.tr("features.intro")); + descLabel.setBorder(createEmptyBorder(12, 12, 4, 12)); + + SwingHelper.equalWidth(installButton, new JButton(SharedLocale.tr("button.cancel"))); + + add(descLabel, BorderLayout.NORTH); + add(container, BorderLayout.CENTER); + add(buttonsPanel, BorderLayout.SOUTH); + + componentsTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() { + public void valueChanged(ListSelectionEvent e) { + updateDescription(); + } + }); + + installButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + synchronized (receiver) { + receiver.notifyAll(); + } + FeatureSelectionDialog.this.dispose(); + } + }); + } + + private void updateDescription() { + Feature feature = features.get(componentsTable.getSelectedRow()); + + if (feature != null) { + descText.setText(feature.getDescription()); + } else { + descText.setText(SharedLocale.tr("features.selectForInfo")); + } + } + +} 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 0000000..9518024 --- /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.MemorySettings; +import com.skcraft.launcher.launch.runtime.JavaRuntime; +import com.skcraft.launcher.launch.runtime.JavaRuntimeFinder; +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.jvmRuntime")), 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 new file mode 100644 index 0000000..497d1aa --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/dialog/LauncherFrame.java @@ -0,0 +1,402 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.dialog; + +import com.skcraft.concurrency.ObservableFuture; +import com.skcraft.launcher.Instance; +import com.skcraft.launcher.InstanceList; +import com.skcraft.launcher.Launcher; +import com.skcraft.launcher.launch.LaunchListener; +import com.skcraft.launcher.launch.LaunchOptions; +import com.skcraft.launcher.launch.LaunchOptions.UpdatePolicy; +import com.skcraft.launcher.swing.*; +import com.skcraft.launcher.util.SharedLocale; +import com.skcraft.launcher.util.SwingExecutor; +import lombok.Getter; +import lombok.NonNull; +import lombok.extern.java.Log; +import net.miginfocom.swing.MigLayout; + +import javax.swing.*; +import javax.swing.event.TableModelEvent; +import javax.swing.event.TableModelListener; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseEvent; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.io.File; +import java.lang.ref.WeakReference; + +import static com.skcraft.launcher.util.SharedLocale.tr; + +/** + * The main launcher frame. + */ +@Log +public class LauncherFrame extends JFrame { + + private final Launcher launcher; + + @Getter + private final InstanceTable instancesTable = new InstanceTable(); + private final InstanceTableModel instancesModel; + @Getter + private final JScrollPane instanceScroll = new JScrollPane(instancesTable); + private WebpagePanel webView; + private JSplitPane splitPane; + private final JButton launchButton = new JButton(SharedLocale.tr("launcher.launch")); + private final JButton refreshButton = new JButton(SharedLocale.tr("launcher.checkForUpdates")); + private final JButton optionsButton = new JButton(SharedLocale.tr("launcher.options")); + private final JButton selfUpdateButton = new JButton(SharedLocale.tr("launcher.updateLauncher")); + private final JCheckBox updateCheck = new JCheckBox(SharedLocale.tr("launcher.downloadUpdates")); + + /** + * Create a new frame. + * + * @param launcher the launcher + */ + public LauncherFrame(@NonNull Launcher launcher) { + super(tr("launcher.title", launcher.getVersion())); + + this.launcher = launcher; + instancesModel = new InstanceTableModel(launcher.getInstances()); + + setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); + setMinimumSize(new Dimension(400, 300)); + initComponents(); + pack(); + setLocationRelativeTo(null); + + SwingHelper.setFrameIcon(this, Launcher.class, "icon.png"); + + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + loadInstances(); + } + }); + } + + private void initComponents() { + JPanel container = createContainerPanel(); + container.setLayout(new MigLayout("fill, insets dialog", "[][]push[][]", "[grow][]")); + + webView = createNewsPanel(); + splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, instanceScroll, webView); + selfUpdateButton.setVisible(launcher.getUpdateManager().getPendingUpdate()); + + launcher.getUpdateManager().addPropertyChangeListener(new PropertyChangeListener() { + @Override + public void propertyChange(PropertyChangeEvent evt) { + if (evt.getPropertyName().equals("pendingUpdate")) { + selfUpdateButton.setVisible((Boolean) evt.getNewValue()); + + } + } + }); + + updateCheck.setSelected(true); + instancesTable.setModel(instancesModel); + launchButton.setFont(launchButton.getFont().deriveFont(Font.BOLD)); + splitPane.setDividerLocation(200); + splitPane.setDividerSize(4); + splitPane.setOpaque(false); + container.add(splitPane, "grow, wrap, span 5, gapbottom unrel, w null:680, h null:350"); + SwingHelper.flattenJSplitPane(splitPane); + container.add(refreshButton); + container.add(updateCheck); + container.add(selfUpdateButton); + container.add(optionsButton); + container.add(launchButton); + + add(container, BorderLayout.CENTER); + + instancesModel.addTableModelListener(new TableModelListener() { + @Override + public void tableChanged(TableModelEvent e) { + if (instancesTable.getRowCount() > 0) { + instancesTable.setRowSelectionInterval(0, 0); + } + } + }); + + instancesTable.addMouseListener(new DoubleClickToButtonAdapter(launchButton)); + + refreshButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + loadInstances(); + launcher.getUpdateManager().checkForUpdate(LauncherFrame.this); + webView.browse(launcher.getNewsURL(), false); + } + }); + + selfUpdateButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + launcher.getUpdateManager().performUpdate(LauncherFrame.this); + } + }); + + optionsButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + showOptions(); + } + }); + + launchButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + launch(); + } + }); + + instancesTable.addMouseListener(new PopupMouseAdapter() { + @Override + protected void showPopup(MouseEvent e) { + int index = instancesTable.rowAtPoint(e.getPoint()); + Instance selected = null; + if (index >= 0) { + instancesTable.setRowSelectionInterval(index, index); + selected = launcher.getInstances().get(index); + } + popupInstanceMenu(e.getComponent(), e.getX(), e.getY(), selected); + } + }); + } + + protected JPanel createContainerPanel() { + return new JPanel(); + } + + /** + * Return the news panel. + * + * @return the news panel + */ + protected WebpagePanel createNewsPanel() { + return WebpagePanel.forURL(launcher.getNewsURL(), false); + } + + /** + * Popup the menu for the instances. + * + * @param component the component + * @param x mouse X + * @param y mouse Y + * @param selected the selected instance, possibly null + */ + private void popupInstanceMenu(Component component, int x, int y, final Instance selected) { + JPopupMenu popup = new JPopupMenu(); + JMenuItem menuItem; + + if (selected != null) { + menuItem = new JMenuItem(!selected.isLocal() ? tr("instance.install") : tr("instance.launch")); + menuItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + launch(); + } + }); + popup.add(menuItem); + + if (selected.isLocal()) { + popup.addSeparator(); + + menuItem = new JMenuItem(SharedLocale.tr("instance.openFolder")); + menuItem.addActionListener(ActionListeners.browseDir( + LauncherFrame.this, selected.getContentDir(), true)); + popup.add(menuItem); + + menuItem = new JMenuItem(SharedLocale.tr("instance.openSaves")); + menuItem.addActionListener(ActionListeners.browseDir( + LauncherFrame.this, new File(selected.getContentDir(), "saves"), true)); + popup.add(menuItem); + + menuItem = new JMenuItem(SharedLocale.tr("instance.openResourcePacks")); + menuItem.addActionListener(ActionListeners.browseDir( + LauncherFrame.this, new File(selected.getContentDir(), "resourcepacks"), true)); + popup.add(menuItem); + + menuItem = new JMenuItem(SharedLocale.tr("instance.openScreenshots")); + menuItem.addActionListener(ActionListeners.browseDir( + LauncherFrame.this, new File(selected.getContentDir(), "screenshots"), true)); + popup.add(menuItem); + + menuItem = new JMenuItem(SharedLocale.tr("instance.copyAsPath")); + menuItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + File dir = selected.getContentDir(); + dir.mkdirs(); + SwingHelper.setClipboard(dir.getAbsolutePath()); + } + }); + 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()) { + menuItem = new JMenuItem(SharedLocale.tr("instance.forceUpdate")); + menuItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + selected.setUpdatePending(true); + launch(); + instancesModel.update(); + } + }); + popup.add(menuItem); + } + + menuItem = new JMenuItem(SharedLocale.tr("instance.hardForceUpdate")); + menuItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + confirmHardUpdate(selected); + } + }); + popup.add(menuItem); + + menuItem = new JMenuItem(SharedLocale.tr("instance.deleteFiles")); + menuItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + confirmDelete(selected); + } + }); + popup.add(menuItem); + } + + popup.addSeparator(); + } + + menuItem = new JMenuItem(SharedLocale.tr("launcher.refreshList")); + menuItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + loadInstances(); + } + }); + popup.add(menuItem); + + popup.show(component, x, y); + + } + + private void confirmDelete(Instance instance) { + if (!SwingHelper.confirmDialog(this, + tr("instance.confirmDelete", instance.getTitle()), SharedLocale.tr("confirmTitle"))) { + return; + } + + ObservableFuture future = launcher.getInstanceTasks().delete(this, instance); + + // Update the list of instances after updating + future.addListener(new Runnable() { + @Override + public void run() { + loadInstances(); + } + }, SwingExecutor.INSTANCE); + } + + private void confirmHardUpdate(Instance instance) { + if (!SwingHelper.confirmDialog(this, SharedLocale.tr("instance.confirmHardUpdate"), SharedLocale.tr("confirmTitle"))) { + return; + } + + ObservableFuture future = launcher.getInstanceTasks().hardUpdate(this, instance); + + // Update the list of instances after updating + future.addListener(new Runnable() { + @Override + public void run() { + launch(); + instancesModel.update(); + } + }, SwingExecutor.INSTANCE); + } + + private void loadInstances() { + ObservableFuture future = launcher.getInstanceTasks().reloadInstances(this); + + future.addListener(new Runnable() { + @Override + public void run() { + instancesModel.update(); + if (instancesTable.getRowCount() > 0) { + instancesTable.setRowSelectionInterval(0, 0); + } + requestFocus(); + } + }, SwingExecutor.INSTANCE); + + ProgressDialog.showProgress(this, future, SharedLocale.tr("launcher.checkingTitle"), SharedLocale.tr("launcher.checkingStatus")); + SwingHelper.addErrorDialogCallback(this, future); + } + + private void showOptions() { + ConfigurationDialog configDialog = new ConfigurationDialog(this, launcher); + configDialog.setVisible(true); + } + + private void launch() { + boolean permitUpdate = updateCheck.isSelected(); + Instance instance = launcher.getInstances().get(instancesTable.getSelectedRow()); + + LaunchOptions options = new LaunchOptions.Builder() + .setInstance(instance) + .setListener(new LaunchListenerImpl(this)) + .setUpdatePolicy(permitUpdate ? UpdatePolicy.UPDATE_IF_SESSION_ONLINE : UpdatePolicy.NO_UPDATE) + .setWindow(this) + .build(); + launcher.getLaunchSupervisor().launch(options); + } + + private static class LaunchListenerImpl implements LaunchListener { + private final WeakReference frameRef; + private final Launcher launcher; + + private LaunchListenerImpl(LauncherFrame frame) { + this.frameRef = new WeakReference(frame); + this.launcher = frame.launcher; + } + + @Override + public void instancesUpdated() { + LauncherFrame frame = frameRef.get(); + if (frame != null) { + frame.instancesModel.update(); + } + } + + @Override + public void gameStarted() { + LauncherFrame frame = frameRef.get(); + if (frame != null) { + frame.dispose(); + } + } + + @Override + public void gameClosed() { + Window newLauncherWindow = launcher.showLauncherWindow(); + launcher.getUpdateManager().checkForUpdate(newLauncherWindow); + } + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/dialog/LoginDialog.java b/launcher/src/main/java/com/skcraft/launcher/dialog/LoginDialog.java new file mode 100644 index 0000000..e99e050 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/dialog/LoginDialog.java @@ -0,0 +1,204 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.dialog; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.skcraft.concurrency.ObservableFuture; +import com.skcraft.concurrency.ProgressObservable; +import com.skcraft.launcher.Configuration; +import com.skcraft.launcher.Launcher; +import com.skcraft.launcher.auth.AuthenticationException; +import com.skcraft.launcher.auth.Session; +import com.skcraft.launcher.auth.YggdrasilLoginService; +import com.skcraft.launcher.persistence.Persistence; +import com.skcraft.launcher.swing.*; +import com.skcraft.launcher.util.SharedLocale; +import com.skcraft.launcher.util.SwingExecutor; +import lombok.Data; +import lombok.Getter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.io.IOException; +import java.util.Optional; +import java.util.concurrent.Callable; + +/** + * The login dialog. + */ +public class LoginDialog extends JDialog { + + private final Launcher launcher; + @Getter private Session session; + + private final JLabel message = new JLabel(SharedLocale.tr("login.defaultMessage")); + private final JTextField usernameText = new JTextField(); + private final JPasswordField passwordText = new JPasswordField(); + private final JButton loginButton = new JButton(SharedLocale.tr("login.login")); + private final LinkButton recoverButton = new LinkButton(SharedLocale.tr("login.recoverAccount")); + private final JButton cancelButton = new JButton(SharedLocale.tr("button.cancel")); + private final FormPanel formPanel = new FormPanel(); + private final LinedBoxPanel buttonsPanel = new LinedBoxPanel(true); + + /** + * Create a new login dialog. + * + * @param owner the owner + * @param launcher the launcher + */ + public LoginDialog(Window owner, @NonNull Launcher launcher, Optional reloginDetails) { + super(owner, ModalityType.DOCUMENT_MODAL); + + this.launcher = launcher; + + setTitle(SharedLocale.tr("login.title")); + initComponents(); + setDefaultCloseOperation(DISPOSE_ON_CLOSE); + setMinimumSize(new Dimension(420, 0)); + setResizable(false); + pack(); + setLocationRelativeTo(owner); + + setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); + addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent event) { + dispose(); + } + }); + + reloginDetails.ifPresent(details -> message.setText(details.message)); + } + + @SuppressWarnings("unchecked") + private void initComponents() { + usernameText.setEditable(true); + + loginButton.setFont(loginButton.getFont().deriveFont(Font.BOLD)); + + formPanel.addRow(message); + formPanel.addRow(new JLabel(SharedLocale.tr("login.idEmail")), usernameText); + formPanel.addRow(new JLabel(SharedLocale.tr("login.password")), passwordText); + buttonsPanel.setBorder(BorderFactory.createEmptyBorder(26, 13, 13, 13)); + + buttonsPanel.addElement(recoverButton); + buttonsPanel.addGlue(); + buttonsPanel.addElement(loginButton); + buttonsPanel.addElement(cancelButton); + + add(formPanel, BorderLayout.CENTER); + add(buttonsPanel, BorderLayout.SOUTH); + + getRootPane().setDefaultButton(loginButton); + + passwordText.setComponentPopupMenu(TextFieldPopupMenu.INSTANCE); + + recoverButton.addActionListener( + ActionListeners.openURL(recoverButton, launcher.getProperties().getProperty("resetPasswordUrl"))); + + loginButton.addActionListener(e -> prepareLogin()); + cancelButton.addActionListener(e -> dispose()); + } + + @SuppressWarnings("deprecation") + private void prepareLogin() { + if (!usernameText.getText().isEmpty()) { + String password = passwordText.getText(); + + if (password == null || password.isEmpty()) { + SwingHelper.showErrorDialog(this, SharedLocale.tr("login.noPasswordError"), SharedLocale.tr("login.noPasswordTitle")); + } else { + attemptLogin(usernameText.getText(), password); + } + } else { + SwingHelper.showErrorDialog(this, SharedLocale.tr("login.noLoginError"), SharedLocale.tr("login.noLoginTitle")); + } + } + + private void attemptLogin(String username, String password) { + LoginCallable callable = new LoginCallable(username, password); + ObservableFuture future = new ObservableFuture( + launcher.getExecutor().submit(callable), callable); + + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(Session result) { + setResult(result); + } + + @Override + public void onFailure(Throwable t) { + } + }, SwingExecutor.INSTANCE); + + ProgressDialog.showProgress(this, future, SharedLocale.tr("login.loggingInTitle"), SharedLocale.tr("login.loggingInStatus")); + SwingHelper.addErrorDialogCallback(this, future); + } + + private void setResult(Session session) { + this.session = session; + dispose(); + } + + public static Session showLoginRequest(Window owner, Launcher launcher) { + return showLoginRequest(owner, launcher, null); + } + + public static Session showLoginRequest(Window owner, Launcher launcher, ReloginDetails reloginDetails) { + LoginDialog dialog = new LoginDialog(owner, launcher, Optional.ofNullable(reloginDetails)); + dialog.setVisible(true); + return dialog.getSession(); + } + + @RequiredArgsConstructor + private class LoginCallable implements Callable, ProgressObservable { + private final String username; + private final String password; + + @Override + public Session call() throws AuthenticationException, IOException, InterruptedException { + YggdrasilLoginService service = launcher.getYggdrasil(); + Session identity = service.login(username, password); + + // The presence of the identity (profile in Mojang terms) corresponds to whether the account + // owns the game, so we need to check that + if (identity != null) { + // Set offline enabled flag to true + Configuration config = launcher.getConfig(); + if (!config.isOfflineEnabled()) { + config.setOfflineEnabled(true); + Persistence.commitAndForget(config); + } + + return identity; + } else { + throw new AuthenticationException("Minecraft not owned", SharedLocale.tr("login.minecraftNotOwnedError")); + } + } + + @Override + public double getProgress() { + return -1; + } + + @Override + public String getStatus() { + return SharedLocale.tr("login.loggingInStatus"); + } + } + + @Data + public static class ReloginDetails { + private final String username; + private final String message; + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/dialog/ProcessConsoleFrame.java b/launcher/src/main/java/com/skcraft/launcher/dialog/ProcessConsoleFrame.java new file mode 100644 index 0000000..73bcd88 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/dialog/ProcessConsoleFrame.java @@ -0,0 +1,235 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.dialog; + +import com.skcraft.launcher.swing.LinedBoxPanel; +import com.skcraft.launcher.swing.SwingHelper; +import com.skcraft.launcher.util.SharedLocale; +import lombok.Getter; +import lombok.Setter; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.PrintWriter; + +import static com.skcraft.launcher.util.SharedLocale.tr; + +/** + * A version of the console window that can manage a process. + */ +public class ProcessConsoleFrame extends ConsoleFrame { + + private JButton killButton; + private JButton minimizeButton; + private TrayIcon trayIcon; + + @Getter private Process process; + @Getter @Setter private boolean killOnClose; + + private PrintWriter processOut; + + /** + * Create a new instance of the frame. + * + * @param numLines the number of log lines + * @param colorEnabled whether color is enabled in the log + */ + public ProcessConsoleFrame(int numLines, boolean colorEnabled) { + super(SharedLocale.tr("console.title"), numLines, colorEnabled); + processOut = new PrintWriter( + getMessageLog().getOutputStream(new Color(0, 0, 255)), true); + initComponents(); + updateComponents(); + } + + /** + * Track the given process. + * + * @param process the process + */ + public synchronized void setProcess(Process process) { + try { + Process lastProcess = this.process; + if (lastProcess != null) { + processOut.println(tr("console.processEndCode", lastProcess.exitValue())); + } + } catch (IllegalThreadStateException e) { + } + + if (process != null) { + processOut.println(SharedLocale.tr("console.attachedToProcess")); + } + + this.process = process; + + SwingUtilities.invokeLater(new Runnable() { + public void run() { + updateComponents(); + } + }); + } + + private synchronized boolean hasProcess() { + return process != null; + } + + @Override + protected void performClose() { + if (hasProcess()) { + if (killOnClose) { + performKill(); + } + } + + if (trayIcon != null) { + SystemTray.getSystemTray().remove(trayIcon); + } + + super.performClose(); + } + + private void performKill() { + if (!confirmKill()) { + return; + } + + synchronized (this) { + if (hasProcess()) { + process.destroy(); + setProcess(null); + } + } + + updateComponents(); + } + + protected void initComponents() { + killButton = new JButton(SharedLocale.tr("console.forceClose")); + minimizeButton = new JButton(); // Text set later + + LinedBoxPanel buttonsPanel = getButtonsPanel(); + buttonsPanel.addGlue(); + buttonsPanel.addElement(killButton); + buttonsPanel.addElement(minimizeButton); + + killButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + performKill(); + } + }); + + minimizeButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + contextualClose(); + } + }); + + if (!setupTrayIcon()) { + minimizeButton.setEnabled(true); + } + } + + private boolean setupTrayIcon() { + if (!SystemTray.isSupported()) { + return false; + } + + trayIcon = new TrayIcon(getTrayRunningIcon()); + trayIcon.setImageAutoSize(true); + trayIcon.setToolTip(SharedLocale.tr("console.trayTooltip")); + + trayIcon.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + reshow(); + } + }); + + PopupMenu popup = new PopupMenu(); + MenuItem item; + + popup.add(item = new MenuItem(SharedLocale.tr("console.trayTitle"))); + item.setEnabled(false); + + popup.add(item = new MenuItem(SharedLocale.tr("console.tray.showWindow"))); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + reshow(); + } + }); + + popup.add(item = new MenuItem(SharedLocale.tr("console.tray.forceClose"))); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + performKill(); + } + }); + + trayIcon.setPopupMenu(popup); + + try { + SystemTray tray = SystemTray.getSystemTray(); + tray.add(trayIcon); + return true; + } catch (AWTException e) { + } + + return false; + } + + private synchronized void updateComponents() { + Image icon = hasProcess() ? getTrayRunningIcon() : getTrayClosedIcon(); + + killButton.setEnabled(hasProcess()); + + if (!hasProcess() || trayIcon == null) { + minimizeButton.setText(SharedLocale.tr("console.closeWindow")); + } else { + minimizeButton.setText(SharedLocale.tr("console.hideWindow")); + } + + if (trayIcon != null) { + trayIcon.setImage(icon); + } + + setIconImage(icon); + } + + private synchronized void contextualClose() { + if (!hasProcess() || trayIcon == null) { + performClose(); + } else { + minimize(); + } + + updateComponents(); + } + + private boolean confirmKill() { + if (System.getProperty("skcraftLauncher.killWithoutConfirm", "false").equalsIgnoreCase("true")) { + return true; + } else { + return SwingHelper.confirmDialog(this, SharedLocale.tr("console.confirmKill"), SharedLocale.tr("console.confirmKillTitle")); + } + } + + private void minimize() { + setVisible(false); + } + + private void reshow() { + setVisible(true); + requestFocus(); + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/dialog/ProgressDialog.java b/launcher/src/main/java/com/skcraft/launcher/dialog/ProgressDialog.java new file mode 100644 index 0000000..4fc027d --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/dialog/ProgressDialog.java @@ -0,0 +1,250 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.dialog; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.skcraft.concurrency.ObservableFuture; +import com.skcraft.concurrency.ProgressObservable; +import com.skcraft.launcher.swing.LinedBoxPanel; +import com.skcraft.launcher.swing.SwingHelper; +import com.skcraft.launcher.util.SharedLocale; +import com.skcraft.launcher.util.SwingExecutor; +import lombok.extern.java.Log; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.lang.ref.WeakReference; +import java.util.Timer; +import java.util.TimerTask; + +import static com.skcraft.launcher.util.SharedLocale.tr; + +@Log +public class ProgressDialog extends JDialog { + + private static WeakReference lastDialogRef; + + private final String defaultTitle; + private final String defaultMessage; + private final JLabel label = new JLabel(); + private final JPanel progressPanel = new JPanel(new BorderLayout(0, 5)); + private final JPanel textAreaPanel = new JPanel(new BorderLayout()); + private final JProgressBar progressBar = new JProgressBar(); + private final LinedBoxPanel buttonsPanel = new LinedBoxPanel(true); + private final JTextArea logText = new JTextArea(); + private final JScrollPane logScroll = new JScrollPane(logText); + private final JButton detailsButton = new JButton(); + private final JButton logButton = new JButton(SharedLocale.tr("progress.viewLog")); + private final JButton cancelButton = new JButton(SharedLocale.tr("button.cancel")); + + public ProgressDialog(Window owner, String title, String message) { + super(owner, title, ModalityType.DOCUMENT_MODAL); + + setResizable(true); + initComponents(); + label.setText(message); + defaultTitle = title; + defaultMessage = message; + setCompactSize(); + setLocationRelativeTo(owner); + + setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); + addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent event) { + if (confirmCancel()) { + cancel(); + dispose(); + } + } + }); + } + + private void setCompactSize() { + detailsButton.setText(SharedLocale.tr("progress.details")); + logButton.setVisible(false); + setMinimumSize(new Dimension(400, 100)); + pack(); + } + + private void setDetailsSize() { + detailsButton.setText(SharedLocale.tr("progress.less")); + logButton.setVisible(true); + setSize(400, 350); + } + + private void initComponents() { + progressBar.setMaximum(1000); + progressBar.setMinimum(0); + progressBar.setIndeterminate(true); + progressBar.setPreferredSize(new Dimension(0, 18)); + + buttonsPanel.addElement(detailsButton); + buttonsPanel.addElement(logButton); + buttonsPanel.addGlue(); + buttonsPanel.addElement(cancelButton); + buttonsPanel.setBorder(BorderFactory.createEmptyBorder(30, 13, 13, 13)); + + logScroll.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS); + logText.setBackground(getBackground()); + logText.setEditable(false); + logText.setLineWrap(true); + logText.setWrapStyleWord(false); + logText.setFont(new JLabel().getFont()); + + progressPanel.add(label, BorderLayout.NORTH); + progressPanel.setBorder(BorderFactory.createEmptyBorder(13, 13, 0, 13)); + progressPanel.add(progressBar, BorderLayout.CENTER); + textAreaPanel.setBorder(BorderFactory.createEmptyBorder(10, 13, 0, 13)); + textAreaPanel.add(logScroll, BorderLayout.CENTER); + + add(progressPanel, BorderLayout.NORTH); + add(textAreaPanel, BorderLayout.CENTER); + add(buttonsPanel, BorderLayout.SOUTH); + + textAreaPanel.setVisible(false); + cancelButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + if (confirmCancel()) { + cancel(); + dispose(); + } + } + }); + + detailsButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + toggleDetails(); + } + }); + + logButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + ConsoleFrame.showMessages(); + } + }); + } + + private boolean confirmCancel() { + return SwingHelper.confirmDialog(this, SharedLocale.tr("progress.confirmCancel"), SharedLocale.tr("progress.confirmCancelTitle")); + } + + protected void cancel() { + } + + private void toggleDetails() { + if (textAreaPanel.isVisible()) { + textAreaPanel.setVisible(false); + setCompactSize(); + } else { + textAreaPanel.setVisible(true); + setDetailsSize(); + } + setLocationRelativeTo(getOwner()); + } + + public static void showProgress(final Window owner, final ObservableFuture future, String title, String message) { + showProgress(owner, future, future, title, message); + } + + public static void showProgress(final Window owner, final ListenableFuture future, ProgressObservable observable, String title, String message) { + final ProgressDialog dialog = new ProgressDialog(owner, title, message) { + @Override + protected void cancel() { + future.cancel(true); + } + }; + + lastDialogRef = new WeakReference(dialog); + + final Timer timer = new Timer(); + timer.scheduleAtFixedRate(new UpdateProgress(dialog, observable), 400, 400); + + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(Object result) { + timer.cancel(); + dialog.dispose(); + } + + @Override + public void onFailure(Throwable t) { + timer.cancel(); + dialog.dispose(); + } + }, SwingExecutor.INSTANCE); + + dialog.setVisible(true); + } + + public static ProgressDialog getLastDialog() { + WeakReference ref = lastDialogRef; + if (ref != null) { + return ref.get(); + } + + return null; + } + + private static class UpdateProgress extends TimerTask { + private final ProgressDialog dialog; + private final ProgressObservable observable; + + public UpdateProgress(ProgressDialog dialog, ProgressObservable observable) { + this.dialog = dialog; + this.observable = observable; + } + + @Override + public void run() { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + JProgressBar progressBar = dialog.progressBar; + JTextArea logText = dialog.logText; + JLabel label = dialog.label; + + double progress = observable.getProgress(); + if (progress >= 0) { + dialog.setTitle(tr("progress.percentTitle", + Math.round(progress * 100 * 100) / 100.0, dialog.defaultTitle)); + progressBar.setValue((int) (progress * 1000)); + progressBar.setIndeterminate(false); + } else { + dialog.setTitle( dialog.defaultTitle); + progressBar.setIndeterminate(true); + } + + String status = observable.getStatus(); + if (status == null) { + status = SharedLocale.tr("progress.defaultStatus"); + label.setText(dialog.defaultMessage); + } else { + int index = status.indexOf('\n'); + if (index == -1) { + label.setText(status); + } else { + label.setText(status.substring(0, index)); + } + } + logText.setText(status); + logText.setCaretPosition(0); + } + }); + } + } + +} 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 0000000..b9bb22d --- /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/dialog/component/ListListenerReducer.java b/launcher/src/main/java/com/skcraft/launcher/dialog/component/ListListenerReducer.java new file mode 100644 index 0000000..be6ef09 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/dialog/component/ListListenerReducer.java @@ -0,0 +1,34 @@ +package com.skcraft.launcher.dialog.component; + +import com.google.common.collect.Lists; + +import javax.swing.event.ListDataEvent; +import javax.swing.event.ListDataListener; +import java.util.List; + +public class ListListenerReducer implements ListDataListener { + private final List listeners = Lists.newArrayList(); + + @Override + public void intervalAdded(ListDataEvent e) { + listeners.forEach(it -> it.intervalAdded(e)); + } + + @Override + public void intervalRemoved(ListDataEvent e) { + listeners.forEach(it -> it.intervalRemoved(e)); + } + + @Override + public void contentsChanged(ListDataEvent e) { + listeners.forEach(it -> it.contentsChanged(e)); + } + + public void addListDataListener(ListDataListener l) { + listeners.add(l); + } + + public void removeListDataListener(ListDataListener l) { + listeners.remove(l); + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/install/Downloader.java b/launcher/src/main/java/com/skcraft/launcher/install/Downloader.java new file mode 100644 index 0000000..137daf1 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/install/Downloader.java @@ -0,0 +1,21 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.install; + +import com.skcraft.concurrency.ProgressObservable; + +import java.io.File; +import java.net.URL; +import java.util.List; + + +public interface Downloader extends ProgressObservable { + + File download(List urls, String key, long size, String name); + + File download(URL url, String key, long size, String name); +} diff --git a/launcher/src/main/java/com/skcraft/launcher/install/FeatureCache.java b/launcher/src/main/java/com/skcraft/launcher/install/FeatureCache.java new file mode 100644 index 0000000..052ed5d --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/install/FeatureCache.java @@ -0,0 +1,19 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.install; + +import lombok.Data; + +import java.util.HashMap; +import java.util.Map; + +@Data +public class FeatureCache { + + private Map selected = new HashMap(); + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/install/FileCopy.java b/launcher/src/main/java/com/skcraft/launcher/install/FileCopy.java new file mode 100644 index 0000000..49eb68b --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/install/FileCopy.java @@ -0,0 +1,48 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.install; + +import com.google.common.io.Files; +import com.skcraft.launcher.Launcher; +import lombok.NonNull; +import lombok.extern.java.Log; + +import java.io.File; +import java.io.IOException; +import java.util.logging.Level; + +import static com.skcraft.launcher.util.SharedLocale.tr; + +@Log +public class FileCopy implements InstallTask { + + private final File from; + private final File to; + + public FileCopy(@NonNull File from, @NonNull File to) { + this.from = from; + this.to = to; + } + + @Override + public void execute(Launcher launcher) throws IOException { + log.log(Level.INFO, "Copying to {0} (from {1})...", new Object[]{to.getAbsoluteFile(), from.getName()}); + to.getParentFile().mkdirs(); + Files.copy(from, to); + } + + @Override + public double getProgress() { + return -1; + } + + @Override + public String getStatus() { + return tr("installer.copyingFile", from, to); + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/install/FileMover.java b/launcher/src/main/java/com/skcraft/launcher/install/FileMover.java new file mode 100644 index 0000000..6943129 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/install/FileMover.java @@ -0,0 +1,48 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.install; + +import com.skcraft.launcher.Launcher; +import lombok.NonNull; +import lombok.extern.java.Log; + +import java.io.File; +import java.io.IOException; +import java.util.logging.Level; + +import static com.skcraft.launcher.util.SharedLocale.tr; + +@Log +public class FileMover implements InstallTask { + + private final File from; + private final File to; + + public FileMover(@NonNull File from, @NonNull File to) { + this.from = from; + this.to = to; + } + + @Override + public void execute(Launcher launcher) throws IOException { + log.log(Level.INFO, "Moving to {0} (from {1})...", new Object[]{to.getAbsoluteFile(), from.getName()}); + to.getParentFile().mkdirs(); + to.delete(); + from.renameTo(to); + } + + @Override + public double getProgress() { + return -1; + } + + @Override + public String getStatus() { + return tr("installer.movingFile", from, to); + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/install/FileVerify.java b/launcher/src/main/java/com/skcraft/launcher/install/FileVerify.java new file mode 100644 index 0000000..d8a313a --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/install/FileVerify.java @@ -0,0 +1,42 @@ +package com.skcraft.launcher.install; + +import com.skcraft.launcher.Launcher; +import com.skcraft.launcher.LauncherException; +import com.skcraft.launcher.util.FileUtils; +import lombok.RequiredArgsConstructor; +import lombok.extern.java.Log; + +import java.io.File; + +@RequiredArgsConstructor +@Log +public class FileVerify implements InstallTask { + private final File target; + private final String name; + private final String hash; + + @Override + public void execute(Launcher launcher) throws Exception { + log.info("Verifying file " + name); + + String actualHash = FileUtils.getShaHash(target); + if (!actualHash.equals(hash)) { + String message = String.format( + "File %s (%s) is corrupt (invalid hash)\n" + + "Expected '%s'\nGot '%s'", + name, target.getAbsolutePath(), hash, actualHash); + + throw new LauncherException(message, message); + } + } + + @Override + public double getProgress() { + return -1; + } + + @Override + public String getStatus() { + return "Verifying " + name; + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/install/HttpDownloader.java b/launcher/src/main/java/com/skcraft/launcher/install/HttpDownloader.java new file mode 100644 index 0000000..ccc3f47 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/install/HttpDownloader.java @@ -0,0 +1,299 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.install; + +import com.google.common.base.Charsets; +import com.google.common.base.Strings; +import com.google.common.hash.HashFunction; +import com.google.common.hash.Hashing; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import com.skcraft.concurrency.ProgressObservable; +import com.skcraft.launcher.util.HttpRequest; +import com.skcraft.launcher.util.SharedLocale; +import lombok.Getter; +import lombok.NonNull; +import lombok.Setter; +import lombok.extern.java.Log; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.*; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.logging.Level; + +import static com.skcraft.launcher.util.SharedLocale.tr; + +@Log +public class HttpDownloader implements Downloader { + + private final Random random = new Random(); + private final HashFunction hf = Hashing.sha1(); + + private final File tempDir; + @Getter @Setter private int threadCount = 6; + @Getter @Setter private int retryDelay = 2000; + @Getter @Setter private int tryCount = 3; + + private List queue = new ArrayList(); + private final Set usedKeys = new HashSet(); + + private final List running = new ArrayList(); + private final List failed = new ArrayList(); + private long downloaded = 0; + private long total = 0; + private int left = 0; + + /** + * Create a new downloader using the given executor. + * + * @param tempDir the temporary directory + */ + public HttpDownloader(@NonNull File tempDir) { + this.tempDir = tempDir; + } + + /** + * Make sure that we aren't re-using hash IDs. + * + * @param baseKey the key to make unique + * @return a unique key + */ + private String createUniqueKey(String baseKey) { + String key = baseKey; + int i = 0; + while (usedKeys.contains(key)) { + key = baseKey + "_" + (i++); + } + usedKeys.add(key); + return key; + } + + @Override + public synchronized File download(@NonNull List urls, @NonNull String key, long size, String name) { + if (urls.isEmpty()) { + throw new IllegalArgumentException("Can't download empty list of URLs"); + } + + String hash = hf.hashString(Strings.nullToEmpty(key) + urls.get(0), Charsets.UTF_8).toString(); + hash = createUniqueKey(hash); + File tempFile = new File(tempDir, hash.substring(0, 2) + "/" + hash); + + // If the file is already downloaded (such as from before), then don't re-download + if (!tempFile.exists()) { + total += size; + left++; + queue.add(new HttpDownloadJob(tempFile, urls, size, name != null ? name : tempFile.getName())); + } + + return tempFile; + } + + + @Override + public File download(URL url, String key, long size, String name) { + List urls = new ArrayList(); + urls.add(url); + return download(urls, key, size, name); + } + + /** + * Prevent further downloads from being queued and download queued files. + * + * @throws InterruptedException thrown on interruption + * @throws IOException thrown on I/O error + */ + public void execute() throws InterruptedException, IOException { + synchronized (this) { + queue = Collections.unmodifiableList(queue); + } + + ListeningExecutorService executor = MoreExecutors.listeningDecorator( + Executors.newFixedThreadPool(threadCount)); + + try { + List> futures = new ArrayList>(); + + synchronized (this) { + for (HttpDownloadJob job : queue) { + futures.add(executor.submit(job)); + } + } + + try { + Futures.allAsList(futures).get(); + } catch (ExecutionException e) { + throw new IOException("Something went wrong", e); + } + + synchronized (this) { + if (failed.size() > 0) { + throw new IOException(failed.size() + " file(s) could not be downloaded"); + } + } + } finally { + executor.shutdownNow(); + } + } + + @Override + public synchronized double getProgress() { + if (total <= 0) { + return -1; + } + + long downloaded = this.downloaded; + for (HttpDownloadJob job : running) { + downloaded += Math.max(0, job.getProgress() * job.size); + } + return downloaded / (double) total; + } + + @Override + public synchronized String getStatus() { + String failMessage = tr("downloader.failedCount", failed.size()); + if (running.size() == 1) { + return tr("downloader.downloadingItem", running.get(0).getName()) + + "\n" + running.get(0).getStatus() + + "\n" + failMessage; + } else if (running.size() > 0) { + StringBuilder builder = new StringBuilder(); + for (HttpDownloadJob job : running) { + builder.append("\n"); + builder.append(job.getStatus()); + } + return tr("downloader.downloadingList", queue.size(), left, failed.size()) + + builder.toString() + + "\n" + failMessage; + } else { + return SharedLocale.tr("downloader.noDownloads"); + } + } + + public class HttpDownloadJob implements Runnable, ProgressObservable { + private final File destFile; + private final List urls; + private final long size; + @Getter private String name; + private HttpRequest request; + + private HttpDownloadJob(File destFile, List urls, long size, String name) { + this.destFile = destFile; + this.urls = urls; + this.size = size; + this.name = name; + } + + @Override + public void run() { + try { + synchronized (HttpDownloader.this) { + running.add(this); + } + + download(); + + synchronized (HttpDownloader.this) { + downloaded += size; + } + } catch (IOException e) { + synchronized (HttpDownloader.this) { + failed.add(this); + } + } catch (InterruptedException e) { + log.info("Download of " + destFile + " was interrupted"); + } finally { + synchronized (HttpDownloader.this) { + left--; + running.remove(this); + } + } + } + + private void download() throws IOException, InterruptedException { + log.log(Level.INFO, "Downloading " + destFile + " from " + urls); + + File destDir = destFile.getParentFile(); + File tempFile = new File(destDir, destFile.getName() + ".tmp"); + destDir.mkdirs(); + + // Try to download + download(tempFile); + + destFile.delete(); + if (!tempFile.renameTo(destFile)) { + throw new IOException(String.format("Failed to rename %s to %s", tempFile, destFile)); + } + } + + private void download(File file) throws IOException, InterruptedException { + int trial = 0; + boolean first = true; + IOException lastException = null; + + do { + for (URL url : urls) { + // Sleep between each trial + if (!first) { + Thread.sleep((long) (retryDelay / 2 + (random.nextDouble() * retryDelay))); + } + first = false; + + try { + tryDownloadFrom(url, file, null, 0); + return; + } catch (IOException e) { + lastException = e; + } + } + } while (++trial < tryCount); + + throw new IOException("Failed to download from " + urls, lastException); + } + + private void tryDownloadFrom(URL url, File file, HttpRequest.PartialDownloadInfo retryDetails, int tries) + throws InterruptedException, IOException { + try { + request = HttpRequest.get(url); + request.setResumeInfo(retryDetails).execute().expectResponseCode(200).saveContent(file); + } catch (IOException e) { + log.log(Level.WARNING, "Failed to download " + url, e); + + // We only want to try to resume a partial download if the request succeeded before + // throwing an exception halfway through. If it didn't succeed, just throw the error. + if (tries >= tryCount || !request.isConnected() || !request.isSuccessCode()) { + throw e; + } + + Optional byteRangeSupport = request.canRetryPartial(); + if (byteRangeSupport.isPresent()) { + tryDownloadFrom(url, file, byteRangeSupport.get(), tries + 1); + } + } + } + + @Override + public double getProgress() { + HttpRequest request = this.request; + return request != null ? request.getProgress() : -1; + } + + @Override + public String getStatus() { + double progress = getProgress(); + if (progress >= 0) { + return tr("downloader.jobProgress", name, Math.round(progress * 100 * 100) / 100.0); + } else { + return tr("downloader.jobPending", name); + } + } + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/install/InstallExtras.java b/launcher/src/main/java/com/skcraft/launcher/install/InstallExtras.java new file mode 100644 index 0000000..a6e3b69 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/install/InstallExtras.java @@ -0,0 +1,17 @@ +package com.skcraft.launcher.install; + +import com.skcraft.launcher.model.loader.LocalLoader; +import lombok.Data; + +import java.io.File; +import java.util.HashMap; + +@Data +public class InstallExtras { + private final File contentDir; + private final HashMap loaders; + + public LocalLoader getLoader(String key) { + return loaders.get(key); + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/install/InstallLog.java b/launcher/src/main/java/com/skcraft/launcher/install/InstallLog.java new file mode 100644 index 0000000..7398200 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/install/InstallLog.java @@ -0,0 +1,87 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.install; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; +import lombok.NonNull; + +import java.io.File; +import java.net.URI; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Data +public class InstallLog { + + @JsonIgnore + private File baseDir; + private Map> entries = new HashMap>(); + @JsonIgnore + private Set cache = new HashSet(); + + public synchronized void add(@NonNull String group, @NonNull String entry) { + cache.add(entry); + Set subEntries = entries.get(group); + if (subEntries == null) { + subEntries = new HashSet(); + entries.put(group, subEntries); + } + subEntries.add(entry); + } + + public synchronized void add(@NonNull File group, @NonNull File entry) { + add(relativize(group), relativize(entry)); + } + + public synchronized boolean has(@NonNull String entry) { + return cache.contains(entry); + } + + public synchronized boolean has(@NonNull File entry) { + return has(relativize(entry)); + } + + public synchronized boolean copyGroupFrom(InstallLog other, String group) { + Set otherSet = other.entries.get(group); + if (otherSet == null) { + return false; + } + for (String entry : otherSet) { + add(group, entry); + } + return true; + } + + public synchronized boolean copyGroupFrom(@NonNull InstallLog other, @NonNull File entry) { + return copyGroupFrom(other, relativize(entry)); + } + + @JsonIgnore + public synchronized Set>> getEntrySet() { + return entries.entrySet(); + } + + public synchronized boolean hasGroup(String group) { + return entries.containsKey(group); + } + + private String relativize(File child) { + checkNotNull(baseDir); + URI uri = child.toURI(); + String relative = baseDir.toURI().relativize(uri).getPath(); + if (relative.equals(uri.toString())) { + throw new IllegalArgumentException("Child path not in base"); + } + return relative; + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/install/InstallLogFileMover.java b/launcher/src/main/java/com/skcraft/launcher/install/InstallLogFileMover.java new file mode 100644 index 0000000..5464c2a --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/install/InstallLogFileMover.java @@ -0,0 +1,51 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.install; + +import com.skcraft.launcher.Launcher; +import lombok.NonNull; +import lombok.extern.java.Log; + +import java.io.File; +import java.io.IOException; +import java.util.logging.Level; + +import static com.skcraft.launcher.util.SharedLocale.tr; + +@Log +public class InstallLogFileMover implements InstallTask { + + private final InstallLog installLog; + private final File from; + private final File to; + + public InstallLogFileMover(InstallLog installLog, @NonNull File from, @NonNull File to) { + this.installLog = installLog; + this.from = from; + this.to = to; + } + + @Override + public void execute(Launcher launcher) throws IOException { + InstallLogFileMover.log.log(Level.INFO, "Installing to {0} (from {1})...", new Object[]{to.getAbsoluteFile(), from.getName()}); + to.getParentFile().mkdirs(); + to.delete(); + from.renameTo(to); + installLog.add(to, to); + } + + @Override + public double getProgress() { + return -1; + } + + @Override + public String getStatus() { + return tr("installer.movingFile", from, to); + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/install/InstallTask.java b/launcher/src/main/java/com/skcraft/launcher/install/InstallTask.java new file mode 100644 index 0000000..2f7d5ab --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/install/InstallTask.java @@ -0,0 +1,16 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.install; + +import com.skcraft.concurrency.ProgressObservable; +import com.skcraft.launcher.Launcher; + +public interface InstallTask extends ProgressObservable { + + void execute(Launcher launcher) throws Exception; + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/install/Installer.java b/launcher/src/main/java/com/skcraft/launcher/install/Installer.java new file mode 100644 index 0000000..22e224a --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/install/Installer.java @@ -0,0 +1,117 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.install; + +import com.skcraft.concurrency.ProgressObservable; +import com.skcraft.launcher.Launcher; +import com.skcraft.launcher.util.SharedLocale; +import lombok.Getter; +import lombok.NonNull; +import lombok.extern.java.Log; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static com.skcraft.launcher.LauncherUtils.checkInterrupted; +import static com.skcraft.launcher.util.SharedLocale.tr; + +@Log +public class Installer implements ProgressObservable { + + @Getter private final File tempDir; + private final HttpDownloader downloader; + + private TaskQueue mainQueue = new TaskQueue(); + private TaskQueue lateQueue = new TaskQueue(); + + private transient TaskQueue activeQueue; + + public Installer(@NonNull File tempDir) { + this.tempDir = tempDir; + this.downloader = new HttpDownloader(tempDir); + } + + public synchronized void queue(@NonNull InstallTask runnable) { + mainQueue.queue(runnable); + } + + public synchronized void queueLate(@NonNull InstallTask runnable) { + lateQueue.queue(runnable); + } + + public void download() throws IOException, InterruptedException { + downloader.execute(); + } + + public synchronized void execute(Launcher launcher) throws Exception { + activeQueue = mainQueue; + mainQueue.execute(launcher); + activeQueue = null; + } + + public synchronized void executeLate(Launcher launcher) throws Exception { + activeQueue = lateQueue; + lateQueue.execute(launcher); + activeQueue = null; + } + + public Downloader getDownloader() { + return downloader; + } + + @Override + public double getProgress() { + if (activeQueue == null) return 0.0; + + return activeQueue.finished / (double) activeQueue.count; + } + + @Override + public String getStatus() { + if (activeQueue != null && activeQueue.running != null) { + InstallTask running = activeQueue.running; + String status = running.getStatus(); + if (status == null) { + status = running.toString(); + } + return tr("installer.executing", activeQueue.count - activeQueue.finished) + "\n" + status; + } else { + return SharedLocale.tr("installer.installing"); + } + } + + public static class TaskQueue { + private List queue = new ArrayList(); + + private int count = 0; + private int finished = 0; + private InstallTask running; + + public synchronized void queue(@NonNull InstallTask runnable) { + queue.add(runnable); + count++; + } + + public synchronized void execute(Launcher launcher) throws Exception { + queue = Collections.unmodifiableList(queue); + + try { + for (InstallTask runnable : queue) { + checkInterrupted(); + running = runnable; + runnable.execute(launcher); + finished++; + } + } finally { + running = null; + } + } + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/install/ProcessorTask.java b/launcher/src/main/java/com/skcraft/launcher/install/ProcessorTask.java new file mode 100644 index 0000000..9b547ed --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/install/ProcessorTask.java @@ -0,0 +1,152 @@ +package com.skcraft.launcher.install; + +import com.google.common.collect.Lists; +import com.skcraft.launcher.Launcher; +import com.skcraft.launcher.model.loader.InstallProcessor; +import com.skcraft.launcher.model.loader.LoaderManifest; +import com.skcraft.launcher.model.loader.LoaderSubResolver; +import com.skcraft.launcher.model.loader.SidedData; +import com.skcraft.launcher.model.minecraft.Library; +import com.skcraft.launcher.model.minecraft.Side; +import com.skcraft.launcher.model.minecraft.VersionManifest; +import com.skcraft.launcher.model.modpack.DownloadableFile; +import com.skcraft.launcher.model.modpack.Manifest; +import com.skcraft.launcher.util.Environment; +import com.skcraft.launcher.util.FileUtils; +import lombok.RequiredArgsConstructor; +import lombok.extern.java.Log; + +import java.io.File; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.jar.Attributes; +import java.util.jar.JarFile; + +import static com.skcraft.launcher.util.SharedLocale.tr; + +@RequiredArgsConstructor +@Log +public class ProcessorTask implements InstallTask { + private final InstallProcessor processor; + private final LoaderManifest loaderManifest; + private final Manifest manifest; + private final HashMap localFiles; + + private transient String message = ""; + private transient double progress = 0; + + @Override + public void execute(Launcher launcher) throws Exception { + VersionManifest versionManifest = manifest.getVersionManifest(); + + LoaderSubResolver resolver = new LoaderSubResolver(manifest, loaderManifest, + 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); + Map outputs = processor.resolveOutputs(resolver); + + message = "Finding libraries"; + Library execFile = loaderManifest.findLibrary(processor.getJar()); + File jar = launcher.getLibraryFile(execFile); + + JarFile jarFile = new JarFile(jar); + String mainClass = jarFile.getManifest().getMainAttributes().getValue(Attributes.Name.MAIN_CLASS); + jarFile.close(); + + if (mainClass == null || mainClass.isEmpty()) { + throw new RuntimeException(String.format("Processor jar file '%s' has no main class!", processor.getJar())); + } + + List classpath = Lists.newArrayList(jar.toURI().toURL()); + int i = 0; + int total = processor.getClasspath().size(); + for (String libraryName : processor.getClasspath()) { + message = "Adding library " + libraryName; + File libraryFile = launcher.getLibraryFile(loaderManifest.findLibrary(libraryName)); + if (!libraryFile.exists()) { + throw new RuntimeException(String.format("Missing library '%s' for processor '%s'", + libraryName, processor.getJar())); + } + + classpath.add(libraryFile.toURI().toURL()); + i++; + progress = (double) i / total; + } + + progress = 0.0; + 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 { + // in java 9+ we need the platform classloader for access to certain modules + parent = (ClassLoader) ClassLoader.class.getDeclaredMethod("getPlatformClassLoader") + .invoke(null); + } catch (Throwable ignored) { + // java 8 or below it's a-ok to have no delegate + 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"; + progress = 1.0; + + if (!outputs.isEmpty()) { + progress = 0.0; + i = 0; + total = outputs.size(); + for (Map.Entry output : outputs.entrySet()) { + File artifact = new File(output.getKey()); + + if (!artifact.exists()) { + throw new RuntimeException(String.format("Artifact '%s' missing", output.getKey())); + } + + 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())); + } + + i++; + progress = (double) i / total; + } + } + } + + @Override + public double getProgress() { + return progress; + } + + @Override + public String getStatus() { + return tr("installer.runningProcessor", processor.getJar(), message); + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/install/UpdateCache.java b/launcher/src/main/java/com/skcraft/launcher/install/UpdateCache.java new file mode 100644 index 0000000..a33d9d8 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/install/UpdateCache.java @@ -0,0 +1,29 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.install; + +import lombok.Data; +import lombok.NonNull; + +import java.util.HashMap; +import java.util.Map; + +@Data +public class UpdateCache { + + private Map cache = new HashMap(); + + public synchronized boolean mark(@NonNull String key, @NonNull String version) { + String current = cache.get(key); + if (current != null && version.equals(current)) { + return false; + } else { + cache.put(key, version); + return true; + } + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/install/ZipExtract.java b/launcher/src/main/java/com/skcraft/launcher/install/ZipExtract.java new file mode 100644 index 0000000..6a1ac4c --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/install/ZipExtract.java @@ -0,0 +1,106 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.install; + +import com.google.common.io.ByteSource; +import com.google.common.io.Closer; +import lombok.Getter; +import lombok.NonNull; +import lombok.Setter; +import org.apache.commons.io.IOUtils; + +import java.io.*; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import static org.apache.commons.io.IOUtils.closeQuietly; + +public class ZipExtract implements Runnable { + + @Getter private final ByteSource source; + @Getter private final File destination; + @Getter @Setter + private List exclude; + + public ZipExtract(@NonNull ByteSource source, @NonNull File destination) { + this.source = source; + this.destination = destination; + } + + @Override + public void run() { + Closer closer = Closer.create(); + + try { + InputStream is = closer.register(source.openBufferedStream()); + ZipInputStream zis = closer.register(new ZipInputStream(is)); + ZipEntry entry; + + destination.getParentFile().mkdirs(); + + while ((entry = zis.getNextEntry()) != null) { + if (matches(entry)) { + File file = new File(getDestination(), entry.getName()); + if (entry.isDirectory()) { + file.mkdirs(); + } else { + writeEntry(zis, file); + } + } + } + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + try { + closer.close(); + } catch (IOException e) { + } + } + } + + /** + * Checks if the given entry should be extracted. + * + * @param entry the entry + * @return true if the entry matches the filter + */ + private boolean matches(ZipEntry entry) { + if (exclude != null) { + for (String pattern : exclude) { + if (entry.getName().startsWith(pattern)) { + return false; + } + } + } + + return true; + } + + private void writeEntry(ZipInputStream zis, File path) throws IOException { + FileOutputStream fos = null; + BufferedOutputStream bos = null; + + try { + path.getParentFile().mkdirs(); + + fos = new FileOutputStream(path); + bos = new BufferedOutputStream(fos); + IOUtils.copy(zis, bos); + } finally { + closeQuietly(bos); + closeQuietly(fos); + } + } + + @Override + public String toString() { + return destination.getName(); + } + + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/launch/JavaProcessBuilder.java b/launcher/src/main/java/com/skcraft/launcher/launch/JavaProcessBuilder.java new file mode 100644 index 0000000..7b7e40e --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/launch/JavaProcessBuilder.java @@ -0,0 +1,137 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.launch; + +import com.skcraft.launcher.launch.runtime.JavaRuntime; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A tool to build the entire command line used to launch a Java process. + * It combines flags, memory settings, arguments, the class path, and + * the main class. + */ +@ToString +public class JavaProcessBuilder { + + private static final Pattern argsPattern = Pattern.compile("(?:([^\"]\\S*)|\"(.+?)\")\\s*"); + + @Getter @Setter private JavaRuntime runtime; + @Getter @Setter private int minMemory; + @Getter @Setter private int maxMemory; + @Getter @Setter private int permGen; + + @Getter private final List classPath = new ArrayList(); + @Getter private final List flags = new ArrayList(); + @Getter private final List args = new ArrayList(); + @Getter @Setter private String mainClass; + + private File getJavaBinPath() throws IOException { + File path = runtime.getDir().getAbsoluteFile(); + + // Try the parent directory + if (!path.exists()) { + throw new IOException( + "The configured Java runtime path '" + path + "' doesn't exist."); + } else if (path.isFile()) { + path = path.getParentFile(); + } + + File binDir = new File(path, "bin"); + if (binDir.isDirectory()) { + path = binDir; + } + + return path; + } + + public JavaProcessBuilder classPath(File file) { + getClassPath().add(file); + return this; + } + + public JavaProcessBuilder classPath(String path) { + getClassPath().add(new File(path)); + return this; + } + + public String buildClassPath() { + StringBuilder builder = new StringBuilder(); + boolean first = true; + + for (File file : classPath) { + if (first) { + first = false; + } else { + builder.append(File.pathSeparator); + } + + builder.append(file.getAbsolutePath()); + } + + return builder.toString(); + } + + public List buildCommand() throws IOException { + List command = new ArrayList(); + + if (getRuntime() != null) { + File javaBinary = new File(getJavaBinPath(), "java"); + command.add(javaBinary.getAbsolutePath()); + } else { + command.add("java"); + } + + command.addAll(flags); + + if (minMemory > 0) { + command.add("-Xms" + minMemory + "M"); + } + + if (maxMemory > 0) { + command.add("-Xmx" + maxMemory + "M"); + } + + if (permGen > 0) { + // If we know the Java version, only add permsize for 7 or older + if (getRuntime() == null || getRuntime().getMajorVersion() < 8) { + command.add("-XX:MaxPermSize=" + permGen + "M"); + } + } + + command.add(mainClass); + command.addAll(args); + + return command; + } + + /** + * Split the given string as simple command line arguments. + * + *

This is not to be used for security purposes.

+ * + * @param str the string + * @return the split args + */ + public static List splitArgs(String str) { + Matcher matcher = argsPattern.matcher(str); + List parts = new ArrayList(); + while (matcher.find()) { + parts.add(matcher.group(1)); + } + return parts; + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/launch/LaunchListener.java b/launcher/src/main/java/com/skcraft/launcher/launch/LaunchListener.java new file mode 100644 index 0000000..a7c47e2 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/launch/LaunchListener.java @@ -0,0 +1,17 @@ +/* + * SKCraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.launch; + +public interface LaunchListener { + + public void instancesUpdated(); + + public void gameStarted(); + + public void gameClosed(); + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/launch/LaunchOptions.java b/launcher/src/main/java/com/skcraft/launcher/launch/LaunchOptions.java new file mode 100644 index 0000000..91aa4be --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/launch/LaunchOptions.java @@ -0,0 +1,96 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.launch; + +import com.skcraft.launcher.Instance; +import com.skcraft.launcher.auth.Session; +import lombok.Data; +import lombok.Getter; + +import java.awt.*; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Data +public class LaunchOptions { + + private final Window window; + private final Instance instance; + private final UpdatePolicy updatePolicy; + private final LaunchListener listener; + private final Session session; + + @Data + public static class Builder { + + private Window window = null; + private Instance instance; + private UpdatePolicy updatePolicy = UpdatePolicy.UPDATE_IF_SESSION_ONLINE; + private LaunchListener listener = new DummyLaunchListener(); + private Session session; + + public Builder setWindow(Window window) { + this.window = window; + return this; + } + + public Builder setInstance(Instance instance) { + this.instance = instance; + return this; + } + + public Builder setUpdatePolicy(UpdatePolicy updatePolicy) { + checkNotNull(updatePolicy, "updatePolicy"); + this.updatePolicy = updatePolicy; + return this; + } + + public Builder setListener(LaunchListener listener) { + checkNotNull(listener, "listener"); + this.listener = listener; + return this; + } + + public Builder setSession(Session session) { + this.session = session; + return this; + } + + public LaunchOptions build() { + checkNotNull(instance, "instance"); + return new LaunchOptions(window, instance, updatePolicy, listener, session); + } + } + + public enum UpdatePolicy { + NO_UPDATE(false), + UPDATE_IF_SESSION_ONLINE(true), + ALWAYS_UPDATE(true); + + @Getter + private final boolean updateEnabled; + + UpdatePolicy(boolean updateEnabled) { + this.updateEnabled = updateEnabled; + } + } + + private static class DummyLaunchListener implements LaunchListener { + @Override + public void instancesUpdated() { + } + + @Override + public void gameStarted() { + } + + @Override + public void gameClosed() { + } + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/launch/LaunchProcessHandler.java b/launcher/src/main/java/com/skcraft/launcher/launch/LaunchProcessHandler.java new file mode 100644 index 0000000..cfa0483 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/launch/LaunchProcessHandler.java @@ -0,0 +1,77 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.launch; + +import com.google.common.base.Function; +import com.skcraft.launcher.Launcher; +import com.skcraft.launcher.dialog.LauncherFrame; +import com.skcraft.launcher.dialog.ProcessConsoleFrame; +import com.skcraft.launcher.swing.MessageLog; +import lombok.NonNull; +import lombok.extern.java.Log; + +import javax.swing.*; +import java.lang.reflect.InvocationTargetException; +import java.util.logging.Level; + +/** + * Handles post-process creation during launch. + */ +@Log +public class LaunchProcessHandler implements Function { + + private static final int CONSOLE_NUM_LINES = 10000; + + private final Launcher launcher; + private ProcessConsoleFrame consoleFrame; + + public LaunchProcessHandler(@NonNull Launcher launcher) { + this.launcher = launcher; + } + + @Override + public ProcessConsoleFrame apply(final Process process) { + log.info("Watching process " + process); + + try { + SwingUtilities.invokeAndWait(new Runnable() { + @Override + public void run() { + consoleFrame = new ProcessConsoleFrame(CONSOLE_NUM_LINES, true); + consoleFrame.setProcess(process); + consoleFrame.setVisible(true); + MessageLog messageLog = consoleFrame.getMessageLog(); + messageLog.consume(process.getInputStream()); + messageLog.consume(process.getErrorStream()); + } + }); + + // Wait for the process to end + process.waitFor(); + } catch (InterruptedException e) { + // Orphan process + } catch (InvocationTargetException e) { + log.log(Level.WARNING, "Unexpected failure", e); + } + + log.info("Process ended, re-showing launcher..."); + + // Restore the launcher + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + if (consoleFrame != null) { + consoleFrame.setProcess(null); + consoleFrame.requestFocus(); + } + } + }); + + return consoleFrame; + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/launch/LaunchSupervisor.java b/launcher/src/main/java/com/skcraft/launcher/launch/LaunchSupervisor.java new file mode 100644 index 0000000..09f2f23 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/launch/LaunchSupervisor.java @@ -0,0 +1,216 @@ +/* + * SKCraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.launch; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.skcraft.concurrency.ObservableFuture; +import com.skcraft.launcher.Instance; +import com.skcraft.launcher.Launcher; +import com.skcraft.launcher.auth.Session; +import com.skcraft.launcher.dialog.AccountSelectDialog; +import com.skcraft.launcher.dialog.ProcessConsoleFrame; +import com.skcraft.launcher.dialog.ProgressDialog; +import com.skcraft.launcher.launch.LaunchOptions.UpdatePolicy; +import com.skcraft.launcher.launch.runtime.JavaRuntime; +import com.skcraft.launcher.model.minecraft.JavaVersion; +import com.skcraft.launcher.persistence.Persistence; +import com.skcraft.launcher.swing.SwingHelper; +import com.skcraft.launcher.update.Updater; +import com.skcraft.launcher.util.SharedLocale; +import com.skcraft.launcher.util.SwingExecutor; +import lombok.RequiredArgsConstructor; +import lombok.extern.java.Log; +import org.apache.commons.io.FileUtils; + +import javax.annotation.Nullable; +import javax.swing.*; +import java.awt.*; +import java.io.File; +import java.io.IOException; +import java.util.Date; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.function.BiPredicate; +import java.util.logging.Level; + +import static com.google.common.util.concurrent.MoreExecutors.sameThreadExecutor; +import static com.skcraft.launcher.util.SharedLocale.tr; + +@Log +public class LaunchSupervisor { + + private final Launcher launcher; + + public LaunchSupervisor(Launcher launcher) { + this.launcher = launcher; + } + + public void launch(LaunchOptions options) { + final Window window = options.getWindow(); + final Instance instance = options.getInstance(); + final LaunchListener listener = options.getListener(); + + try { + boolean update = options.getUpdatePolicy().isUpdateEnabled() && instance.isUpdatePending(); + + // Store last access date + Date now = new Date(); + instance.setLastAccessed(now); + Persistence.commitAndForget(instance); + + // Perform login + final Session session; + if (options.getSession() != null) { + session = options.getSession(); + } else { + session = AccountSelectDialog.showAccountRequest(window, launcher); + if (session == null) { + return; + } + } + + // If we have to update, we have to update + if (!instance.isInstalled()) { + update = true; + } + + if (update) { + // Execute the updater + Updater updater = new Updater(launcher, instance); + updater.setOnline(options.getUpdatePolicy() == UpdatePolicy.ALWAYS_UPDATE || session.isOnline()); + ObservableFuture future = new ObservableFuture( + launcher.getExecutor().submit(updater), updater); + + // Show progress + ProgressDialog.showProgress(window, future, SharedLocale.tr("launcher.updatingTitle"), tr("launcher.updatingStatus", instance.getTitle())); + SwingHelper.addErrorDialogCallback(window, future); + + // Update the list of instances after updating + future.addListener(new Runnable() { + @Override + public void run() { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + listener.instancesUpdated(); + } + }); + } + }, SwingExecutor.INSTANCE); + + // On success, launch also + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(Instance result) { + launch(window, instance, session, listener); + } + + @Override + public void onFailure(Throwable t) { + } + }, SwingExecutor.INSTANCE); + } else { + launch(window, instance, session, listener); + } + } catch (ArrayIndexOutOfBoundsException e) { + SwingHelper.showErrorDialog(window, SharedLocale.tr("launcher.noInstanceError"), SharedLocale.tr("launcher.noInstanceTitle")); + } + } + + private void launch(Window window, Instance instance, Session session, final LaunchListener listener) { + final File extractDir = launcher.createExtractDir(); + + // Get the process + Runner task = new Runner(launcher, instance, session, extractDir, new RuntimeVerifier(instance)); + ObservableFuture processFuture = new ObservableFuture( + launcher.getExecutor().submit(task), task); + + // Show process for the process retrieval + ProgressDialog.showProgress( + window, processFuture, SharedLocale.tr("launcher.launchingTItle"), tr("launcher.launchingStatus", instance.getTitle())); + + // If the process is started, get rid of this window + Futures.addCallback(processFuture, new FutureCallback() { + @Override + public void onSuccess(Process result) { + SwingUtilities.invokeLater(listener::gameStarted); + } + + @Override + public void onFailure(Throwable t) { + } + }); + + // Watch the created process + ListenableFuture future = Futures.transform( + processFuture, new LaunchProcessHandler(launcher), launcher.getExecutor()); + SwingHelper.addErrorDialogCallback(null, future); + + // Clean up at the very end + future.addListener(() -> { + try { + log.info("Process ended; cleaning up " + extractDir.getAbsolutePath()); + FileUtils.deleteDirectory(extractDir); + } catch (IOException e) { + log.log(Level.WARNING, "Failed to clean up " + extractDir.getAbsolutePath(), e); + } + }, sameThreadExecutor()); + + // Hook up launch listener + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(@Nullable ProcessConsoleFrame result) { + // gameStarted was only invoked on success above, so only call gameClosed on success + listener.gameClosed(); + } + + @Override + public void onFailure(Throwable t) { + // likely user cancellation + if (!(t instanceof CancellationException)) { + log.info("Process failure: " + t.getLocalizedMessage()); + } + } + }, SwingExecutor.INSTANCE); + } + + @RequiredArgsConstructor + static class RuntimeVerifier implements BiPredicate { + private final Instance instance; + + @Override + public boolean test(JavaRuntime javaRuntime, JavaVersion javaVersion) { + ListenableFuture fut = SwingExecutor.INSTANCE.submit(() -> { + Object[] options = new Object[]{ + tr("button.cancel"), + tr("button.launchAnyway"), + }; + + String message = tr("runner.wrongJavaVersion", + instance.getTitle(), javaVersion.getMajorVersion(), javaRuntime.getVersion()); + int picked = JOptionPane.showOptionDialog(null, + SwingHelper.htmlWrap(message), + tr("launcher.javaMismatchTitle"), + JOptionPane.DEFAULT_OPTION, + JOptionPane.WARNING_MESSAGE, + null, + options, + null); + + return picked == 1; + }); + + try { + return fut.get(); + } catch (ExecutionException | InterruptedException e) { + throw new RuntimeException(e); + } + } + } +} 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 0000000..eed8b64 --- /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 new file mode 100644 index 0000000..7846861 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/launch/Runner.java @@ -0,0 +1,483 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.launch; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import com.google.common.io.Files; +import com.skcraft.concurrency.DefaultProgress; +import com.skcraft.concurrency.ProgressObservable; +import com.skcraft.launcher.*; +import com.skcraft.launcher.auth.Session; +import com.skcraft.launcher.install.ZipExtract; +import com.skcraft.launcher.launch.runtime.JavaRuntime; +import com.skcraft.launcher.launch.runtime.JavaRuntimeFinder; +import com.skcraft.launcher.model.minecraft.*; +import com.skcraft.launcher.persistence.Persistence; +import com.skcraft.launcher.util.Environment; +import com.skcraft.launcher.util.Platform; +import com.skcraft.launcher.util.SharedLocale; +import lombok.Getter; +import lombok.NonNull; +import lombok.Setter; +import lombok.extern.java.Log; +import org.apache.commons.lang.text.StrSubstitutor; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.Callable; +import java.util.concurrent.CancellationException; +import java.util.function.BiPredicate; + +import static com.skcraft.launcher.LauncherUtils.checkInterrupted; +import static com.skcraft.launcher.util.SharedLocale.tr; + +/** + * Handles the launching of an instance. + */ +@Log +public class Runner implements Callable, ProgressObservable { + + private ProgressObservable progress = new DefaultProgress(0, SharedLocale.tr("runner.preparing")); + + private final ObjectMapper mapper = new ObjectMapper(); + private final Launcher launcher; + private final Instance instance; + private final Session session; + private final File extractDir; + private final BiPredicate javaRuntimeMismatch; + @Getter @Setter private Environment environment = Environment.getInstance(); + + private VersionManifest versionManifest; + private AssetsIndex assetsIndex; + private File virtualAssetsDir; + private Configuration config; + private JavaProcessBuilder builder; + private AssetsRoot assetsRoot; + private FeatureList.Mutable featureList; + + /** + * Create a new instance launcher. + * @param launcher the launcher + * @param instance the instance + * @param session the session + * @param extractDir the directory to extract to + * @param javaRuntimeMismatch + */ + public Runner(@NonNull Launcher launcher, @NonNull Instance instance, + @NonNull Session session, @NonNull File extractDir, + BiPredicate javaRuntimeMismatch) { + this.launcher = launcher; + this.instance = instance; + this.session = session; + this.extractDir = extractDir; + this.javaRuntimeMismatch = javaRuntimeMismatch; + this.featureList = new FeatureList.Mutable(); + } + + /** + * Get the path to the JAR. + * + * @return the JAR path + */ + private File getJarPath() { + File jarPath = instance.getCustomJarPath(); + if (!jarPath.exists()) { + jarPath = launcher.getJarPath(versionManifest); + } + return jarPath; + } + + @Override + public Process call() throws Exception { + if (!instance.isInstalled()) { + throw new LauncherException("Update required", SharedLocale.tr("runner.updateRequired")); + } + + config = launcher.getConfig(); + builder = new JavaProcessBuilder(); + assetsRoot = launcher.getAssets(); + + // Load manifiests + versionManifest = mapper.readValue(instance.getVersionPath(), VersionManifest.class); + + // Load assets index + File assetsFile = assetsRoot.getIndexPath(versionManifest); + try { + assetsIndex = mapper.readValue(assetsFile, AssetsIndex.class); + } catch (FileNotFoundException e) { + instance.setInstalled(false); + Persistence.commitAndForget(instance); + throw new LauncherException("Missing assets index " + assetsFile.getAbsolutePath(), + tr("runner.missingAssetsIndex", instance.getTitle(), assetsFile.getAbsolutePath())); + } catch (IOException e) { + instance.setInstalled(false); + Persistence.commitAndForget(instance); + throw new LauncherException("Corrupt assets index " + assetsFile.getAbsolutePath(), + tr("runner.corruptAssetsIndex", instance.getTitle(), assetsFile.getAbsolutePath())); + } + + // Copy over assets to the tree + try { + AssetsRoot.AssetsTreeBuilder assetsBuilder = assetsRoot.createAssetsBuilder(versionManifest); + progress = assetsBuilder; + virtualAssetsDir = assetsBuilder.build(); + } catch (LauncherException e) { + instance.setInstalled(false); + Persistence.commitAndForget(instance); + throw e; + } + + progress = new DefaultProgress(0.9, SharedLocale.tr("runner.collectingArgs")); + builder.setMainClass(versionManifest.getMainClass()); + + addWindowArgs(); + addLibraries(); + addJvmArgs(); + addJarArgs(); + addProxyArgs(); + addServerArgs(); + addPlatformArgs(); + addLegacyArgs(); + + callLaunchModifier(); + + verifyJavaRuntime(); + + ProcessBuilder processBuilder = new ProcessBuilder(builder.buildCommand()); + processBuilder.directory(instance.getContentDir()); + Runner.log.info("Launching: " + builder); + checkInterrupted(); + + progress = new DefaultProgress(1, SharedLocale.tr("runner.startingJava")); + + return processBuilder.start(); + } + + /** + * Call the manifest launch modifier. + */ + private void callLaunchModifier() { + instance.modify(builder); + } + + private void verifyJavaRuntime() { + JavaRuntime pickedRuntime = builder.getRuntime(); + JavaVersion targetVersion = versionManifest.getJavaVersion(); + + if (pickedRuntime == null || targetVersion == null) { + return; + } + + if (pickedRuntime.getMajorVersion() != targetVersion.getMajorVersion()) { + boolean launchAnyway = javaRuntimeMismatch.test(pickedRuntime, targetVersion); + + if (!launchAnyway) { + throw new CancellationException("Launch cancelled by user."); + } + } + } + + /** + * Add platform-specific arguments. + */ + private void addPlatformArgs() { + // Mac OS X arguments + if (getEnvironment().getPlatform() == Platform.MAC_OS_X) { + File icnsPath = assetsIndex.getObjectPath(assetsRoot, "icons/minecraft.icns"); + if (icnsPath != null) { + builder.getFlags().add("-Xdock:icon=" + icnsPath.getAbsolutePath()); + builder.getFlags().add("-Xdock:name=Minecraft"); + } + } + } + + /** + * Add libraries. + */ + private void addLibraries() throws LauncherException { + // Add libraries to classpath or extract the libraries as necessary + for (Library library : versionManifest.getLibraries()) { + if (!library.matches(environment)) { + continue; + } + + File path = new File(launcher.getLibrariesDir(), library.getPath(environment)); + + if (path.exists()) { + Library.Extract extract = library.getExtract(); + if (extract != null) { + ZipExtract zipExtract = new ZipExtract(Files.asByteSource(path), extractDir); + zipExtract.setExclude(extract.getExclude()); + zipExtract.run(); + } else { + builder.classPath(path); + } + } else { + instance.setInstalled(false); + Persistence.commitAndForget(instance); + throw new LauncherException("Missing library " + library.getName(), + tr("runner.missingLibrary", instance.getTitle(), library.getName())); + } + } + + // The official launcher puts the vanilla jar at the end of the classpath, we'll do the same + builder.classPath(getJarPath()); + } + + /** + * Add JVM arguments. + * + * @throws IOException on I/O error + */ + 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) { + minMemory = 1024; + } + + if (maxMemory <= 0) { + maxMemory = 1024; + } + + if (permGen <= 0) { + permGen = 128; + } + + if (permGen <= 64) { + permGen = 64; + } + + if (minMemory > maxMemory) { + maxMemory = minMemory; + } + + builder.setMinMemory(minMemory); + builder.setMaxMemory(maxMemory); + builder.setPermGen(permGen); + + JavaRuntime selectedRuntime = Optional.ofNullable(instance.getSettings().getRuntime()) + .orElseGet(() -> Optional.ofNullable(versionManifest.getJavaVersion()) + .flatMap(JavaRuntimeFinder::findBestJavaRuntime) + .orElse(config.getJavaRuntime()) + ); + + // Builder defaults to the PATH `java` if the runtime is null + builder.setRuntime(selectedRuntime); + + List flags = builder.getFlags(); + String[] rawJvmArgsList = new String[] { + config.getJvmArgs(), + instance.getSettings().getCustomJvmArgs() + }; + + for (String rawJvmArgs : rawJvmArgsList) { + if (!Strings.isNullOrEmpty(rawJvmArgs)) { + flags.addAll(JavaProcessBuilder.splitArgs(rawJvmArgs)); + } + } + + List javaArguments = versionManifest.getArguments().getJvmArguments(); + StrSubstitutor substitutor = new StrSubstitutor(getCommandSubstitutions()); + for (GameArgument arg : javaArguments) { + if (arg.shouldApply(environment, featureList)) { + for (String subArg : arg.getValues()) { + flags.add(substitutor.replace(subArg)); + } + } + } + + if (versionManifest.getLogging() != null && versionManifest.getLogging().getClient() != null) { + log.info("Logging config present, log4j2 bug likely mitigated"); + + VersionManifest.LoggingConfig config = versionManifest.getLogging().getClient(); + File configFile = new File(launcher.getLibrariesDir(), config.getFile().getId()); + StrSubstitutor loggingSub = new StrSubstitutor(ImmutableMap.of("path", configFile.getAbsolutePath())); + + flags.add(loggingSub.replace(config.getArgument())); + } + } + + /** + * Add arguments for the application. + * + * @throws JsonProcessingException on error + */ + private void addJarArgs() throws JsonProcessingException { + List args = builder.getArgs(); + + List rawArgs = versionManifest.getArguments().getGameArguments(); + StrSubstitutor substitutor = new StrSubstitutor(getCommandSubstitutions()); + for (GameArgument arg : rawArgs) { + if (arg.shouldApply(environment, featureList)) { + for (String subArg : arg.getValues()) { + args.add(substitutor.replace(subArg)); + } + } + } + } + + /** + * Add proxy arguments. + */ + private void addProxyArgs() { + List args = builder.getArgs(); + + if (config.isProxyEnabled()) { + String host = config.getProxyHost(); + int port = config.getProxyPort(); + String username = config.getProxyUsername(); + String password = config.getProxyPassword(); + + if (!Strings.isNullOrEmpty(host) && port > 0 && port < 65535) { + args.add("--proxyHost"); + args.add(config.getProxyHost()); + args.add("--proxyPort"); + args.add(String.valueOf(port)); + + if (!Strings.isNullOrEmpty(username)) { + builder.getArgs().add("--proxyUser"); + builder.getArgs().add(username); + builder.getArgs().add("--proxyPass"); + builder.getArgs().add(password); + } + } + } + } + + /** + * Add server arguments. + */ + private void addServerArgs() { + List args = builder.getArgs(); + + if (config.isServerEnabled()) { + String host = config.getServerHost(); + int port = config.getServerPort(); + + if (!Strings.isNullOrEmpty(host) && port > 0 && port < 65535) { + args.add("--server"); + args.add(host); + args.add("--port"); + args.add(String.valueOf(port)); + } + } + } + + /** + * Add window arguments. + */ + private void addWindowArgs() { + int width = config.getWindowWidth(); + + if (width >= 10) { + featureList.addFeature("has_custom_resolution", true); + } + } + + /** + * Add arguments to make legacy Minecraft work. + */ + private void addLegacyArgs() { + List flags = builder.getFlags(); + + if (versionManifest.getMinimumLauncherVersion() < 21) { + // Add bits that the legacy manifests don't + flags.add("-Djava.library.path=" + extractDir.getAbsoluteFile()); + flags.add("-cp"); + flags.add(builder.buildClassPath()); + + if (featureList.hasFeature("has_custom_resolution")) { + List args = builder.getArgs(); + args.add("--width"); + args.add(String.valueOf(config.getWindowWidth())); + args.add("--height"); + args.add(String.valueOf(config.getWindowHeight())); + } + + // Add old platform hacks that the new manifests already specify + if (getEnvironment().getPlatform() == Platform.WINDOWS) { + flags.add("-XX:HeapDumpPath=MojangTricksIntelDriversForPerformance_javaw.exe_minecraft.exe.heapdump"); + } + } + + if (versionManifest.getMinimumLauncherVersion() < 18) { + // TODO find out exactly what versions need this hack. + flags.add("-Dminecraft.applet.TargetDirectory=" + instance.getContentDir()); + } + } + + /** + * Build the list of command substitutions. + * + * @return the map of substitutions + * @throws JsonProcessingException on error + */ + private Map getCommandSubstitutions() throws JsonProcessingException { + Map map = new HashMap(); + + map.put("version_name", versionManifest.getId()); + map.put("version_type", launcher.getProperties().getProperty("launcherShortname")); + + map.put("auth_access_token", session.getAccessToken()); + map.put("auth_session", session.getSessionToken()); + map.put("auth_player_name", session.getName()); + map.put("auth_uuid", session.getUuid()); + + map.put("profile_name", session.getName()); + map.put("user_type", session.getUserType().getId()); + map.put("user_properties", mapper.writeValueAsString(session.getUserProperties())); + + map.put("game_directory", instance.getContentDir().getAbsolutePath()); + map.put("game_assets", virtualAssetsDir.getAbsolutePath()); + map.put("assets_root", launcher.getAssets().getDir().getAbsolutePath()); + map.put("assets_index_name", versionManifest.getAssetId()); + + map.put("resolution_width", String.valueOf(config.getWindowWidth())); + map.put("resolution_height", String.valueOf(config.getWindowHeight())); + + map.put("launcher_name", launcher.getTitle()); + map.put("launcher_version", launcher.getVersion()); + 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; + } + + @Override + public double getProgress() { + return progress.getProgress(); + } + + @Override + public String getStatus() { + return progress.getStatus(); + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/launch/runtime/AddJavaRuntime.java b/launcher/src/main/java/com/skcraft/launcher/launch/runtime/AddJavaRuntime.java new file mode 100644 index 0000000..06265ed --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/launch/runtime/AddJavaRuntime.java @@ -0,0 +1,19 @@ +package com.skcraft.launcher.launch.runtime; + +import java.io.File; + +/** + * Special sentinel subclass used to represent the "add custom runtime" entry in the dropdown + */ +public class AddJavaRuntime extends JavaRuntime { + public AddJavaRuntime() { + super(new File(""), "", false); + } + + @Override + public String toString() { + return "Add undetected Java runtime..."; + } + + public static final AddJavaRuntime ADD_RUNTIME_SENTINEL = new AddJavaRuntime(); +} diff --git a/launcher/src/main/java/com/skcraft/launcher/launch/runtime/JavaReleaseFile.java b/launcher/src/main/java/com/skcraft/launcher/launch/runtime/JavaReleaseFile.java new file mode 100644 index 0000000..b7cee3b --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/launch/runtime/JavaReleaseFile.java @@ -0,0 +1,47 @@ +package com.skcraft.launcher.launch.runtime; + +import com.skcraft.launcher.util.EnvironmentParser; +import lombok.extern.java.Log; + +import java.io.File; +import java.io.IOException; +import java.util.Map; +import java.util.logging.Level; + +@Log +public class JavaReleaseFile { + private Map backingMap; + + private JavaReleaseFile(Map releaseDetails) { + this.backingMap = releaseDetails; + } + + public String getVersion() { + return backingMap.get("JAVA_VERSION"); + } + + public String getArch() { + return backingMap.get("OS_ARCH"); + } + + public boolean isArch64Bit() { + return getArch() == null || getArch().matches("x64|x86_64|amd64|aarch64"); + } + + public static JavaReleaseFile parseFromRelease(File javaPath) { + File releaseFile = new File(javaPath, "release"); + + if (releaseFile.exists()) { + try { + Map releaseDetails = EnvironmentParser.parse(releaseFile); + + return new JavaReleaseFile(releaseDetails); + } catch (IOException e) { + log.log(Level.WARNING, "Failed to read release file " + releaseFile.getAbsolutePath(), e); + return null; + } + } + + return null; + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/launch/runtime/JavaRuntime.java b/launcher/src/main/java/com/skcraft/launcher/launch/runtime/JavaRuntime.java new file mode 100644 index 0000000..ed0e39f --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/launch/runtime/JavaRuntime.java @@ -0,0 +1,115 @@ +package com.skcraft.launcher.launch.runtime; + +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[0].equals("1") && parts.length > 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() { + String version = this.version != null ? this.version : "unknown"; + + 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/runtime/JavaRuntimeFinder.java b/launcher/src/main/java/com/skcraft/launcher/launch/runtime/JavaRuntimeFinder.java new file mode 100644 index 0000000..01ecf2e --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/launch/runtime/JavaRuntimeFinder.java @@ -0,0 +1,125 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.launch.runtime; + +import com.skcraft.launcher.model.minecraft.JavaVersion; +import com.skcraft.launcher.util.Environment; +import lombok.extern.java.Log; + +import java.io.File; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Finds the best Java runtime to use. + */ +@Log +public final class JavaRuntimeFinder { + + private JavaRuntimeFinder() { + } + + /** + * Get all available Java runtimes on the system + * @return List of available Java runtimes sorted by newest first + */ + public static List getAvailableRuntimes() { + Environment env = Environment.getInstance(); + PlatformRuntimeFinder runtimeFinder = getRuntimeFinder(env); + + if (runtimeFinder == null) { + return Collections.emptyList(); + } + + // Add Minecraft javas + List mcRuntimes = MinecraftJavaFinder.scanLauncherDirectories(env, + runtimeFinder.getLauncherDirectories(env)); + Set entries = new HashSet<>(mcRuntimes); + + // Add system Javas + runtimeFinder.getCandidateJavaLocations().stream() + .map(JavaRuntimeFinder::getRuntimeFromPath) + .filter(Objects::nonNull) + .forEach(entries::add); + + // Add extra runtimes + entries.addAll(runtimeFinder.getExtraRuntimes()); + + return entries.stream().sorted().collect(Collectors.toList()); + } + + /** + * Find the best runtime for a given Java version + * @param targetVersion Version to match + * @return Java runtime if available, empty Optional otherwise + */ + 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) { + return getRuntimeFromPath(new File(path)); + } + + public static JavaRuntime getRuntimeFromPath(File target) { + // Normalize target to root first + if (target.isFile()) { + // Probably referring directly to bin/java, back up two levels + target = target.getParentFile().getParentFile(); + } else if (target.getName().equals("bin")) { + // Probably copied the bin directory that java.exe is in + target = target.getParentFile(); + } + + // Find the release file + File releaseFile = new File(target, "release"); + if (!releaseFile.isFile()) { + releaseFile = new File(target, "jre/release"); + // may still not exist - parseFromRelease below will return null if so + } + + // Find the bin folder + File binFolder = new File(target, "bin"); + if (!binFolder.isDirectory()) { + binFolder = new File(target, "jre/bin"); + } + + if (!binFolder.isDirectory()) { + // No bin folder, this isn't a usable install + return null; + } + + JavaReleaseFile release = JavaReleaseFile.parseFromRelease(releaseFile.getParentFile()); + if (release == null) { + // Make some assumptions... + return new JavaRuntime(binFolder.getParentFile(), null, true); + } + + return new JavaRuntime(binFolder.getParentFile(), release.getVersion(), release.isArch64Bit()); + } + + private static PlatformRuntimeFinder getRuntimeFinder(Environment env) { + switch (env.getPlatform()) { + case WINDOWS: + return new WindowsRuntimeFinder(); + case MAC_OS_X: + return new MacRuntimeFinder(); + case LINUX: + return new LinuxRuntimeFinder(); + default: + return null; + } + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/launch/runtime/LinuxRuntimeFinder.java b/launcher/src/main/java/com/skcraft/launcher/launch/runtime/LinuxRuntimeFinder.java new file mode 100644 index 0000000..1ab0df6 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/launch/runtime/LinuxRuntimeFinder.java @@ -0,0 +1,44 @@ +package com.skcraft.launcher.launch.runtime; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import com.skcraft.launcher.util.Environment; + +import java.io.File; +import java.io.IOException; +import java.util.*; + +public class LinuxRuntimeFinder implements PlatformRuntimeFinder { + @Override + public Set getLauncherDirectories(Environment env) { + return ImmutableSet.of(new File(System.getenv("HOME"), ".minecraft")); + } + + @Override + public List getCandidateJavaLocations() { + ArrayList entries = Lists.newArrayList(); + + String javaHome = System.getenv("JAVA_HOME"); + if (javaHome != null) { + entries.add(new File(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(entries::add); + } + + return entries; + } + + @Override + public List getExtraRuntimes() { + return Collections.emptyList(); + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/launch/runtime/MacRuntimeFinder.java b/launcher/src/main/java/com/skcraft/launcher/launch/runtime/MacRuntimeFinder.java new file mode 100644 index 0000000..22a21df --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/launch/runtime/MacRuntimeFinder.java @@ -0,0 +1,57 @@ +package com.skcraft.launcher.launch.runtime; + +import com.dd.plist.NSArray; +import com.dd.plist.NSDictionary; +import com.dd.plist.NSObject; +import com.dd.plist.PropertyListParser; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import com.skcraft.launcher.util.Environment; +import lombok.extern.java.Log; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.logging.Level; + +@Log +public class MacRuntimeFinder implements PlatformRuntimeFinder { + @Override + public Set getLauncherDirectories(Environment env) { + return ImmutableSet.of(new File(System.getenv("HOME"), "Library/Application Support/minecraft")); + } + + @Override + public List getCandidateJavaLocations() { + return Collections.emptyList(); + } + + @Override + public List getExtraRuntimes() { + ArrayList entries = Lists.newArrayList(); + + try { + Process p = Runtime.getRuntime().exec("/usr/libexec/java_home -X"); + NSArray root = (NSArray) PropertyListParser.parse(p.getInputStream()); + NSObject[] arr = root.getArray(); + for (NSObject obj : arr) { + NSDictionary dict = (NSDictionary) obj; + entries.add(new JavaRuntime( + new File(dict.objectForKey("JVMHomePath").toString()).getAbsoluteFile(), + dict.objectForKey("JVMVersion").toString(), + isArch64Bit(dict.objectForKey("JVMArch").toString()) + )); + } + } catch (Throwable err) { + log.log(Level.WARNING, "Failed to parse java_home command", err); + } + + return entries; + } + + private static boolean isArch64Bit(String string) { + return string == null || string.matches("x64|x86_64|amd64|aarch64"); + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/launch/runtime/MinecraftJavaFinder.java b/launcher/src/main/java/com/skcraft/launcher/launch/runtime/MinecraftJavaFinder.java new file mode 100644 index 0000000..5dcbf6b --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/launch/runtime/MinecraftJavaFinder.java @@ -0,0 +1,66 @@ +package com.skcraft.launcher.launch.runtime; + +import com.skcraft.launcher.util.Environment; +import com.skcraft.launcher.util.Platform; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * Scans Minecraft bundled Java directories + */ +public class MinecraftJavaFinder { + public static List scanLauncherDirectories(Environment env, Collection launcherDirs) { + ArrayList entries = new ArrayList<>(); + + for (File install : launcherDirs) { + File runtimes = new File(install, "runtime"); + File[] runtimeList = runtimes.listFiles(); + if (runtimeList != null) { + for (File potential : runtimeList) { + JavaRuntime runtime = scanPotentialRuntime(env, potential); + + if (runtime != null) { + entries.add(runtime); + } + } + } + } + + return entries; + } + + private static JavaRuntime scanPotentialRuntime(Environment env, File potential) { + String runtimeName = potential.getName(); + if (runtimeName.startsWith("jre-x")) { + boolean is64Bit = runtimeName.equals("jre-x64"); + + JavaReleaseFile release = JavaReleaseFile.parseFromRelease(potential); + String version = release != null ? release.getVersion() : null; + + JavaRuntime runtime = new JavaRuntime(potential.getAbsoluteFile(), version, is64Bit); + runtime.setMinecraftBundled(true); + return runtime; + } else { + String[] children = potential.list((dir, name) -> new File(dir, name).isDirectory()); + if (children == null || children.length != 1) return null; + String platformName = children[0]; + + File javaDir = new File(potential, String.format("%s/%s", platformName, runtimeName)); + if (env.getPlatform() == Platform.MAC_OS_X) { + javaDir = new File(javaDir, "jre.bundle/Contents/Home"); + } + + JavaReleaseFile release = JavaReleaseFile.parseFromRelease(javaDir); + if (release == null) { + return null; + } + + JavaRuntime runtime = new JavaRuntime(javaDir.getAbsoluteFile(), release.getVersion(), release.isArch64Bit()); + runtime.setMinecraftBundled(true); + return runtime; + } + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/launch/runtime/PlatformRuntimeFinder.java b/launcher/src/main/java/com/skcraft/launcher/launch/runtime/PlatformRuntimeFinder.java new file mode 100644 index 0000000..1d66636 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/launch/runtime/PlatformRuntimeFinder.java @@ -0,0 +1,31 @@ +package com.skcraft.launcher.launch.runtime; + +import com.skcraft.launcher.util.Environment; + +import java.io.File; +import java.util.List; +import java.util.Set; + +public interface PlatformRuntimeFinder { + /** + * Get the list of possible launcher locations for this platform + * @return List of possible launcher locations + */ + Set getLauncherDirectories(Environment env); + + /** + * Get a list of candidate folders to check for Java runtimes. + * The returned folders will be checked for "release" files which describe the version and architecture. + * + * @return List of folders that may contain Java runtimes + */ + List getCandidateJavaLocations(); + + /** + * Get a list of extra runtimes obtained using platform-specific logic. + * e.g. on Windows, registry entries are returned + * + * @return List of extra Java runtimes + */ + List getExtraRuntimes(); +} diff --git a/launcher/src/main/java/com/skcraft/launcher/launch/runtime/WindowsRuntimeFinder.java b/launcher/src/main/java/com/skcraft/launcher/launch/runtime/WindowsRuntimeFinder.java new file mode 100644 index 0000000..1308965 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/launch/runtime/WindowsRuntimeFinder.java @@ -0,0 +1,91 @@ +package com.skcraft.launcher.launch.runtime; + +import com.google.common.collect.Lists; +import com.skcraft.launcher.util.Environment; +import com.skcraft.launcher.util.WinRegistry; +import com.sun.jna.platform.win32.WinReg; +import lombok.extern.java.Log; + +import java.io.File; +import java.io.IOException; +import java.util.*; +import java.util.logging.Level; + +@Log +public class WindowsRuntimeFinder implements PlatformRuntimeFinder { + @Override + public Set getLauncherDirectories(Environment env) { + HashSet launcherDirs = new HashSet<>(); + + try { + String launcherPath = WinRegistry.readString(WinReg.HKEY_CURRENT_USER, + "SOFTWARE\\Mojang\\InstalledProducts\\Minecraft Launcher", "InstallLocation"); + + launcherDirs.add(new File(launcherPath)); + } catch (Throwable err) { + log.log(Level.WARNING, "Failed to read launcher location from registry", err); + } + + String programFiles = Objects.equals(env.getArchBits(), "64") + ? System.getenv("ProgramFiles(x86)") + : System.getenv("ProgramFiles"); + + // Mojang likes to move the java runtime directory + launcherDirs.add(new File(programFiles, "Minecraft")); + launcherDirs.add(new File(programFiles, "Minecraft Launcher")); + launcherDirs.add(new File(System.getenv("LOCALAPPDATA"), "Packages\\Microsoft.4297127D64EC6_8wekyb3d8bbwe\\LocalCache\\Local")); + + return launcherDirs; + } + + @Override + public List getCandidateJavaLocations() { + return Collections.emptyList(); + } + + @Override + public List getExtraRuntimes() { + ArrayList entries = Lists.newArrayList(); + + getEntriesFromRegistry(entries, "SOFTWARE\\JavaSoft\\Java Runtime Environment"); + getEntriesFromRegistry(entries, "SOFTWARE\\JavaSoft\\Java Development Kit"); + getEntriesFromRegistry(entries, "SOFTWARE\\JavaSoft\\JDK"); + + return entries; + } + + private static void getEntriesFromRegistry(Collection entries, String basePath) + throws IllegalArgumentException { + try { + List subKeys = WinRegistry.readStringSubKeys(WinReg.HKEY_LOCAL_MACHINE, basePath); + for (String subKey : subKeys) { + JavaRuntime entry = getEntryFromRegistry(basePath, subKey); + if (entry != null) { + entries.add(entry); + } + } + } catch (Throwable err) { + log.log(Level.INFO, "Failed to read Java locations from registry in " + basePath); + } + } + + private static JavaRuntime getEntryFromRegistry(String basePath, String version) { + String regPath = basePath + "\\" + version; + String path = WinRegistry.readString(WinReg.HKEY_LOCAL_MACHINE, regPath, "JavaHome"); + File dir = new File(path); + if (dir.exists() && new File(dir, "bin/java.exe").exists()) { + return new JavaRuntime(dir, version, guessIf64BitWindows(dir)); + } else { + return null; + } + } + + private static boolean guessIf64BitWindows(File path) { + try { + String programFilesX86 = System.getenv("ProgramFiles(x86)"); + return programFilesX86 == null || !path.getCanonicalPath().startsWith(new File(programFilesX86).getCanonicalPath()); + } catch (IOException ignored) { + return false; + } + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/loader/BasicInstallProfile.java b/launcher/src/main/java/com/skcraft/launcher/model/loader/BasicInstallProfile.java new file mode 100644 index 0000000..b5f5f2f --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/loader/BasicInstallProfile.java @@ -0,0 +1,35 @@ +package com.skcraft.launcher.model.loader; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class BasicInstallProfile { + private String profile; + private int spec; + + @JsonProperty("install") + private Legacy legacyProfile; + + @JsonIgnore + public boolean isLegacy() { + return getLegacyProfile() != null; + } + + public String resolveProfileName() { + if (isLegacy()) { + return getLegacyProfile().getProfileName(); + } else { + return getProfile(); + } + } + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Legacy { + private String profileName; + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/loader/ExtendedSidedData.java b/launcher/src/main/java/com/skcraft/launcher/model/loader/ExtendedSidedData.java new file mode 100644 index 0000000..d83fc6b --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/loader/ExtendedSidedData.java @@ -0,0 +1,10 @@ +package com.skcraft.launcher.model.loader; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class ExtendedSidedData extends SidedData { + private T common; +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/loader/FabricMod.java b/launcher/src/main/java/com/skcraft/launcher/model/loader/FabricMod.java new file mode 100644 index 0000000..0afbb91 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/loader/FabricMod.java @@ -0,0 +1,12 @@ +package com.skcraft.launcher.model.loader; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class FabricMod implements Versionable { + private String id; + private String name; + private String version; +} 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 new file mode 100644 index 0000000..45e18f7 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/loader/InstallProcessor.java @@ -0,0 +1,52 @@ +package com.skcraft.launcher.model.loader; + +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; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class InstallProcessor { + private String jar; + private List classpath; + private List args; + private Map outputs; + private List sides; + + public List resolveArgs(LoaderSubResolver resolver) { + return Lists.transform(getArgs(), resolver); + } + + public Map resolveOutputs(final LoaderSubResolver resolver) { + if (getOutputs() == null) return Collections.emptyMap(); + + HashMap result = new HashMap(); + + for (Map.Entry entry : getOutputs().entrySet()) { + result.put(resolver.apply(entry.getKey()), resolver.apply(entry.getValue())); + } + + 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/LoaderManifest.java b/launcher/src/main/java/com/skcraft/launcher/model/loader/LoaderManifest.java new file mode 100644 index 0000000..6d6cb7a --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/loader/LoaderManifest.java @@ -0,0 +1,29 @@ +package com.skcraft.launcher.model.loader; + +import com.skcraft.launcher.model.minecraft.Library; +import com.skcraft.launcher.model.modpack.DownloadableFile; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class LoaderManifest { + private List libraries; + private Map> sidedData; + private List downloadableFiles; + + public Library findLibrary(String name) { + for (Library library : getLibraries()) { + if (library.matches(name)) { + return library; + } + } + + return null; + } +} 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 new file mode 100644 index 0000000..9042daa --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/loader/LoaderSubResolver.java @@ -0,0 +1,122 @@ +package com.skcraft.launcher.model.loader; + +import com.google.common.base.Function; +import com.skcraft.launcher.model.minecraft.Library; +import com.skcraft.launcher.model.minecraft.Side; +import com.skcraft.launcher.model.modpack.DownloadableFile; +import com.skcraft.launcher.model.modpack.Manifest; +import com.skcraft.launcher.util.Environment; +import lombok.RequiredArgsConstructor; + +import java.io.File; +import java.util.HashMap; + +@RequiredArgsConstructor +public class LoaderSubResolver implements Function { + private final Manifest manifest; + private final LoaderManifest loader; + private final Environment env; + private final Side side; + private final File libraryDir; + private final HashMap localFiles; + + public String getPathOf(String... rest) { + File file = libraryDir; + for (String part : rest) { + file = new File(file, part); + } + + return file.getAbsolutePath(); + } + + @Override + 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; + char end = arg.charAt(bound); + + if (start == '{' && end == '}') { + SidedData sidedData = loader.getSidedData().get(arg.substring(1, bound)); + if (sidedData != null) { + arg = sidedData.resolveFor(side); + } + } else if (start == '[' && end == ']') { + String libraryName = arg.substring(1, bound); + Library library = loader.findLibrary(libraryName); + if (library != null) { + arg = getPathOf(library.getPath(env)); + } else { + arg = getPathOf(Library.mavenNameToPath(libraryName)); + } + } else if (start == '&' && end == '&') { + String localFileName = arg.substring(1, bound); + + if (localFiles.containsKey(localFileName)) { + arg = localFiles.get(localFileName).getLocation().getAbsolutePath(); + } else { + arg = localFileName; + } + } else if (start == '\'' && end == '\'') { + arg = arg.substring(1, bound); + } else { + return 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/LocalLoader.java b/launcher/src/main/java/com/skcraft/launcher/model/loader/LocalLoader.java new file mode 100644 index 0000000..d6ff587 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/loader/LocalLoader.java @@ -0,0 +1,12 @@ +package com.skcraft.launcher.model.loader; + +import com.skcraft.launcher.model.modpack.DownloadableFile; +import lombok.Data; + +import java.util.HashMap; + +@Data +public class LocalLoader { + private final LoaderManifest manifest; + private final HashMap localFiles; +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/loader/MavenUrl.java b/launcher/src/main/java/com/skcraft/launcher/model/loader/MavenUrl.java new file mode 100644 index 0000000..fdc6ca4 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/loader/MavenUrl.java @@ -0,0 +1,24 @@ +package com.skcraft.launcher.model.loader; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.skcraft.launcher.model.minecraft.Library; +import lombok.Data; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class MavenUrl { + @JsonProperty("maven") + private String name; + private String url; + private String version; + private boolean stable; + + public Library toLibrary() { + Library library = new Library(); + library.setName(name); + library.setUrl(url); + + return library; + } +} 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 new file mode 100644 index 0000000..33e7319 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/loader/ProcessorEntry.java @@ -0,0 +1,27 @@ +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; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@EqualsAndHashCode(callSuper = false) +public class ProcessorEntry extends ManifestEntry { + private String loaderName; + private InstallProcessor processor; + + @Override + public void install(Installer installer, InstallLog log, UpdateCache cache, InstallExtras extras) throws Exception { + LocalLoader loader = extras.getLoader(loaderName); + + 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/QuiltMod.java b/launcher/src/main/java/com/skcraft/launcher/model/loader/QuiltMod.java new file mode 100644 index 0000000..6d0169b --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/loader/QuiltMod.java @@ -0,0 +1,29 @@ +package com.skcraft.launcher.model.loader; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class QuiltMod implements Versionable { + @JsonProperty("schema_version") + private int schemaVersion; + + @JsonProperty("quilt_loader") + private Mod meta; + + @Override + public String getVersion() { + return meta.getVersion(); + } + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Mod { + private String version; + + @JsonProperty("intermediate_mappings") + private String intermediateMappings; + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/loader/SidedData.java b/launcher/src/main/java/com/skcraft/launcher/model/loader/SidedData.java new file mode 100644 index 0000000..ea06805 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/loader/SidedData.java @@ -0,0 +1,28 @@ +package com.skcraft.launcher.model.loader; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.skcraft.launcher.model.minecraft.Side; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor(staticName = "create") +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public class SidedData { + private T client; + private T server; + + public T resolveFor(Side side) { + switch (side) { + case CLIENT: return client; + case SERVER: return server; + default: return null; + } + } + + public static SidedData of(T singleValue) { + return new SidedData(singleValue, singleValue); + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/loader/VersionInfo.java b/launcher/src/main/java/com/skcraft/launcher/model/loader/VersionInfo.java new file mode 100644 index 0000000..ecc1ba8 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/loader/VersionInfo.java @@ -0,0 +1,41 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.model.loader; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.google.common.base.Splitter; +import com.skcraft.launcher.model.minecraft.GameArgument; +import com.skcraft.launcher.model.minecraft.Library; +import com.skcraft.launcher.model.minecraft.MinecraftArguments; +import com.skcraft.launcher.model.minecraft.VersionManifest; +import lombok.Data; + +import java.util.List; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class VersionInfo { + private String id; + private MinecraftArguments arguments; + private String mainClass; + private List libraries; + private SidedData logging; + + @JsonIgnore private transient boolean overridingArguments; + + public void setMinecraftArguments(String argumentString) { + MinecraftArguments minecraftArguments = new MinecraftArguments(); + + for (String arg : Splitter.on(' ').split(argumentString)) { + minecraftArguments.getGameArguments().add(new GameArgument(arg)); + } + + setArguments(minecraftArguments); + setOverridingArguments(true); + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/loader/Versionable.java b/launcher/src/main/java/com/skcraft/launcher/model/loader/Versionable.java new file mode 100644 index 0000000..b2faa2b --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/loader/Versionable.java @@ -0,0 +1,5 @@ +package com.skcraft.launcher.model.loader; + +public interface Versionable { + String getVersion(); +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/loader/profiles/LegacyInstallProfile.java b/launcher/src/main/java/com/skcraft/launcher/model/loader/profiles/LegacyInstallProfile.java new file mode 100644 index 0000000..8b0f1d5 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/loader/profiles/LegacyInstallProfile.java @@ -0,0 +1,21 @@ +package com.skcraft.launcher.model.loader.profiles; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.skcraft.launcher.model.loader.VersionInfo; +import lombok.Data; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class LegacyInstallProfile { + @JsonProperty("install") + private InstallData installData; + private VersionInfo versionInfo; + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class InstallData { + private String path; + private String filePath; + } +} 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 new file mode 100644 index 0000000..b6b9478 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/loader/profiles/ModernForgeInstallProfile.java @@ -0,0 +1,38 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.model.loader.profiles; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.google.common.base.Function; +import com.google.common.collect.Lists; +import com.skcraft.launcher.model.loader.InstallProcessor; +import com.skcraft.launcher.model.loader.ProcessorEntry; +import com.skcraft.launcher.model.loader.SidedData; +import com.skcraft.launcher.model.minecraft.Library; +import lombok.Data; + +import java.util.List; +import java.util.Map; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class ModernForgeInstallProfile { + private int spec; + private List libraries; + private List processors; + private Map> data; + private String minecraft; + + public List toProcessorEntries(final String loaderName) { + return Lists.transform(getProcessors(), new Function() { + @Override + public ProcessorEntry apply(InstallProcessor input) { + return new ProcessorEntry(loaderName, input); + } + }); + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/Asset.java b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/Asset.java new file mode 100644 index 0000000..122e9ce --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/Asset.java @@ -0,0 +1,19 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.model.minecraft; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class Asset { + + private String hash; + private int size; + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/AssetsIndex.java b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/AssetsIndex.java new file mode 100644 index 0000000..53f56a0 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/AssetsIndex.java @@ -0,0 +1,33 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.model.minecraft; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.skcraft.launcher.AssetsRoot; +import lombok.Data; +import lombok.NonNull; + +import java.io.File; +import java.util.Map; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class AssetsIndex { + + private boolean virtual; + private Map objects; + + public File getObjectPath(@NonNull AssetsRoot assetsRoot, @NonNull String name) { + Asset asset = objects.get(name); + if (asset != null) { + return assetsRoot.getObjectPath(asset); + } else { + return null; + } + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/FeatureList.java b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/FeatureList.java new file mode 100644 index 0000000..a32bcc1 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/FeatureList.java @@ -0,0 +1,36 @@ +package com.skcraft.launcher.model.minecraft; + +import com.google.common.collect.Maps; +import lombok.NoArgsConstructor; + +import java.util.Map; + +/** + * List of enabled features for Minecraft feature rules. + */ +@NoArgsConstructor +public class FeatureList { + protected Map features = Maps.newHashMap(); + + public boolean doesMatch(Map features) { + for (Map.Entry entry : features.entrySet()) { + if (!entry.getValue().equals(this.features.get(entry.getKey()))) { + return false; + } + } + + return true; + } + + public boolean hasFeature(String key) { + return features.get(key) != null && features.get(key); + } + + public static class Mutable extends FeatureList { + public void addFeature(String key, boolean value) { + features.put(key, value); + } + } + + public static final FeatureList EMPTY = new FeatureList(); +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/GameArgument.java b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/GameArgument.java new file mode 100644 index 0000000..5c4046d --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/GameArgument.java @@ -0,0 +1,54 @@ +package com.skcraft.launcher.model.minecraft; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.base.Joiner; +import com.google.common.collect.Lists; +import com.skcraft.launcher.model.minecraft.mapper.ArgumentValueDeserializer; +import com.skcraft.launcher.model.minecraft.mapper.ArgumentValueSerializer; +import com.skcraft.launcher.util.Environment; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +@NoArgsConstructor +public class GameArgument { + @JsonProperty("value") + @JsonDeserialize(using = ArgumentValueDeserializer.class) + @JsonSerialize(using = ArgumentValueSerializer.class) + private List values; + private List rules; + + public GameArgument(List values) { + this.values = values; + } + + public GameArgument(String value) { + this.values = Lists.newArrayList(value); + } + + @JsonIgnore + public String getJoinedValue() { + return Joiner.on(' ').join(values); + } + + public boolean shouldApply(Environment environment, FeatureList featureList) { + if (getRules() == null) return true; + + boolean result = false; + + for (Rule rule : rules) { + if (rule.matches(environment, featureList)) { + result = rule.isAllowed(); + } + } + + return result; + } +} 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 0000000..9cd2674 --- /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/Library.java b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/Library.java new file mode 100644 index 0000000..3945c10 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/Library.java @@ -0,0 +1,279 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.model.minecraft; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.google.common.base.Joiner; +import com.google.common.base.Splitter; +import com.google.common.collect.Lists; +import com.skcraft.launcher.util.Environment; +import lombok.Data; +import org.apache.commons.lang.builder.EqualsBuilder; +import org.apache.commons.lang.builder.HashCodeBuilder; + +import java.util.List; +import java.util.Map; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class Library { + + private String name; + private Downloads downloads; + private Map natives; + private Extract extract; + private List rules; + + // Forge-added + private String comment; + + // Custom + @JsonInclude(value = JsonInclude.Include.NON_DEFAULT) + private boolean generated; + + public boolean matches(Environment environment) { + boolean allow = false; + + if (getRules() != null) { + for (Rule rule : getRules()) { + if (rule.matches(environment, FeatureList.EMPTY)) { + allow = rule.isAllowed(); + } + } + } else { + allow = true; + } + + return allow; + } + + public String getNativeString(Environment environment) { + if (getNatives() != null) { + String nativeString; + + switch (environment.getPlatform()) { + case LINUX: + nativeString = getNatives().get("linux"); + break; + case WINDOWS: + nativeString = getNatives().get("windows"); + break; + case MAC_OS_X: + nativeString = getNatives().get("osx"); + break; + default: + return null; + } + + if (nativeString != null) { + return nativeString.replace("${arch}", environment.getArchBits()); + } + } + + return null; + } + + public void ensureDownloadsExist() { + if (getDownloads() == null) { + setServerreq(true); // BACKWARDS COMPATIBILITY + } + } + + /** + * BACKWARDS COMPATIBILITY: + * Some library definitions only come with a "name" key and don't trigger any other compatibility measures. + * Therefore, if a library has no artifacts when this is called, we call {@link #setServerreq)} to trigger + * artifact generation that assumes the source is the Minecraft libraries URL. + * There is also some special handling for natives in this function; if we have no extra artifacts (newer specs + * put this in "classifiers" in the download list) then we make up an artifact by adding a maven classifier to + * the library name and using that. + */ + public Artifact getArtifact(Environment environment) { + ensureDownloadsExist(); + + String nativeString = getNativeString(environment); + + if (nativeString != null) { + if (getDownloads().getClassifiers() == null) { + // BACKWARDS COMPATIBILITY: make up a virtual artifact + Artifact virtualArtifact = new Artifact(); + virtualArtifact.setUrl(getDownloads().getArtifact().getUrl()); + virtualArtifact.setPath(mavenNameToPath(name + ":" + nativeString)); + + return virtualArtifact; + } + + return getDownloads().getClassifiers().get(nativeString); + } else { + return getDownloads().getArtifact(); + } + } + + public String getPath(Environment environment) { + return getArtifact(environment).getPath(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Library library = (Library) o; + + EqualsBuilder builder = new EqualsBuilder(); + builder.append(name, library.getName()); + // If libraries have different natives lists, they should be separate. + builder.append(natives, library.getNatives()); + + return builder.isEquals(); + } + + @Override + public int hashCode() { + HashCodeBuilder builder = new HashCodeBuilder(45, 23); + + if (name != null) + builder.append(name); + + if (natives != null) + builder.append(natives); + + return builder.toHashCode(); + } + + @Data + public static class Extract { + private List exclude; + } + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Artifact { + private String path; + private String url; + private String sha1; + private int size; + } + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Downloads { + private Artifact artifact; + private Map classifiers; + + @JsonIgnore + public List getAllArtifacts() { + List artifacts = Lists.newArrayList(); + + if (artifact != null) + artifacts.add(artifact); + + if (classifiers != null) + artifacts.addAll(classifiers.values()); + + return artifacts; + } + } + + /** + * BACKWARDS COMPATIBILITY: + * Various sources use the old-style library specification, where there are two keys - "name" and "url", + * rather than the newer multiple-artifact style. This setter is called by Jackson when the "url" property + * is present, and uses it to create a "virtual" artifact using the URL given to us here plus the library + * name parsed out into a path. + */ + public void setUrl(String url) { + Artifact virtualArtifact = new Artifact(); + + virtualArtifact.setUrl(url); + if (getName() != null) { + virtualArtifact.setPath(mavenNameToPath(getName())); + } + + Downloads downloads = new Downloads(); + downloads.setArtifact(virtualArtifact); + + setDownloads(downloads); + } + + public void setName(String name) { + int classifierPos = name.indexOf("@"); + if (classifierPos != -1) { + // Take off classifiers + name = name.substring(0, classifierPos); + } + + this.name = name; + + // [DEEP SIGH] + // Sometimes 'name' comes after 'url', and I can't figure out how to get Jackson to enforce order + // So we have to do this silly check to make sure we have a path. + if (getDownloads() != null) { + if (getDownloads().getArtifact() == null) return; + + if (getDownloads().getArtifact().getPath() == null) { + getDownloads().getArtifact().setPath(mavenNameToPath(name)); + } + } + } + + /** + * BACKWARDS COMPATIBILITY: + * Some old Forge distributions use a parameter called "serverreq" to indicate that the dependency should + * be fetched from the Minecraft library source; this setter handles that. + */ + public void setServerreq(boolean value) { + if (value && getDownloads() == null) { + setUrl("https://libraries.minecraft.net/"); // TODO get this from properties? + } + } + + /** + * Classifier-independent library name check + * @param mavenName Maven name of a library, possibly with a classifier + * @return True if this library is named 'mavenName'. + */ + public boolean matches(String mavenName) { + int classifierPos = mavenName.indexOf('@'); + if (classifierPos != -1) { + mavenName = mavenName.substring(0, classifierPos); + } + + return this.name.equals(mavenName); + } + + public static String mavenNameToPath(String mavenName) { + List split = Splitter.on(':').splitToList(mavenName); + int size = split.size(); + + String group = split.get(0); + String name = split.get(1); + String version = split.get(2); + String extension = "jar"; + + String fileName = name + "-" + version; + + if (size > 3) { + String classifier = split.get(3); + + if (classifier.indexOf("@") != -1) { + List parts = Splitter.on('@').splitToList(classifier); + + classifier = parts.get(0); + extension = parts.get(1); + } + + fileName += "-" + classifier; + } + + fileName += "." + extension; + + return Joiner.on('/').join(group.replace('.', '/'), name, version, fileName); + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/MinecraftArguments.java b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/MinecraftArguments.java new file mode 100644 index 0000000..077f7a4 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/MinecraftArguments.java @@ -0,0 +1,30 @@ +package com.skcraft.launcher.model.minecraft; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.skcraft.launcher.model.minecraft.mapper.MinecraftArgumentsDeserializer; +import com.skcraft.launcher.model.minecraft.mapper.MinecraftArgumentsSerializer; +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class MinecraftArguments { + @JsonProperty("game") + @JsonTypeInfo(use = JsonTypeInfo.Id.NONE) + @JsonSerialize(contentUsing = MinecraftArgumentsSerializer.class) + @JsonDeserialize(contentUsing = MinecraftArgumentsDeserializer.class) + private List gameArguments = new ArrayList<>(); + + @JsonProperty("jvm") + @JsonTypeInfo(use = JsonTypeInfo.Id.NONE) + @JsonSerialize(contentUsing = MinecraftArgumentsSerializer.class) + @JsonDeserialize(contentUsing = MinecraftArgumentsDeserializer.class) + private List jvmArguments = new ArrayList<>(); +} + diff --git a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/ReleaseList.java b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/ReleaseList.java new file mode 100644 index 0000000..4cfcc1d --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/ReleaseList.java @@ -0,0 +1,43 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.model.minecraft; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; +import lombok.NonNull; + +import java.util.List; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class ReleaseList { + + private LatestReleases latest; + private List versions; + + /** + * Get a release with the given ID. + * + * @param id the ID + * @return the release + */ + public Version find(@NonNull String id) { + for (Version version : getVersions()) { + if (version.getId().equals(id)) { + return version; + } + } + return null; + } + + @Data + public static class LatestReleases { + private String snapshot; + private String release; + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/Rule.java b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/Rule.java new file mode 100644 index 0000000..097292f --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/Rule.java @@ -0,0 +1,77 @@ +package com.skcraft.launcher.model.minecraft; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.skcraft.launcher.model.minecraft.mapper.PlatformDeserializer; +import com.skcraft.launcher.model.minecraft.mapper.PlatformSerializer; +import com.skcraft.launcher.util.Environment; +import com.skcraft.launcher.util.Platform; +import lombok.Data; + +import java.util.Map; +import java.util.regex.Pattern; + +@Data +public class Rule { + private Action action; + private OS os; + private Map features; + + private boolean doesOsMatch(Environment environment) { + if (getOs() == null) { + return true; + } else { + return getOs().matches(environment); + } + } + + private boolean doFeaturesMatch(FeatureList match) { + if (getFeatures() == null) return true; + + return match.doesMatch(features); + } + + public boolean matches(Environment environment, FeatureList match) { + return doesOsMatch(environment) && doFeaturesMatch(match); + } + + @JsonIgnore + public boolean isAllowed() { + return action == Action.ALLOW; + } + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class OS { + private Platform platform; + private Pattern version; + + @JsonProperty("name") + @JsonDeserialize(using = PlatformDeserializer.class) + @JsonSerialize(using = PlatformSerializer.class) + public Platform getPlatform() { + return platform; + } + + public boolean matches(Environment environment) { + return (getPlatform() == null || getPlatform().equals(environment.getPlatform())) && + (getVersion() == null || getVersion().matcher(environment.getPlatformVersion()).matches()); + } + } + + public enum Action { + ALLOW, + DISALLOW; + + @JsonCreator + public static Action fromJson(String text) { + return valueOf(text.toUpperCase()); + } + + @JsonValue + public String toJson() { + return name().toLowerCase(); + } + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/Side.java b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/Side.java new file mode 100644 index 0000000..94079b2 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/Side.java @@ -0,0 +1,21 @@ +package com.skcraft.launcher.model.minecraft; + +public enum Side { + CLIENT("client"), + SERVER("server"); + + private String name; + + Side(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + @Override + public String toString() { + return name; + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/Version.java b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/Version.java new file mode 100644 index 0000000..a580df2 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/Version.java @@ -0,0 +1,62 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.model.minecraft; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Getter; +import lombok.NonNull; +import lombok.Setter; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class Version { + + @Getter + @Setter + @NonNull + private String id; + + @Getter + @Setter + @NonNull + private String url; + + public Version() { + } + + public Version(@NonNull String id) { + this.id = id; + } + + @JsonIgnore + public String getName() { + return id; + } + + @Override + public String toString() { + return getName(); + } + + boolean thisEquals(Version other) { + return getId().equals(other.getId()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Version version = (Version) o; + return thisEquals(version) && version.thisEquals(this); + } + + @Override + public int hashCode() { + return id.hashCode(); + } +} 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 new file mode 100644 index 0000000..f1ffa2e --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/VersionManifest.java @@ -0,0 +1,88 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.model.minecraft; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Splitter; +import com.skcraft.launcher.model.loader.SidedData; +import lombok.Data; + +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class VersionManifest { + + private String id; + private Date time; + private Date releaseTime; + private String assets; + private AssetIndex assetIndex; + private String type; + private MinecraftArguments arguments; + private String mainClass; + private int minimumLauncherVersion; + private LinkedHashSet libraries; + private JavaVersion javaVersion; + private SidedData logging; + private Map downloads = new HashMap(); + + public String getAssetId() { + return getAssetIndex() != null + ? getAssetIndex().getId() + : "legacy"; + } + + public Library findLibrary(String name) { + for (Library library : getLibraries()) { + if (library.getName().equals(name)) { + return library; + } + } + + return null; + } + + public void setMinecraftArguments(String minecraftArguments) { + MinecraftArguments result = new MinecraftArguments(); + + for (String arg : Splitter.on(' ').split(minecraftArguments)) { + result.getGameArguments().add(new GameArgument(arg)); + } + + setArguments(result); + } + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Artifact { + private String id; + private String url; + private int size; + + @JsonProperty("sha1") + private String hash; + } + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class AssetIndex { + private String id; + private String url; + } + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class LoggingConfig { + private String argument; + private Artifact file; + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/mapper/ArgumentValueDeserializer.java b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/mapper/ArgumentValueDeserializer.java new file mode 100644 index 0000000..de99313 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/mapper/ArgumentValueDeserializer.java @@ -0,0 +1,35 @@ +package com.skcraft.launcher.model.minecraft.mapper; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.exc.InvalidFormatException; +import com.fasterxml.jackson.databind.type.TypeFactory; +import com.google.common.collect.Lists; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +public class ArgumentValueDeserializer extends StdDeserializer> { + protected ArgumentValueDeserializer() { + super(TypeFactory.defaultInstance().constructCollectionType(List.class, String.class)); + } + + @Override + public List deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { + if (!jp.hasCurrentToken()) jp.nextToken(); + + if (jp.getCurrentToken() == JsonToken.START_ARRAY) { + String[] allValues = jp.readValueAs(String[].class); + return Arrays.asList(allValues); + } else if (jp.getCurrentToken() == JsonToken.VALUE_STRING) { + String value = jp.readValueAs(String.class); + return Lists.newArrayList(value); + } + + throw new InvalidFormatException(jp, "Invalid JSON type for deserializer (not string or array)", null, List.class); + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/mapper/ArgumentValueSerializer.java b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/mapper/ArgumentValueSerializer.java new file mode 100644 index 0000000..94b00b2 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/mapper/ArgumentValueSerializer.java @@ -0,0 +1,25 @@ +package com.skcraft.launcher.model.minecraft.mapper; + +import com.fasterxml.jackson.core.JsonGenerationException; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import com.fasterxml.jackson.databind.type.TypeFactory; + +import java.io.IOException; +import java.util.List; + +public class ArgumentValueSerializer extends StdSerializer> { + protected ArgumentValueSerializer() { + super(TypeFactory.defaultInstance().constructCollectionType(List.class, String.class)); + } + + @Override + public void serialize(List value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException { + if (value.size() == 1) { + jgen.writeString(value.get(0)); + } else { + provider.defaultSerializeValue(value, jgen); + } + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/mapper/MinecraftArgumentsDeserializer.java b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/mapper/MinecraftArgumentsDeserializer.java new file mode 100644 index 0000000..f297d37 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/mapper/MinecraftArgumentsDeserializer.java @@ -0,0 +1,31 @@ +package com.skcraft.launcher.model.minecraft.mapper; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.exc.InvalidFormatException; +import com.skcraft.launcher.model.minecraft.GameArgument; + +import java.io.IOException; + +public class MinecraftArgumentsDeserializer extends StdDeserializer { + protected MinecraftArgumentsDeserializer() { + super(GameArgument.class); + } + + @Override + public GameArgument deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { + if (!jp.hasCurrentToken()) jp.nextToken(); + + if (jp.getCurrentToken() == JsonToken.START_OBJECT) { + return jp.readValueAs(GameArgument.class); + } else if (jp.getCurrentToken() == JsonToken.VALUE_STRING) { + String argument = jp.getValueAsString(); + return new GameArgument(argument); + } + + throw new InvalidFormatException(jp, "Invalid JSON type for deserializer (not string or object)", null, GameArgument.class); + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/mapper/MinecraftArgumentsSerializer.java b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/mapper/MinecraftArgumentsSerializer.java new file mode 100644 index 0000000..7b579a2 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/mapper/MinecraftArgumentsSerializer.java @@ -0,0 +1,24 @@ +package com.skcraft.launcher.model.minecraft.mapper; + +import com.fasterxml.jackson.core.JsonGenerationException; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import com.skcraft.launcher.model.minecraft.GameArgument; + +import java.io.IOException; + +public class MinecraftArgumentsSerializer extends StdSerializer { + protected MinecraftArgumentsSerializer() { + super(GameArgument.class); + } + + @Override + public void serialize(GameArgument value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException { + if (value.getValues().size() == 1 && (value.getRules() == null || value.getRules().size() == 0)) { + jgen.writeString(value.getValues().get(0)); + } else { + provider.defaultSerializeValue(value, jgen); + } + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/mapper/PlatformDeserializer.java b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/mapper/PlatformDeserializer.java new file mode 100644 index 0000000..6fd9dc7 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/mapper/PlatformDeserializer.java @@ -0,0 +1,35 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.model.minecraft.mapper; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.skcraft.launcher.util.Platform; + +import java.io.IOException; + +public class PlatformDeserializer extends JsonDeserializer { + + @Override + public Platform deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) + throws IOException { + String text = jsonParser.getText(); + if (text.equalsIgnoreCase("windows")) { + return Platform.WINDOWS; + } else if (text.equalsIgnoreCase("linux")) { + return Platform.LINUX; + } else if (text.equalsIgnoreCase("solaris")) { + return Platform.SOLARIS; + } else if (text.equalsIgnoreCase("osx")) { + return Platform.MAC_OS_X; + } else { + throw new IOException("Unknown platform: " + text); + } + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/mapper/PlatformSerializer.java b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/mapper/PlatformSerializer.java new file mode 100644 index 0000000..d703734 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/mapper/PlatformSerializer.java @@ -0,0 +1,41 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.model.minecraft.mapper; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.skcraft.launcher.util.Platform; + +import java.io.IOException; + +public class PlatformSerializer extends JsonSerializer { + + @Override + public void serialize(Platform platform, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) + throws IOException, JsonProcessingException { + switch (platform) { + case WINDOWS: + jsonGenerator.writeString("windows"); + break; + case MAC_OS_X: + jsonGenerator.writeString("osx"); + break; + case LINUX: + jsonGenerator.writeString("linux"); + break; + case SOLARIS: + jsonGenerator.writeString("solaris"); + break; + case UNKNOWN: + jsonGenerator.writeNull(); + break; + } + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/modpack/BaseManifest.java b/launcher/src/main/java/com/skcraft/launcher/model/modpack/BaseManifest.java new file mode 100644 index 0000000..3aa84ea --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/modpack/BaseManifest.java @@ -0,0 +1,18 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.model.modpack; + +import lombok.Data; + +@Data +public class BaseManifest { + + private String title; + private String name; + private String version; + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/modpack/Condition.java b/launcher/src/main/java/com/skcraft/launcher/model/modpack/Condition.java new file mode 100644 index 0000000..8f0332c --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/modpack/Condition.java @@ -0,0 +1,21 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.model.modpack; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +@JsonTypeInfo(use=JsonTypeInfo.Id.NAME, include=JsonTypeInfo.As.PROPERTY, property="if") +@JsonSubTypes({ + @JsonSubTypes.Type(value = RequireAny.class, name = "requireAny"), + @JsonSubTypes.Type(value = RequireAll.class, name = "requireAll") +}) +public interface Condition { + + boolean matches(); + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/modpack/DownloadableFile.java b/launcher/src/main/java/com/skcraft/launcher/model/modpack/DownloadableFile.java new file mode 100644 index 0000000..1eddaf0 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/modpack/DownloadableFile.java @@ -0,0 +1,34 @@ +package com.skcraft.launcher.model.modpack; + +import com.skcraft.launcher.install.Installer; +import com.skcraft.launcher.model.minecraft.Side; +import lombok.Data; +import lombok.NonNull; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; + +import static com.skcraft.launcher.LauncherUtils.concat; + +@Data +public class DownloadableFile { + private String name; + private String hash; + private String location; + private Side side; + private int size; + + public LocalFile download(@NonNull Installer installer, Manifest manifest) throws MalformedURLException { + URL url = concat(manifest.getObjectsUrl(), getLocation()); + + File local = installer.getDownloader().download(url, hash, size, name); + return new LocalFile(local, name); + } + + @Data + public static class LocalFile { + private final File location; + private final String name; + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/modpack/Feature.java b/launcher/src/main/java/com/skcraft/launcher/model/modpack/Feature.java new file mode 100644 index 0000000..820e701 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/modpack/Feature.java @@ -0,0 +1,69 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.model.modpack; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIdentityInfo; +import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.annotation.ObjectIdGenerators; +import com.google.common.base.Strings; +import lombok.Data; + +@JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property="name") +@Data +public class Feature implements Comparable { + + public enum Recommendation { + STARRED, + AVOID; + + @JsonCreator + public static Recommendation fromJson(String text) { + return valueOf(text.toUpperCase()); + } + + @JsonValue + public String toJson() { + return name().toLowerCase(); + }; + }; + + private String name; + private String description; + private Recommendation recommendation; + private boolean selected; + + public Feature() { + } + + public Feature(String name, String description, boolean selected) { + this.name = name; + this.description = description; + this.selected = selected; + } + + public Feature(Feature feature) { + setName(feature.getName()); + setDescription(feature.getDescription()); + setSelected(feature.isSelected()); + } + + @Override + public int hashCode() { + return super.hashCode(); + } + + @Override + public boolean equals(Object other) { + return super.equals(other); + } + + @Override + public int compareTo(Feature o) { + return Strings.nullToEmpty(getName()).compareTo(Strings.nullToEmpty(o.getName())); + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/modpack/FileInstall.java b/launcher/src/main/java/com/skcraft/launcher/model/modpack/FileInstall.java new file mode 100644 index 0000000..b7c4cd6 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/modpack/FileInstall.java @@ -0,0 +1,92 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.model.modpack; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.google.common.hash.HashFunction; +import com.google.common.hash.Hashing; +import com.google.common.io.Files; +import com.skcraft.launcher.install.*; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NonNull; +import org.apache.commons.io.FilenameUtils; + +import java.io.File; +import java.io.IOException; +import java.net.URL; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.skcraft.launcher.LauncherUtils.concat; + +@Data +@EqualsAndHashCode(callSuper = false) +public class FileInstall extends ManifestEntry { + + private static HashFunction hf = Hashing.sha1(); + private String version; + private String hash; + private String location; + private String to; + private long size; + private boolean userFile; + + @JsonIgnore + public String getImpliedVersion() { + return checkNotNull(version != null ? version : hash); + } + + @JsonIgnore + public String getTargetPath() { + return checkNotNull(this.to != null ? this.to : location); + } + + @Override + public void install(@NonNull Installer installer, @NonNull InstallLog log, + @NonNull UpdateCache cache, InstallExtras extras) throws IOException { + if (getWhen() != null && !getWhen().matches()) { + return; + } + + String targetPath = getTargetPath(); + File targetFile = new File(extras.getContentDir(), targetPath); + String fileVersion = getImpliedVersion(); + URL url = concat(getManifest().getObjectsUrl(), getLocation()); + + if (shouldUpdate(cache, targetFile)) { + long size = this.size; + if (size <= 0) { + size = 10 * 1024; + } + + File tempFile = installer.getDownloader().download(url, fileVersion, size, to); + installer.queue(new InstallLogFileMover(log, tempFile, targetFile)); + } else { + log.add(to, to); + } + } + + private boolean shouldUpdate(UpdateCache cache, File targetFile) throws IOException { + if (targetFile.exists() && isUserFile()) { + return false; + } + + if (!targetFile.exists()) { + return true; + } + + if (hash != null) { + String existingHash = Files.hash(targetFile, hf).toString(); + if (existingHash.equalsIgnoreCase(hash)) { + return false; + } + } + + return cache.mark(FilenameUtils.normalize(getTargetPath()), getImpliedVersion()); + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/modpack/LaunchModifier.java b/launcher/src/main/java/com/skcraft/launcher/model/modpack/LaunchModifier.java new file mode 100644 index 0000000..5ad0226 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/modpack/LaunchModifier.java @@ -0,0 +1,31 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.model.modpack; + +import com.google.common.collect.Lists; +import com.skcraft.launcher.launch.JavaProcessBuilder; +import lombok.Data; + +import java.util.List; + +@Data +public class LaunchModifier { + + private List flags = Lists.newArrayList(); + + public void setFlags(List flags) { + this.flags = flags != null ? flags : Lists.newArrayList(); + } + + public void modify(JavaProcessBuilder builder) { + if (flags != null) { + for (String flag : flags) { + builder.getFlags().add(flag); + } + } + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/modpack/Manifest.java b/launcher/src/main/java/com/skcraft/launcher/model/modpack/Manifest.java new file mode 100644 index 0000000..49ec313 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/modpack/Manifest.java @@ -0,0 +1,98 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.model.modpack; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonManagedReference; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Strings; +import com.skcraft.launcher.Instance; +import com.skcraft.launcher.LauncherUtils; +import com.skcraft.launcher.install.Installer; +import com.skcraft.launcher.model.loader.LoaderManifest; +import com.skcraft.launcher.model.minecraft.VersionManifest; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Data +@EqualsAndHashCode(callSuper = true) +public class Manifest extends BaseManifest { + + public static final int MIN_PROTOCOL_VERSION = 3; + + private int minimumVersion; + private URL baseUrl; + private String librariesLocation; + private String objectsLocation; + private String gameVersion; + @JsonProperty("launch") + private LaunchModifier launchModifier; + private List features = new ArrayList(); + @JsonManagedReference("manifest") + private List tasks = new ArrayList(); + @Getter @Setter @JsonIgnore + private Installer installer; + private VersionManifest versionManifest; + private Map loaders = new HashMap(); + + @JsonIgnore + public URL getLibrariesUrl() { + if (Strings.nullToEmpty(getLibrariesLocation()) == null) { + return null; + } + + try { + return LauncherUtils.concat(baseUrl, Strings.nullToEmpty(getLibrariesLocation()) + "/"); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } + + @JsonIgnore + public URL getObjectsUrl() { + if (Strings.nullToEmpty(getObjectsLocation()) == null) { + return baseUrl; + } + + try { + return LauncherUtils.concat(baseUrl, Strings.nullToEmpty(getObjectsLocation()) + "/"); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } + + public void updateName(String name) { + if (name != null) { + setName(name); + } + } + + public void updateTitle(String title) { + if (title != null) { + setTitle(title); + } + } + + public void updateGameVersion(String gameVersion) { + if (gameVersion != null) { + setGameVersion(gameVersion); + } + } + + public void update(Instance instance) { + instance.setLaunchModifier(getLaunchModifier()); + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/modpack/ManifestEntry.java b/launcher/src/main/java/com/skcraft/launcher/model/modpack/ManifestEntry.java new file mode 100644 index 0000000..776e450 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/modpack/ManifestEntry.java @@ -0,0 +1,39 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.model.modpack; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.skcraft.launcher.install.InstallExtras; +import com.skcraft.launcher.install.InstallLog; +import com.skcraft.launcher.install.Installer; +import com.skcraft.launcher.install.UpdateCache; +import com.skcraft.launcher.model.loader.ProcessorEntry; +import lombok.Data; +import lombok.ToString; + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type", + defaultImpl = FileInstall.class) +@JsonSubTypes({ + @JsonSubTypes.Type(value = FileInstall.class, name = "file"), + @JsonSubTypes.Type(value = ProcessorEntry.class, name = "process") +}) +@Data +@ToString(exclude = "manifest") +public abstract class ManifestEntry { + + @JsonBackReference("manifest") + private Manifest manifest; + private Condition when; + + public abstract void install(Installer installer, InstallLog log, UpdateCache cache, InstallExtras extras) throws Exception; + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/modpack/ManifestInfo.java b/launcher/src/main/java/com/skcraft/launcher/model/modpack/ManifestInfo.java new file mode 100644 index 0000000..6d63f48 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/modpack/ManifestInfo.java @@ -0,0 +1,30 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.model.modpack; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class ManifestInfo extends BaseManifest implements Comparable { + + private String location; + private int priority; + + @Override + public int compareTo(ManifestInfo o) { + if (priority > o.getPriority()) { + return -1; + } else if (priority < o.getPriority()) { + return 1; + } else { + return 0; + } + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/modpack/PackageList.java b/launcher/src/main/java/com/skcraft/launcher/model/modpack/PackageList.java new file mode 100644 index 0000000..4268d9e --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/modpack/PackageList.java @@ -0,0 +1,21 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.model.modpack; + +import lombok.Data; + +import java.util.List; + +@Data +public class PackageList { + + public static final int MIN_VERSION = 1; + + private int minimumVersion; + private List packages; + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/modpack/RequireAll.java b/launcher/src/main/java/com/skcraft/launcher/model/modpack/RequireAll.java new file mode 100644 index 0000000..ce84174 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/modpack/RequireAll.java @@ -0,0 +1,46 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.model.modpack; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@Data +public class RequireAll implements Condition { + + private List features = new ArrayList(); + + public RequireAll() { + } + + public RequireAll(List features) { + this.features = features; + } + + public RequireAll(Feature... feature) { + features.addAll(Arrays.asList(feature)); + } + + @Override + public boolean matches() { + if (features == null) { + return true; + } + + for (Feature feature : features) { + if (!feature.isSelected()) { + return false; + } + } + + return true; + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/modpack/RequireAny.java b/launcher/src/main/java/com/skcraft/launcher/model/modpack/RequireAny.java new file mode 100644 index 0000000..299595a --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/modpack/RequireAny.java @@ -0,0 +1,46 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.model.modpack; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@Data +public class RequireAny implements Condition { + + private List features = new ArrayList(); + + public RequireAny() { + } + + public RequireAny(List features) { + this.features = features; + } + + public RequireAny(Feature... feature) { + features.addAll(Arrays.asList(feature)); + } + + @Override + public boolean matches() { + if (features == null) { + return true; + } + + for (Feature feature : features) { + if (feature.isSelected()) { + return true; + } + } + + return false; + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/persistence/MkdirByteSink.java b/launcher/src/main/java/com/skcraft/launcher/persistence/MkdirByteSink.java new file mode 100644 index 0000000..e76be6c --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/persistence/MkdirByteSink.java @@ -0,0 +1,31 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.persistence; + +import com.google.common.io.ByteSink; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; + +class MkdirByteSink extends ByteSink { + + private final ByteSink delegate; + private final File dir; + + public MkdirByteSink(ByteSink delegate, File dir) { + this.delegate = delegate; + this.dir = dir; + } + + @Override + public OutputStream openStream() throws IOException { + dir.mkdirs(); + return delegate.openStream(); + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/persistence/Persistence.java b/launcher/src/main/java/com/skcraft/launcher/persistence/Persistence.java new file mode 100644 index 0000000..529a2b5 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/persistence/Persistence.java @@ -0,0 +1,252 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.persistence; + +import com.fasterxml.jackson.core.PrettyPrinter; +import com.fasterxml.jackson.core.util.DefaultIndenter; +import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.io.ByteSink; +import com.google.common.io.ByteSource; +import com.google.common.io.Closer; +import com.google.common.io.Files; +import lombok.NonNull; +import lombok.extern.java.Log; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.OutputStream; +import java.util.WeakHashMap; +import java.util.logging.Level; + +/** + * Simple persistence framework that can read an object from a file, bind + * that object to that file, and allow any code having a reference to the + * object make changes to the object and save those changes back to disk. + *

+ * For example: + *
config = Persistence.load(file, Configuration.class);
+ * config.changeSomething();
+ * Persistence.commit(config);
+ */ +@Log +public final class Persistence { + + private static final ObjectMapper mapper = new ObjectMapper(); + private static final WeakHashMap bound = new WeakHashMap(); + public static final DefaultPrettyPrinter L2F_LIST_PRETTY_PRINTER; + + static { + L2F_LIST_PRETTY_PRINTER = new DefaultPrettyPrinter(); + L2F_LIST_PRETTY_PRINTER.indentArraysWith(DefaultIndenter.SYSTEM_LINEFEED_INSTANCE); + } + + private Persistence() { + } + + /** + * Bind an object to a path where the object will be saved. + * + * @param object the object + * @param sink the byte sink + */ + public static void bind(@NonNull Object object, @NonNull ByteSink sink) { + synchronized (bound) { + bound.put(object, sink); + } + } + + /** + * Save an object to file. + * + * @param object the object + * @throws java.io.IOException on save error + */ + public static void commit(@NonNull Object object) throws IOException { + ByteSink sink; + synchronized (bound) { + sink = bound.get(object); + if (sink == null) { + throw new IOException("Cannot persist unbound object: " + object); + } + } + + Closer closer = Closer.create(); + try { + OutputStream os = closer.register(sink.openBufferedStream()); + mapper.writeValue(os, object); + } finally { + closer.close(); + } + } + + /** + * Save an object to file, and send all errors to the log. + * + * @param object the object + */ + public static void commitAndForget(@NonNull Object object) { + try { + commit(object); + } catch (IOException e) { + log.log(Level.WARNING, "Failed to save " + object.getClass() + ": " + object.toString(), e); + } + } + + /** + * Read an object from a byte source, without binding it. + * + * @param source byte source + * @param cls the class + * @param returnNull true to return null if the object could not be loaded + * @param the type of class + * @return an object + */ + public static V read(ByteSource source, Class cls, boolean returnNull) { + V object; + Closer closer = Closer.create(); + + try { + object = mapper.readValue(closer.register(source.openBufferedStream()), cls); + } catch (IOException e) { + if (!(e instanceof FileNotFoundException)) { + log.log(Level.INFO, "Failed to load" + cls.getCanonicalName(), e); + } + + if (returnNull) { + return null; + } + + try { + object = cls.newInstance(); + } catch (InstantiationException e1) { + throw new RuntimeException( + "Failed to construct object with no-arg constructor", e1); + } catch (IllegalAccessException e1) { + throw new RuntimeException( + "Failed to construct object with no-arg constructor", e1); + } + } finally { + try { + closer.close(); + } catch (IOException e) { + } + } + + return object; + } + + /** + * Read an object from file, without binding it. + * + * @param file the file + * @param cls the class + * @param returnNull true to return null if the object could not be loaded + * @param the type of class + * @return an object + */ + public static V read(File file, Class cls, boolean returnNull) { + return read(Files.asByteSource(file), cls, returnNull); + } + + + /** + * Read an object from file, without binding it. + * + * @param file the file + * @param cls the class + * @param the type of class + * @return an object + */ + public static V read(File file, Class cls) { + return read(file, cls, false); + } + + /** + * Read an object from file. + * + * @param file the file + * @param cls the class + * @param returnNull true to return null if the object could not be loaded + * @param the type of class + * @return an object + */ + public static V load(File file, Class cls, boolean returnNull) { + ByteSource source = Files.asByteSource(file); + ByteSink sink = new MkdirByteSink(Files.asByteSink(file), file.getParentFile()); + + Scrambled scrambled = cls.getAnnotation(Scrambled.class); + if (cls.getAnnotation(Scrambled.class) != null) { + source = new ScramblingSourceFilter(source, scrambled.value()); + sink = new ScramblingSinkFilter(sink, scrambled.value()); + } + + V object = read(source, cls, returnNull); + Persistence.bind(object, sink); + return object; + } + + /** + * Read an object from file. + * + *

If the file does not exist or loading fails, construct a new instance of + * the given class by using its no-arg constructor.

+ * + * @param file the file + * @param cls the class + * @param the type of class + * @return an object + */ + public static V load(File file, Class cls) { + return load(file, cls, false); + } + + /** + * Write an object to file. + * + * @param file the file + * @param object the object + * @throws java.io.IOException on I/O error + */ + public static void write(File file, Object object) throws IOException { + write(file, object, null); + } + + /** + * Write an object to file. + * + * @param file the file + * @param object the object + * @param prettyPrinter a pretty printer to use, or null + * @throws java.io.IOException on I/O error + */ + public static void write(File file, Object object, PrettyPrinter prettyPrinter) throws IOException { + file.getParentFile().mkdirs(); + if (prettyPrinter != null) { + mapper.writer(prettyPrinter).writeValue(file, object); + } else { + mapper.writeValue(file, object); + } + } + + /** + * Write an object to a string. + * + * @param object the object + * @param prettyPrinter a pretty printer to use, or null + * @throws java.io.IOException on I/O error + */ + public static String writeValueAsString(Object object, PrettyPrinter prettyPrinter) throws IOException { + if (prettyPrinter != null) { + return mapper.writer(prettyPrinter).writeValueAsString(object); + } else { + return mapper.writeValueAsString(object); + } + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/persistence/Scrambled.java b/launcher/src/main/java/com/skcraft/launcher/persistence/Scrambled.java new file mode 100644 index 0000000..672bbb0 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/persistence/Scrambled.java @@ -0,0 +1,47 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.persistence; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Classes that are annotated with this will be saved scrambled + * to disk when saved using {@link com.skcraft.launcher.persistence.Persistence}. + *

+ * The data may be scrambled using an encryption algorithm, but it's not + * done with security in mind. Decryption requires a key, and that + * key would either have to be stored in the source code, defeating the + * purpose of encryption, or the user would have to be prompted with a + * password every time (possibly through an OS key ring service). + * That creates extra hassle, so it is not done here. + *

+ * Therefore , you should not depend on data that is scrambled to + * be secure. It does, however, make it impossible for most people + * to just read the file's contents, which is "better than nothing" + * Documentation should not indicate to the user that the data is + * protected however, because that would provide a false sense of + * security. + *

+ * Account data is scrambled to make it harder to extract passwords. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Scrambled { + + /** + * A key used in scrambling. + *

+ * The key should not change once deployed. + * + * @return a key + */ + String value(); + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/persistence/ScramblingSinkFilter.java b/launcher/src/main/java/com/skcraft/launcher/persistence/ScramblingSinkFilter.java new file mode 100644 index 0000000..862a6e9 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/persistence/ScramblingSinkFilter.java @@ -0,0 +1,58 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.persistence; + +import com.google.common.io.ByteSink; + +import javax.crypto.*; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.PBEParameterSpec; +import java.io.IOException; +import java.io.OutputStream; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.util.Random; + +class ScramblingSinkFilter extends ByteSink { + + private final ByteSink delegate; + private final String key; + + public ScramblingSinkFilter(ByteSink delegate, String key) { + this.delegate = delegate; + this.key = key; + } + + @Override + public OutputStream openStream() throws IOException { + Cipher cipher = null; + try { + cipher = getCipher(Cipher.ENCRYPT_MODE, key); + } catch (Throwable e) { + throw new IOException("Failed to create cipher", e); + } + return new CipherOutputStream(delegate.openStream(), cipher); + } + + public static Cipher getCipher(int mode, String password) + throws InvalidKeySpecException, NoSuchAlgorithmException, + NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException { + // These parameters were used for encrypting lastlogin on old official Minecraft launchers + Random random = new Random(0x29482c2L); + byte salt[] = new byte[8]; + random.nextBytes(salt); + PBEParameterSpec paramSpec = new PBEParameterSpec(salt, 5); + SecretKeyFactory factory = SecretKeyFactory.getInstance("PBEWithMD5AndDES"); + SecretKey key = factory.generateSecret(new PBEKeySpec(password.toCharArray())); + Cipher cipher = Cipher.getInstance("PBEWithMD5AndDES"); + cipher.init(mode, key, paramSpec); + return cipher; + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/persistence/ScramblingSourceFilter.java b/launcher/src/main/java/com/skcraft/launcher/persistence/ScramblingSourceFilter.java new file mode 100644 index 0000000..fdfa03b --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/persistence/ScramblingSourceFilter.java @@ -0,0 +1,37 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.persistence; + +import com.google.common.io.ByteSource; + +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import java.io.IOException; +import java.io.InputStream; + +class ScramblingSourceFilter extends ByteSource { + + private final ByteSource delegate; + private final String key; + + public ScramblingSourceFilter(ByteSource delegate, String key) { + this.delegate = delegate; + this.key = key; + } + + @Override + public InputStream openStream() throws IOException { + Cipher cipher = null; + try { + cipher = ScramblingSinkFilter.getCipher(Cipher.DECRYPT_MODE, key); + } catch (Throwable e) { + throw new IOException("Failed to create cipher", e); + } + return new CipherInputStream(delegate.openStream(), cipher); + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/selfupdate/ComparableVersion.java b/launcher/src/main/java/com/skcraft/launcher/selfupdate/ComparableVersion.java new file mode 100644 index 0000000..1adcf30 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/selfupdate/ComparableVersion.java @@ -0,0 +1,366 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package com.skcraft.launcher.selfupdate; + +import java.util.*; + +/** + * Generic implementation of version comparison. + *

+ * NOTE: This class is a copy of r658725 of http://svn.apache.org/repos/asf/maven/artifact/trunk/src/main/java/org/apache/maven/artifact/versioning/ComparableVersion.java. + * + * @author Kenney Westerhof + * @author Herve Boutemy + * @version $Id$ + */ +@SuppressWarnings("unchecked") +public class ComparableVersion + implements Comparable { + private String value; + + private String canonical; + + private ListItem items; + + private interface Item { + public static final int INTEGER_ITEM = 0; + public static final int STRING_ITEM = 1; + public static final int LIST_ITEM = 2; + + public int compareTo(Item item); + + public int getType(); + + public boolean isNull(); + } + + /** + * Represents a numeric item in the version item list. + */ + private static class IntegerItem + implements Item { + private Integer value; + + public IntegerItem(Integer i) { + this.value = i; + } + + public int getType() { + return INTEGER_ITEM; + } + + public boolean isNull() { + return (value == 0); + } + + public int compareTo(Item item) { + if (item == null) { + return value == 0 ? 0 : 1; // 1.0 == 1, 1.1 > 1 + } + + switch (item.getType()) { + case INTEGER_ITEM: + return value.compareTo(((IntegerItem) item).value); + + case STRING_ITEM: + return 1; // 1.1 > 1-sp + + case LIST_ITEM: + return 1; // 1.1 > 1-1 + + default: + throw new RuntimeException("invalid item: " + item.getClass()); + } + } + + public String toString() { + return value.toString(); + } + } + + /** + * Represents a string in the version item list, usually a qualifier. + */ + private static class StringItem + implements Item { + private final static String[] QUALIFIERS = {"snapshot", "alpha", "beta", "milestone", "rc", "", "sp"}; + + private final static List _QUALIFIERS = Arrays.asList(QUALIFIERS); + + private final static Properties ALIASES = new Properties(); + + static { + ALIASES.put("ga", ""); + ALIASES.put("final", ""); + ALIASES.put("cr", "rc"); + } + + /** + * A comparable for the empty-string qualifier. This one is used to determine if a given qualifier makes the + * version older than one without a qualifier, or more recent. + */ + private static Comparable RELEASE_VERSION_INDEX = String.valueOf(_QUALIFIERS.indexOf("")); + + private String value; + + public StringItem(String value, boolean followedByDigit) { + if (followedByDigit && value.length() == 1) { + // a1 = alpha-1, b1 = beta-1, m1 = milestone-1 + switch (value.charAt(0)) { + case 'a': + value = "alpha"; + break; + case 'b': + value = "beta"; + break; + case 'm': + value = "milestone"; + break; + } + } + this.value = ALIASES.getProperty(value, value); + } + + public int getType() { + return STRING_ITEM; + } + + public boolean isNull() { + return (comparableQualifier(value).compareTo(RELEASE_VERSION_INDEX) == 0); + } + + /** + * Returns a comparable for a qualifier. + *

+ * This method both takes into account the ordering of known qualifiers as well as lexical ordering for unknown + * qualifiers. + *

+ * just returning an Integer with the index here is faster, but requires a lot of if/then/else to check for -1 + * or QUALIFIERS.size and then resort to lexical ordering. Most comparisons are decided by the first character, + * so this is still fast. If more characters are needed then it requires a lexical sort anyway. + * + * @param qualifier + * @return + */ + public static Comparable comparableQualifier(String qualifier) { + int i = _QUALIFIERS.indexOf(qualifier); + + return i == -1 ? _QUALIFIERS.size() + "-" + qualifier : String.valueOf(i); + } + + public int compareTo(Item item) { + if (item == null) { + // 1-rc < 1, 1-ga > 1 + return comparableQualifier(value).compareTo(RELEASE_VERSION_INDEX); + } + switch (item.getType()) { + case INTEGER_ITEM: + return -1; // 1.any < 1.1 ? + + case STRING_ITEM: + return comparableQualifier(value).compareTo(comparableQualifier(((StringItem) item).value)); + + case LIST_ITEM: + return -1; // 1.any < 1-1 + + default: + throw new RuntimeException("invalid item: " + item.getClass()); + } + } + + public String toString() { + return value; + } + } + + /** + * Represents a version list item. This class is used both for the global item list and for sub-lists (which start + * with '-(number)' in the version specification). + */ + private static class ListItem + extends ArrayList + implements Item { + public int getType() { + return LIST_ITEM; + } + + public boolean isNull() { + return (size() == 0); + } + + void normalize() { + for (ListIterator iterator = listIterator(size()); iterator.hasPrevious(); ) { + Item item = (Item) iterator.previous(); + if (item.isNull()) { + iterator.remove(); // remove null trailing items: 0, "", empty list + } else { + break; + } + } + } + + public int compareTo(Item item) { + if (item == null) { + if (size() == 0) { + return 0; // 1-0 = 1- (normalize) = 1 + } + Item first = (Item) get(0); + return first.compareTo(null); + } + switch (item.getType()) { + case INTEGER_ITEM: + return -1; // 1-1 < 1.0.x + + case STRING_ITEM: + return 1; // 1-1 > 1-sp + + case LIST_ITEM: + Iterator left = iterator(); + Iterator right = ((ListItem) item).iterator(); + + while (left.hasNext() || right.hasNext()) { + Item l = left.hasNext() ? (Item) left.next() : null; + Item r = right.hasNext() ? (Item) right.next() : null; + + // if this is shorter, then invert the compare and mul with -1 + int result = l == null ? -1 * r.compareTo(l) : l.compareTo(r); + + if (result != 0) { + return result; + } + } + + return 0; + + default: + throw new RuntimeException("invalid item: " + item.getClass()); + } + } + + public String toString() { + StringBuffer buffer = new StringBuffer("("); + for (Iterator iter = iterator(); iter.hasNext(); ) { + buffer.append(iter.next()); + if (iter.hasNext()) { + buffer.append(','); + } + } + buffer.append(')'); + return buffer.toString(); + } + } + + public ComparableVersion(String version) { + parseVersion(version); + } + + public final void parseVersion(String version) { + this.value = version; + + items = new ListItem(); + + version = version.toLowerCase(Locale.ENGLISH); + + ListItem list = items; + + Stack stack = new Stack(); + stack.push(list); + + boolean isDigit = false; + + int startIndex = 0; + + for (int i = 0; i < version.length(); i++) { + char c = version.charAt(i); + + if (c == '.') { + if (i == startIndex) { + list.add(new IntegerItem(0)); + } else { + list.add(parseItem(isDigit, version.substring(startIndex, i))); + } + startIndex = i + 1; + } else if (c == '-') { + if (i == startIndex) { + list.add(new IntegerItem(0)); + } else { + list.add(parseItem(isDigit, version.substring(startIndex, i))); + } + startIndex = i + 1; + + if (isDigit) { + list.normalize(); // 1.0-* = 1-* + + if ((i + 1 < version.length()) && Character.isDigit(version.charAt(i + 1))) { + // new ListItem only if previous were digits and new char is a digit, + // ie need to differentiate only 1.1 from 1-1 + list.add(list = new ListItem()); + + stack.push(list); + } + } + } else if (Character.isDigit(c)) { + if (!isDigit && i > startIndex) { + list.add(new StringItem(version.substring(startIndex, i), true)); + startIndex = i; + } + + isDigit = true; + } else { + if (isDigit && i > startIndex) { + list.add(parseItem(true, version.substring(startIndex, i))); + startIndex = i; + } + + isDigit = false; + } + } + + if (version.length() > startIndex) { + list.add(parseItem(isDigit, version.substring(startIndex))); + } + + while (!stack.isEmpty()) { + list = (ListItem) stack.pop(); + list.normalize(); + } + + canonical = items.toString(); + } + + private static Item parseItem(boolean isDigit, String buf) { + return isDigit ? new IntegerItem(new Integer(buf)) : new StringItem(buf, false); + } + + public int compareTo(Object o) { + return items.compareTo(((ComparableVersion) o).items); + } + + public String toString() { + return value; + } + + public boolean equals(Object o) { + return (o instanceof ComparableVersion) && canonical.equals(((ComparableVersion) o).canonical); + } + + public int hashCode() { + return canonical.hashCode(); + } +} \ No newline at end of file diff --git a/launcher/src/main/java/com/skcraft/launcher/selfupdate/LatestVersionInfo.java b/launcher/src/main/java/com/skcraft/launcher/selfupdate/LatestVersionInfo.java new file mode 100644 index 0000000..1b14b84 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/selfupdate/LatestVersionInfo.java @@ -0,0 +1,22 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.selfupdate; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; + +import java.net.URL; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class LatestVersionInfo { + + private String version; + private URL url; + private boolean packed; + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/selfupdate/SelfUpdater.java b/launcher/src/main/java/com/skcraft/launcher/selfupdate/SelfUpdater.java new file mode 100644 index 0000000..c23b1a3 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/selfupdate/SelfUpdater.java @@ -0,0 +1,69 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.selfupdate; + +import com.skcraft.concurrency.DefaultProgress; +import com.skcraft.concurrency.ProgressObservable; +import com.skcraft.launcher.Launcher; +import com.skcraft.launcher.install.FileMover; +import com.skcraft.launcher.install.Installer; +import com.skcraft.launcher.util.SharedLocale; +import lombok.NonNull; + +import java.io.File; +import java.net.URL; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class SelfUpdater implements Callable, ProgressObservable { + + private final Launcher launcher; + private final URL url; + private final Installer installer; + private ProgressObservable progress = new DefaultProgress(0, SharedLocale.tr("updater.updating")); + + public SelfUpdater(@NonNull Launcher launcher, @NonNull URL url) { + this.launcher = launcher; + this.url = url; + this.installer = new Installer(launcher.getInstallerDir()); + } + + @Override + public File call() throws Exception { + ExecutorService executor = Executors.newSingleThreadExecutor(); + + try { + File dir = launcher.getLauncherBinariesDir(); + File file = new File(dir, System.currentTimeMillis() + ".jar"); + File tempFile = installer.getDownloader().download(url, "", 10000, "launcher.jar"); + + progress = installer.getDownloader(); + installer.download(); + + installer.queue(new FileMover(tempFile, file)); + + progress = installer; + installer.execute(launcher); + + return file; + } finally { + executor.shutdownNow(); + } + } + + @Override + public double getProgress() { + return progress.getProgress(); + } + + @Override + public String getStatus() { + return progress.getStatus(); + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/selfupdate/UpdateChecker.java b/launcher/src/main/java/com/skcraft/launcher/selfupdate/UpdateChecker.java new file mode 100644 index 0000000..2acfc1e --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/selfupdate/UpdateChecker.java @@ -0,0 +1,62 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.selfupdate; + +import com.skcraft.launcher.Launcher; +import com.skcraft.launcher.LauncherException; +import com.skcraft.launcher.util.HttpRequest; +import com.skcraft.launcher.util.SharedLocale; +import lombok.NonNull; +import lombok.extern.java.Log; + +import java.net.URL; +import java.util.concurrent.Callable; + +/** + * A worker that checks for an update to the launcher. A URL is returned + * if there is an update to be downloaded. + */ +@Log +public class UpdateChecker implements Callable { + + private final Launcher launcher; + + public UpdateChecker(@NonNull Launcher launcher) { + this.launcher = launcher; + } + + @Override + public LatestVersionInfo call() throws Exception { + try { + UpdateChecker.log.info("Checking for update..."); + + URL url = HttpRequest.url(launcher.getProperties().getProperty("selfUpdateUrl")); + + LatestVersionInfo versionInfo = HttpRequest.get(url) + .execute() + .expectResponseCode(200) + .returnContent() + .asJson(LatestVersionInfo.class); + + ComparableVersion current = new ComparableVersion(launcher.getVersion()); + ComparableVersion latest = new ComparableVersion(versionInfo.getVersion()); + + UpdateChecker.log.info("Latest version is " + latest + ", while current is " + current); + + if (latest.compareTo(current) >= 1) { + UpdateChecker.log.info("Update available at " + versionInfo.getUrl()); + return versionInfo; + } else { + UpdateChecker.log.info("No update required."); + return null; + } + } catch (Exception e) { + throw new LauncherException(e, SharedLocale.tr("errors.selfUpdateCheckError")); + } + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/swing/ActionListeners.java b/launcher/src/main/java/com/skcraft/launcher/swing/ActionListeners.java new file mode 100644 index 0000000..807d3ae --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/swing/ActionListeners.java @@ -0,0 +1,53 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.swing; + +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; + +/** + * Utility method to make {@link ActionListeners}. + */ +public final class ActionListeners { + + private ActionListeners() { + } + + public static ActionListener dispose(final Window window) { + return new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + window.dispose(); + } + }; + } + + public static ActionListener openURL(final Component component, final String url) { + return new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + SwingHelper.openURL(url, component); + } + }; + } + + public static ActionListener browseDir( + final Component component, final File dir, final boolean create) { + return new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + if (create) { + dir.mkdirs(); + } + SwingHelper.browseDir(dir, component); + } + }; + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/swing/CheckboxTable.java b/launcher/src/main/java/com/skcraft/launcher/swing/CheckboxTable.java new file mode 100644 index 0000000..40b91df --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/swing/CheckboxTable.java @@ -0,0 +1,23 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.swing; + +import javax.swing.*; +import javax.swing.table.TableModel; + +public class CheckboxTable extends DefaultTable { + + @Override + public void setModel(TableModel dataModel) { + super.setModel(dataModel); + try { + getColumnModel().getColumn(0).setMaxWidth((int) new JCheckBox().getPreferredSize().getWidth()); + } catch (ArrayIndexOutOfBoundsException e) { + } + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/swing/DefaultTable.java b/launcher/src/main/java/com/skcraft/launcher/swing/DefaultTable.java new file mode 100644 index 0000000..01b7fef --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/swing/DefaultTable.java @@ -0,0 +1,25 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.swing; + +import javax.swing.*; +import java.awt.*; + +/** + * The default table style used throughout the launcher. + */ +public class DefaultTable extends JTable { + + public DefaultTable() { + setShowGrid(false); + setRowHeight((int) (Math.max(getRowHeight(), new JCheckBox().getPreferredSize().getHeight() - 2))); + setIntercellSpacing(new Dimension(0, 0)); + setFillsViewportHeight(true); + setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/swing/DirectoryField.java b/launcher/src/main/java/com/skcraft/launcher/swing/DirectoryField.java new file mode 100644 index 0000000..55f8864 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/swing/DirectoryField.java @@ -0,0 +1,106 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.swing; + +import javax.swing.*; +import javax.swing.filechooser.FileFilter; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; + +public class DirectoryField extends JPanel { + + private static final long serialVersionUID = 5706210803738919578L; + + private final JTextField textField; + private final JButton browseButton; + + public DirectoryField() { + setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); + + textField = new JTextField(30); + textField.setMaximumSize(textField.getPreferredSize()); + add(textField); + + add(Box.createHorizontalStrut(3)); + + browseButton = new JButton("Browse..."); + browseButton.setPreferredSize(new Dimension( + browseButton.getPreferredSize().width, + textField.getPreferredSize().height)); + add(browseButton); + + browseButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + browse(); + } + }); + + textField.setComponentPopupMenu(TextFieldPopupMenu.INSTANCE); + } + + public JTextField getTextField() { + return textField; + } + + public JButton getBrowseButton() { + return browseButton; + } + + public void setPath(String path) { + getTextField().setText(path); + } + + public String getPath() { + return getTextField().getText(); + } + + protected JFileChooser getFileChooser() { + JFileChooser chooser = new JFileChooser(); + chooser.setDialogTitle("Select folder"); + chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + chooser.setFileFilter(new FileFilter() { + @Override + public boolean accept(File pathname) { + if (pathname.isDirectory()) return true; + return false; + } + + @Override + public String getDescription() { + return "Directories"; + } + }); + + return chooser; + } + + public void browse() { + JFileChooser chooser = getFileChooser(); + File f = new File(getPath()); + if (f.exists() && f.isFile()) { + f = f.getParentFile(); + } + chooser.setCurrentDirectory(f); + + int returnVal = chooser.showOpenDialog(this); + + if (returnVal == JFileChooser.APPROVE_OPTION) { + setPath(chooser.getSelectedFile().getPath()); + } + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + getTextField().setEnabled(enabled); + getBrowseButton().setEnabled(enabled); + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/swing/DoubleClickToButtonAdapter.java b/launcher/src/main/java/com/skcraft/launcher/swing/DoubleClickToButtonAdapter.java new file mode 100644 index 0000000..5c6a841 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/swing/DoubleClickToButtonAdapter.java @@ -0,0 +1,28 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.swing; + +import javax.swing.*; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; + +public class DoubleClickToButtonAdapter extends MouseAdapter { + + private final AbstractButton button; + + public DoubleClickToButtonAdapter(AbstractButton button) { + this.button = button; + } + + @Override + public void mouseClicked(MouseEvent e) { + if (e.getClickCount() == 2) { + button.doClick(); + } + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/swing/EmptyIcon.java b/launcher/src/main/java/com/skcraft/launcher/swing/EmptyIcon.java new file mode 100644 index 0000000..8ab1b26 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/swing/EmptyIcon.java @@ -0,0 +1,38 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.swing; + +import javax.swing.*; +import java.awt.*; + +public final class EmptyIcon implements Icon { + + private int width; + private int height; + + public EmptyIcon() { + this(0, 0); + } + + public EmptyIcon(int width, int height) { + this.width = width; + this.height = height; + } + + public int getIconHeight() { + return height; + } + + public int getIconWidth() { + return width; + } + + @Override + public void paintIcon(Component c, Graphics g, int x, int y) { + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/swing/EventQueueExecutor.java b/launcher/src/main/java/com/skcraft/launcher/swing/EventQueueExecutor.java new file mode 100644 index 0000000..3aa66f0 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/swing/EventQueueExecutor.java @@ -0,0 +1,24 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.swing; + +import java.awt.*; +import java.util.concurrent.Executor; + +public class EventQueueExecutor implements Executor { + + public static final EventQueueExecutor INSTANCE = new EventQueueExecutor(); + + private EventQueueExecutor() { + } + + @Override + public void execute(Runnable runnable) { + EventQueue.invokeLater(runnable); + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/swing/FeatureTableModel.java b/launcher/src/main/java/com/skcraft/launcher/swing/FeatureTableModel.java new file mode 100644 index 0000000..3c44bd1 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/swing/FeatureTableModel.java @@ -0,0 +1,106 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.swing; + +import com.skcraft.launcher.model.modpack.Feature; +import com.skcraft.launcher.util.SharedLocale; + +import javax.swing.table.AbstractTableModel; +import java.util.List; + +public class FeatureTableModel extends AbstractTableModel { + + private final List features; + + public FeatureTableModel(List features) { + this.features = features; + } + + @Override + public String getColumnName(int columnIndex) { + switch (columnIndex) { + case 1: + return SharedLocale.tr("features.nameColumn"); + default: + return null; + } + } + + @Override + public Class getColumnClass(int columnIndex) { + switch (columnIndex) { + case 0: + return Boolean.class; + case 1: + return String.class; + default: + return null; + } + } + + @Override + public void setValueAt(Object value, int rowIndex, int columnIndex) { + switch (columnIndex) { + case 0: + features.get(rowIndex).setSelected((boolean) (Boolean) value); + break; + case 1: + default: + break; + } + } + + @Override + public boolean isCellEditable(int rowIndex, int columnIndex) { + switch (columnIndex) { + case 0: + return true; + case 1: + return false; + default: + return false; + } + } + + @Override + public int getRowCount() { + return features.size(); + } + + @Override + public int getColumnCount() { + return 2; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + switch (columnIndex) { + case 0: + return features.get(rowIndex).isSelected(); + case 1: + Feature feature = features.get(rowIndex); + return "" + SwingHelper.htmlEscape(feature.getName()) + getAddendum(feature) + ""; + default: + return null; + } + } + + private String getAddendum(Feature feature) { + if (feature.getRecommendation() == null) { + return ""; + } + switch (feature.getRecommendation()) { + case STARRED: + return " " + SharedLocale.tr("features.starred") + ""; + case AVOID: + return " " + SharedLocale.tr("features.avoid") + ""; + default: + return ""; + } + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/swing/FileField.java b/launcher/src/main/java/com/skcraft/launcher/swing/FileField.java new file mode 100644 index 0000000..273852e --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/swing/FileField.java @@ -0,0 +1,44 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.swing; + +import java.io.File; + +import javax.swing.JFileChooser; +import javax.swing.filechooser.FileFilter; + +public class FileField extends DirectoryField { + + private final String title; + private final String description; + + public FileField(String title, String description) { + this.title = title; + this.description = description; + } + + @Override + protected JFileChooser getFileChooser() { + JFileChooser chooser = new JFileChooser(); + chooser.setDialogTitle(title); + chooser.setFileSelectionMode(JFileChooser.FILES_ONLY); + chooser.setFileFilter(new FileFilter() { + @Override + public boolean accept(File pathname) { + return true; + } + + @Override + public String getDescription() { + return description; + } + }); + + return chooser; + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/swing/FormPanel.java b/launcher/src/main/java/com/skcraft/launcher/swing/FormPanel.java new file mode 100644 index 0000000..5808227 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/swing/FormPanel.java @@ -0,0 +1,53 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.swing; + +import javax.swing.*; +import java.awt.*; + +public class FormPanel extends JPanel { + + private static final GridBagConstraints labelConstraints; + private static final GridBagConstraints fieldConstraints; + private static final GridBagConstraints wideFieldConstraints; + + private final GridBagLayout layout; + + static { + fieldConstraints = new GridBagConstraints(); + fieldConstraints.fill = GridBagConstraints.HORIZONTAL; + fieldConstraints.weightx = 1.0; + fieldConstraints.gridwidth = GridBagConstraints.REMAINDER; + fieldConstraints.insets = new Insets(5, 5, 2, 5); + + labelConstraints = (GridBagConstraints) fieldConstraints.clone(); + labelConstraints.weightx = 0.0; + labelConstraints.gridwidth = 1; + labelConstraints.insets = new Insets(4, 5, 1, 10); + + wideFieldConstraints = (GridBagConstraints) fieldConstraints.clone(); + wideFieldConstraints.insets = new Insets(7, 2, 1, 2); + } + + public FormPanel() { + setLayout(layout = new GridBagLayout()); + setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + } + + public void addRow(Component label, Component component) { + add(label); + add(component); + layout.setConstraints(label, labelConstraints); + layout.setConstraints(component, fieldConstraints); + } + + public void addRow(Component component) { + add(component); + layout.setConstraints(component, wideFieldConstraints); + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/swing/HeaderPanel.java b/launcher/src/main/java/com/skcraft/launcher/swing/HeaderPanel.java new file mode 100644 index 0000000..da04fa3 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/swing/HeaderPanel.java @@ -0,0 +1,27 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.swing; + +import javax.swing.*; +import java.awt.*; + +public class HeaderPanel extends JPanel { + + public HeaderPanel() { + setBackground(new Color(0xDB5036)); + } + + @Override + public Dimension getPreferredSize() { + return new Dimension(200, 60); + } + + @Override + public void paint(Graphics g) { + super.paint(g); + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/swing/InstanceTable.java b/launcher/src/main/java/com/skcraft/launcher/swing/InstanceTable.java new file mode 100644 index 0000000..63643ed --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/swing/InstanceTable.java @@ -0,0 +1,26 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.swing; + +import javax.swing.table.TableModel; + +public class InstanceTable extends DefaultTable { + + public InstanceTable() { + super(); + setTableHeader(null); + } + + @Override + public void setModel(TableModel dataModel) { + super.setModel(dataModel); + try { + getColumnModel().getColumn(0).setMaxWidth(24); + } catch (ArrayIndexOutOfBoundsException e) { + } + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/swing/InstanceTableModel.java b/launcher/src/main/java/com/skcraft/launcher/swing/InstanceTableModel.java new file mode 100644 index 0000000..583011d --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/swing/InstanceTableModel.java @@ -0,0 +1,115 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.swing; + +import com.skcraft.launcher.Instance; +import com.skcraft.launcher.InstanceList; +import com.skcraft.launcher.Launcher; +import com.skcraft.launcher.util.SharedLocale; + +import javax.swing.*; +import javax.swing.table.AbstractTableModel; + +public class InstanceTableModel extends AbstractTableModel { + + private final InstanceList instances; + private final Icon instanceIcon; + private final Icon customInstanceIcon; + private final Icon downloadIcon; + + public InstanceTableModel(InstanceList instances) { + this.instances = instances; + instanceIcon = SwingHelper.createIcon(Launcher.class, "instance_icon.png", 16, 16); + customInstanceIcon = SwingHelper.createIcon(Launcher.class, "custom_instance_icon.png", 16, 16); + downloadIcon = SwingHelper.createIcon(Launcher.class, "download_icon.png", 14, 14); + } + + public void update() { + instances.sort(); + fireTableDataChanged(); + } + + @Override + public String getColumnName(int columnIndex) { + switch (columnIndex) { + case 0: + return ""; + case 1: + return SharedLocale.tr("launcher.modpackColumn"); + default: + return null; + } + } + + @Override + public Class getColumnClass(int columnIndex) { + switch (columnIndex) { + case 0: + return ImageIcon.class; + case 1: + return String.class; + default: + return null; + } + } + + @Override + public void setValueAt(Object value, int rowIndex, int columnIndex) { + switch (columnIndex) { + case 0: + instances.get(rowIndex).setSelected((boolean) (Boolean) value); + break; + case 1: + default: + break; + } + } + + @Override + public boolean isCellEditable(int rowIndex, int columnIndex) { + switch (columnIndex) { + case 0: + return false; + case 1: + return false; + default: + return false; + } + } + + @Override + public int getRowCount() { + return instances.size(); + } + + @Override + public int getColumnCount() { + return 2; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + Instance instance; + switch (columnIndex) { + case 0: + instance = instances.get(rowIndex); + if (!instance.isLocal()) { + return downloadIcon; + } else if (instance.getManifestURL() != null) { + return instanceIcon; + } else { + return customInstanceIcon; + } + case 1: + instance = instances.get(rowIndex); + return instance.getTitle(); + default: + return null; + } + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/swing/LinedBoxPanel.java b/launcher/src/main/java/com/skcraft/launcher/swing/LinedBoxPanel.java new file mode 100644 index 0000000..cb61e11 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/swing/LinedBoxPanel.java @@ -0,0 +1,52 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.swing; + +import lombok.Getter; +import lombok.Setter; + +import javax.swing.*; +import java.awt.*; + +public class LinedBoxPanel extends JPanel { + + @Getter + private final boolean horizontal; + @Getter @Setter + private int spacing = 6; + private boolean needsSpacer = false; + + public LinedBoxPanel(boolean horizontal) { + this.horizontal = horizontal; + setLayout(new BoxLayout(this, + horizontal ? BoxLayout.X_AXIS : BoxLayout.Y_AXIS)); + setBorder(BorderFactory.createEmptyBorder(0, 10, 10, 10)); + } + + public LinedBoxPanel fullyPadded() { + setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + return this; + } + + public void addElement(Component component) { + if (needsSpacer) { + add(horizontal ? + Box.createHorizontalStrut(spacing) : + Box.createVerticalStrut(spacing)); + } + add(component); + needsSpacer = true; + } + + public void addGlue() { + add(horizontal ? + Box.createHorizontalGlue() : + Box.createVerticalGlue()); + needsSpacer = false; + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/swing/LinkButton.java b/launcher/src/main/java/com/skcraft/launcher/swing/LinkButton.java new file mode 100644 index 0000000..30c45fb --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/swing/LinkButton.java @@ -0,0 +1,71 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.swing; + +import javax.swing.*; +import javax.swing.border.Border; +import java.awt.*; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; + +public class LinkButton extends JButton { + + private static final Color LINK_COLOR = Color.blue; + private static final Border LINK_BORDER = BorderFactory.createEmptyBorder(0, 0, 1, 0); + private static final Border HOVER_BORDER = BorderFactory.createMatteBorder(0, 0, 1, 0, LINK_COLOR); + + public LinkButton() { + super(); + setupLink(); + } + + public LinkButton(Action a) { + super(a); + setupLink(); + } + + public LinkButton(Icon icon) { + super(icon); + setupLink(); + } + + public LinkButton(String text, Icon icon) { + super(text, icon); + setupLink(); + } + + public LinkButton(String text) { + super(text); + setupLink(); + } + + public void setupLink() { + setBorder(LINK_BORDER); + setForeground(LINK_COLOR); + setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + setFocusPainted(false); + setRequestFocusEnabled(false); + setContentAreaFilled(false); + addMouseListener(new MouseAdapter() { + @Override + public void mouseEntered(MouseEvent e) { + ((JComponent) e.getComponent()).setBorder(HOVER_BORDER); + } + + @Override + public void mouseReleased(MouseEvent e) { + ((JComponent) e.getComponent()).setBorder(LINK_BORDER); + } + + @Override + public void mouseExited(MouseEvent e) { + ((JComponent) e.getComponent()).setBorder(LINK_BORDER); + } + }); + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/swing/MessageLog.java b/launcher/src/main/java/com/skcraft/launcher/swing/MessageLog.java new file mode 100644 index 0000000..1281540 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/swing/MessageLog.java @@ -0,0 +1,321 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.swing; + +import com.skcraft.launcher.LauncherUtils; +import com.skcraft.launcher.util.LimitLinesDocumentListener; +import com.skcraft.launcher.util.SimpleLogFormatter; + +import javax.swing.*; +import javax.swing.text.*; +import java.awt.*; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; + +import static org.apache.commons.io.IOUtils.closeQuietly; + +/** + * A simple message log. + */ +public class MessageLog extends JPanel { + + private static final Logger rootLogger = Logger.getLogger(""); + + private final int numLines; + private final boolean colorEnabled; + + protected JTextComponent textComponent; + protected Document document; + + private Handler loggerHandler; + protected final SimpleAttributeSet defaultAttributes = new SimpleAttributeSet(); + protected final SimpleAttributeSet highlightedAttributes; + protected final SimpleAttributeSet errorAttributes; + protected final SimpleAttributeSet infoAttributes; + protected final SimpleAttributeSet debugAttributes; + + public MessageLog(int numLines, boolean colorEnabled) { + this.numLines = numLines; + this.colorEnabled = colorEnabled; + + this.highlightedAttributes = new SimpleAttributeSet(); + StyleConstants.setForeground(highlightedAttributes, new Color(0xFF7F00)); + + this.errorAttributes = new SimpleAttributeSet(); + StyleConstants.setForeground(errorAttributes, new Color(0xFF0000)); + this.infoAttributes = new SimpleAttributeSet(); + this.debugAttributes = new SimpleAttributeSet(); + + setLayout(new BorderLayout()); + + initComponents(); + } + + private void initComponents() { + if (colorEnabled) { + JTextPane text = new JTextPane() { + @Override + public boolean getScrollableTracksViewportWidth() { + return true; + } + }; + this.textComponent = text; + } else { + JTextArea text = new JTextArea(); + this.textComponent = text; + text.setLineWrap(true); + text.setWrapStyleWord(true); + } + + textComponent.setFont(new JLabel().getFont()); + textComponent.setEditable(false); + textComponent.setComponentPopupMenu(TextFieldPopupMenu.INSTANCE); + DefaultCaret caret = (DefaultCaret) textComponent.getCaret(); + caret.setUpdatePolicy(DefaultCaret.NEVER_UPDATE); + document = textComponent.getDocument(); + document.addDocumentListener(new LimitLinesDocumentListener(numLines, true)); + + JScrollPane scrollText = new JScrollPane(textComponent); + scrollText.setBorder(null); + scrollText.setVerticalScrollBarPolicy( + ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS); + scrollText.setHorizontalScrollBarPolicy( + ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + + add(scrollText, BorderLayout.CENTER); + } + + public String getPastableText() { + String text = textComponent.getText().replaceAll("[\r\n]+", "\n"); + text = text.replaceAll("Session ID is [A-Fa-f0-9]+", "Session ID is [redacted]"); + return text; + } + + public void clear() { + textComponent.setText(""); + } + + /** + * Log a message given the {@link javax.swing.text.AttributeSet}. + * + * @param line line + * @param attributes attribute set, or null for none + */ + public void log(final String line, AttributeSet attributes) { + final Document d = document; + final JTextComponent t = textComponent; + if (colorEnabled) { + if (line.startsWith("(!!)")) { + attributes = highlightedAttributes; + } + } + final AttributeSet a = attributes; + + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + try { + int offset = d.getLength(); + d.insertString(offset, line, + (a != null && colorEnabled) ? a : defaultAttributes); + t.setCaretPosition(d.getLength()); + } catch (BadLocationException ble) { + + } + } + }); + } + + /** + * Get an output stream that can be written to. + * + * @return output stream + */ + public ConsoleOutputStream getOutputStream() { + return getOutputStream((AttributeSet) null); + } + + /** + * Get an output stream with the given attribute set. + * + * @param attributes attributes + * @return output stream + */ + public ConsoleOutputStream getOutputStream(AttributeSet attributes) { + return new ConsoleOutputStream(attributes); + } + + /** + * Get an output stream using the give color. + * + * @param color color to use + * @return output stream + */ + public ConsoleOutputStream getOutputStream(Color color) { + SimpleAttributeSet attributes = new SimpleAttributeSet(); + StyleConstants.setForeground(attributes, color); + return getOutputStream(attributes); + } + + /** + * Consume an input stream and print it to the dialog. The consumer + * will be in a separate daemon thread. + * + * @param from stream to read + */ + public void consume(InputStream from) { + consume(from, getOutputStream()); + } + + /** + * Consume an input stream and print it to the dialog. The consumer + * will be in a separate daemon thread. + * + * @param from stream to read + * @param color color to use + */ + public void consume(InputStream from, Color color) { + consume(from, getOutputStream(color)); + } + + /** + * Consume an input stream and print it to the dialog. The consumer + * will be in a separate daemon thread. + * + * @param from stream to read + * @param attributes attributes + */ + public void consume(InputStream from, AttributeSet attributes) { + consume(from, getOutputStream(attributes)); + } + + /** + * Internal method to consume a stream. + * + * @param from stream to consume + * @param outputStream console stream to write to + */ + private void consume(InputStream from, ConsoleOutputStream outputStream) { + final InputStream in = from; + final PrintWriter out = new PrintWriter(outputStream, true); + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + byte[] buffer = new byte[1024]; + try { + int len; + while ((len = in.read(buffer)) != -1) { + String s = new String(buffer, 0, len); + System.out.print(s); + out.append(s); + out.flush(); + } + } catch (IOException e) { + } finally { + closeQuietly(in); + closeQuietly(out); + } + } + }); + thread.setDaemon(true); + thread.start(); + } + + /** + * Register a global logger listener. + */ + public void registerLoggerHandler() { + loggerHandler = new ConsoleLoggerHandler(); + rootLogger.addHandler(loggerHandler); + } + + /** + * Detach the handler on the global logger. + */ + public void detachGlobalHandler() { + if (loggerHandler != null) { + rootLogger.removeHandler(loggerHandler); + loggerHandler = null; + } + } + + public SimpleAttributeSet asDefault() { + return defaultAttributes; + } + + public SimpleAttributeSet asHighlighted() { + return highlightedAttributes; + } + + public SimpleAttributeSet asError() { + return errorAttributes; + } + + public SimpleAttributeSet asInfo() { + return infoAttributes; + } + + public SimpleAttributeSet asDebug() { + return debugAttributes; + } + + /** + * Used to send logger messages to the console. + */ + private class ConsoleLoggerHandler extends Handler { + private final SimpleLogFormatter formatter = new SimpleLogFormatter(); + + @Override + public void publish(LogRecord record) { + Level level = record.getLevel(); + Throwable t = record.getThrown(); + AttributeSet attributes = defaultAttributes; + + if (level.intValue() >= Level.WARNING.intValue()) { + attributes = errorAttributes; + } else if (level.intValue() < Level.INFO.intValue()) { + attributes = debugAttributes; + } + + log(formatter.format(record), attributes); + } + + @Override + public void flush() { + } + + @Override + public void close() throws SecurityException { + } + } + + /** + * Used to send console messages to the console. + */ + private class ConsoleOutputStream extends ByteArrayOutputStream { + private AttributeSet attributes; + + private ConsoleOutputStream(AttributeSet attributes) { + this.attributes = attributes; + } + + @Override + public void flush() { + String data = toString(); + if (data.length() == 0) return; + log(data, attributes); + reset(); + } + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/swing/ObjectSwingMapper.java b/launcher/src/main/java/com/skcraft/launcher/swing/ObjectSwingMapper.java new file mode 100644 index 0000000..6c4957d --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/swing/ObjectSwingMapper.java @@ -0,0 +1,181 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.swing; + +import com.google.common.base.Strings; +import lombok.NonNull; + +import javax.swing.*; +import javax.swing.text.JTextComponent; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +public class ObjectSwingMapper { + + private final List mappings = new ArrayList(); + private final Object object; + + public ObjectSwingMapper(@NonNull Object object) { + this.object = object; + } + + public void copyFromObject() { + for (FieldMapping mapping : mappings) { + mapping.copyFromObject(); + } + } + + public void copyFromSwing() { + for (FieldMapping mapping : mappings) { + mapping.copyFromSwing(); + } + } + + private void add(@NonNull FieldMapping mapping) { + mappings.add(mapping); + } + + private MutatorAccessorField getField(@NonNull String field, Class clazz) { + return new MutatorAccessorField(object, field, clazz); + } + + public void map(@NonNull final JTextComponent textComponent, String name) { + final MutatorAccessorField field = getField(name, String.class); + + add(new FieldMapping() { + @Override + public void copyFromObject() { + textComponent.setText(field.get()); + } + + @SuppressWarnings("unchecked") + @Override + public void copyFromSwing() { + field.set(Strings.emptyToNull(textComponent.getText())); + } + }); + } + + public void map(@NonNull final JSpinner spinner, String name) { + final MutatorAccessorField field = getField(name, int.class); + + add(new FieldMapping() { + @Override + public void copyFromObject() { + spinner.setValue(field.get()); + } + + @SuppressWarnings("unchecked") + @Override + public void copyFromSwing() { + field.set((Integer) spinner.getValue()); + } + }); + } + + public void map(@NonNull final JCheckBox check, String name) { + final MutatorAccessorField field = getField(name, boolean.class); + + add(new FieldMapping() { + @Override + public void copyFromObject() { + check.setSelected(field.get()); + } + + @SuppressWarnings("unchecked") + @Override + public void copyFromSwing() { + field.set(check.isSelected()); + } + }); + } + + public static interface FieldMapping { + void copyFromObject(); + void copyFromSwing(); + } + + public static class MutatorAccessorField { + private final Class clazz; + private final Object object; + private final Method mutator; + private final Method accessor; + + public MutatorAccessorField(Object object, String name, Class clazz) { + this.object = object; + this.clazz = clazz; + + Method mutator = null; + Method accessor = null; + for (Method method : object.getClass().getMethods()) { + if (isAccessor(method, name)) { + accessor = method; + } else if (isMutator(method, name)) { + mutator = method; + } + } + + if (accessor == null) { + throw new NoSuchMethodError("Failed to find accessor pair on " + + object.getClass().getCanonicalName() + " for " + name); + } + + if (mutator == null) { + throw new NoSuchMethodError("Failed to find mutator pair on " + + object.getClass().getCanonicalName() + " for " + name); + } + + this.mutator = mutator; + this.accessor = accessor; + } + + private boolean isAccessor(Method method, String name) { + String methodName = method.getName(); + Class[] paramTypes = method.getParameterTypes(); + Class returnType = method.getReturnType(); + + return (methodName.equalsIgnoreCase("get" + name) || + methodName.equalsIgnoreCase("is" + name)) && + paramTypes.length == 0 && + clazz.isAssignableFrom(returnType); + } + + private boolean isMutator(Method method, String name) { + String methodName = method.getName(); + Class[] paramTypes = method.getParameterTypes(); + + return methodName.equalsIgnoreCase("set" + name) && + paramTypes.length == 1 && + paramTypes[0].isAssignableFrom(clazz); + } + + @SuppressWarnings("unchecked") + public V get() { + try { + Object value = accessor.invoke(object); + return (V) value; + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } catch (InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + public void set(V value) { + try { + mutator.invoke(object, value); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } catch (InvocationTargetException e) { + throw new RuntimeException(e); + } + } + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/swing/PopupMouseAdapter.java b/launcher/src/main/java/com/skcraft/launcher/swing/PopupMouseAdapter.java new file mode 100644 index 0000000..ea3f6cc --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/swing/PopupMouseAdapter.java @@ -0,0 +1,33 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.swing; + +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; + +/** + * An implementation of MouseAdapter that makes it easier to handle right click menus. + */ +public abstract class PopupMouseAdapter extends MouseAdapter { + + @Override + public void mousePressed(MouseEvent e) { + if (e.isPopupTrigger()) { + showPopup(e); + } + } + + @Override + public void mouseReleased(MouseEvent e) { + if (e.isPopupTrigger()) { + showPopup(e); + } + } + + protected abstract void showPopup(MouseEvent e); + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/swing/SelectionKeeper.java b/launcher/src/main/java/com/skcraft/launcher/swing/SelectionKeeper.java new file mode 100644 index 0000000..dd777a6 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/swing/SelectionKeeper.java @@ -0,0 +1,50 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.swing; + +import lombok.NonNull; + +import javax.swing.*; +import javax.swing.event.ListDataEvent; +import javax.swing.event.ListDataListener; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; + +public class SelectionKeeper implements ListSelectionListener, ListDataListener { + + private final JList list; + private Object lastSelected; + + private SelectionKeeper(@NonNull JList list) { + this.list = list; + } + + public void intervalAdded(ListDataEvent e) { + list.setSelectedValue(lastSelected, true); + } + + public void intervalRemoved(ListDataEvent e) { + list.setSelectedValue(lastSelected, true); + } + + public void contentsChanged(ListDataEvent e) { + list.setSelectedValue(lastSelected, true); + } + + public void valueChanged(ListSelectionEvent e) { + if (!e.getValueIsAdjusting()) { + lastSelected = list.getSelectedValue(); + } + } + + public static void attach(@NonNull JList list) { + SelectionKeeper s = new SelectionKeeper(list); + list.addListSelectionListener(s); + list.getModel().addListDataListener(s); + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/swing/SwingHelper.java b/launcher/src/main/java/com/skcraft/launcher/swing/SwingHelper.java new file mode 100644 index 0000000..1ee7a1c --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/swing/SwingHelper.java @@ -0,0 +1,481 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.swing; + +import com.google.common.base.Joiner; +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.skcraft.launcher.LauncherException; +import com.skcraft.launcher.util.Environment; +import com.skcraft.launcher.util.Platform; +import com.skcraft.launcher.util.SharedLocale; +import com.skcraft.launcher.util.SwingExecutor; +import lombok.NonNull; +import lombok.extern.java.Log; + +import javax.imageio.ImageIO; +import javax.swing.*; +import javax.swing.border.Border; +import javax.swing.plaf.basic.BasicSplitPaneDivider; +import javax.swing.plaf.basic.BasicSplitPaneUI; +import javax.swing.text.JTextComponent; +import java.awt.*; +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.ClipboardOwner; +import java.awt.datatransfer.StringSelection; +import java.awt.datatransfer.Transferable; +import java.awt.event.ActionListener; +import java.awt.image.BufferedImage; +import java.io.*; +import java.lang.reflect.InvocationTargetException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.List; +import java.util.concurrent.CancellationException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; + +import static com.skcraft.launcher.util.SharedLocale.tr; +import static org.apache.commons.io.IOUtils.closeQuietly; + +/** + * Swing utility methods. + */ +@Log +public final class SwingHelper { + + private static final ClipboardOwner clipboardOwner = new ClipboardOwner() { + @Override + public void lostOwnership(Clipboard clipboard, Transferable contents) { + + } + }; + + private static final Joiner NEW_LINE_JOINER = Joiner.on("\n"); + + private SwingHelper() { + } + + public static String htmlEscape(String str) { + return str.replace(">", ">") + .replace("<", "<") + .replace("&", "&"); + } + + public static String htmlWrap(String message) { + // To force the label to wrap, convert the message to broken HTML + return "

" + htmlEscape(message); + } + + public static void setClipboard(String text) { + Toolkit.getDefaultToolkit().getSystemClipboard().setContents( + new StringSelection(text), clipboardOwner); + } + + public static void browseDir(File file, Component component) { + try { + Desktop.getDesktop().open(file); + } catch (IOException e) { + JOptionPane.showMessageDialog(component, tr("errors.openDirError", file.getAbsolutePath()), + SharedLocale.tr("errorTitle"), JOptionPane.ERROR_MESSAGE); + } + } + + /** + * Opens a system web browser for the given URL. + * + * @param url the URL + * @param parentComponent the component from which to show any errors + */ + public static void openURL(@NonNull String url, @NonNull Component parentComponent) { + try { + openURL(new URL(url), parentComponent); + } catch (MalformedURLException e) { + } + } + + /** + * Opens a system web browser for the given URL. + * + * @param url the URL + * @param parentComponent the component from which to show any errors + */ + public static void openURL(URL url, Component parentComponent) { + try { + openURL(url.toURI()); + } catch (IOException e) { + showErrorDialog(parentComponent, tr("errors.openUrlError", url.toString()), SharedLocale.tr("errorTitle")); + } catch (URISyntaxException e) { + log.log(Level.WARNING, "Malformed URL; this is a programming error!", e); + } + } + + public static void openURL(URI url) throws IOException { + try { + Desktop.getDesktop().browse(url); + } catch (UnsupportedOperationException e) { + if (Environment.detectPlatform() == Platform.LINUX) { + // Try xdg-open instead + Runtime.getRuntime().exec(new String[]{"xdg-open", url.toString()}); + } + } + } + + /** + * Shows an popup error dialog, with potential extra details shown either immediately + * or available on the dialog. + * + * @param parentComponent the frame from which the dialog is displayed, otherwise + * null to use the default frame + * @param message the message to display + * @param title the title string for the dialog + * @see #showMessageDialog(java.awt.Component, String, String, String, int) for details + */ + public static void showErrorDialog(Component parentComponent, @NonNull String message, + @NonNull String title) { + showErrorDialog(parentComponent, message, title, null); + } + + /** + * Shows an popup error dialog, with potential extra details shown either immediately + * or available on the dialog. + * + * @param parentComponent the frame from which the dialog is displayed, otherwise + * null to use the default frame + * @param message the message to display + * @param title the title string for the dialog + * @param throwable the exception, or null if there is no exception to show + * @see #showMessageDialog(java.awt.Component, String, String, String, int) for details + */ + public static void showErrorDialog(Component parentComponent, @NonNull String message, + @NonNull String title, Throwable throwable) { + String detailsText = null; + + // Get a string version of the exception and use that for + // the extra details text + if (throwable != null) { + StringWriter sw = new StringWriter(); + throwable.printStackTrace(new PrintWriter(sw)); + detailsText = sw.toString(); + } + + showMessageDialog(parentComponent, + message, title, + detailsText, JOptionPane.ERROR_MESSAGE); + } + + /** + * Show a message dialog using + * {@link javax.swing.JOptionPane#showMessageDialog(java.awt.Component, Object, String, int)}. + * + *

The dialog will be shown from the Event Dispatch Thread, regardless of the + * thread it is called from. In either case, the method will block until the + * user has closed the dialog (or dialog creation fails for whatever reason).

+ * + * @param parentComponent the frame from which the dialog is displayed, otherwise + * null to use the default frame + * @param message the message to display + * @param title the title string for the dialog + * @param messageType see {@link javax.swing.JOptionPane#showMessageDialog(java.awt.Component, Object, String, int)} + * for available message types + */ + public static void showMessageDialog(final Component parentComponent, + @NonNull final String message, + @NonNull final String title, + final String detailsText, + final int messageType) { + + if (SwingUtilities.isEventDispatchThread()) { + String htmlMessage = htmlWrap(message); + + JPanel panel = new JPanel(new BorderLayout(0, detailsText != null ? 20 : 0)); + + // Add the main message + panel.add(new JLabel(htmlMessage), BorderLayout.NORTH); + + // Add the extra details + if (detailsText != null) { + JTextArea textArea = new JTextArea(SharedLocale.tr("errors.reportErrorPreface") + detailsText); + JLabel tempLabel = new JLabel(); + textArea.setFont(tempLabel.getFont()); + textArea.setBackground(tempLabel.getBackground()); + textArea.setTabSize(2); + textArea.setEditable(false); + textArea.setComponentPopupMenu(TextFieldPopupMenu.INSTANCE); + + JScrollPane scrollPane = new JScrollPane(textArea); + scrollPane.setPreferredSize(new Dimension(350, 120)); + panel.add(scrollPane, BorderLayout.CENTER); + } + + JOptionPane.showMessageDialog( + parentComponent, panel, title, messageType); + } else { + // Call method again from the Event Dispatch Thread + try { + SwingUtilities.invokeAndWait(new Runnable() { + @Override + public void run() { + showMessageDialog( + parentComponent, message, title, + detailsText, messageType); + } + }); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (InvocationTargetException e) { + throw new RuntimeException(e); + } + } + } + + /** + * Asks the user a binary yes or no question. + * + * @param parentComponent the component + * @param message the message to display + * @param title the title string for the dialog + * @return whether 'yes' was selected + */ + public static boolean confirmDialog(final Component parentComponent, + @NonNull final String message, + @NonNull final String title) { + if (SwingUtilities.isEventDispatchThread()) { + return JOptionPane.showConfirmDialog( + parentComponent, message, title, JOptionPane.YES_NO_OPTION) == + JOptionPane.YES_OPTION; + } else { + // Use an AtomicBoolean to pass the result back from the + // Event Dispatcher Thread + final AtomicBoolean yesSelected = new AtomicBoolean(); + + try { + SwingUtilities.invokeAndWait(new Runnable() { + @Override + public void run() { + yesSelected.set(confirmDialog(parentComponent, title, message)); + } + }); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (InvocationTargetException e) { + throw new RuntimeException(e); + } + + return yesSelected.get(); + } + } + + /** + * Equalize the width of the given components. + * + * @param component component + */ + public static void equalWidth(Component ... component) { + double widest = 0; + for (Component comp : component) { + Dimension dim = comp.getPreferredSize(); + if (dim.getWidth() > widest) { + widest = dim.getWidth(); + } + } + + for (Component comp : component) { + Dimension dim = comp.getPreferredSize(); + comp.setPreferredSize(new Dimension((int) widest, (int) dim.getHeight())); + } + } + + /** + * Remove all the opaqueness of the given components and child components. + * + * @param components list of components + */ + public static void removeOpaqueness(@NonNull Component ... components) { + for (Component component : components) { + if (component instanceof JComponent) { + JComponent jComponent = (JComponent) component; + jComponent.setOpaque(false); + removeOpaqueness(jComponent.getComponents()); + } + } + } + + public static Image createImage(Class clazz, String name) { + URL resource = clazz.getResource(name); + if (resource != null) { + return Toolkit.getDefaultToolkit().createImage(resource); + } else { + log.log(Level.WARNING, "Failed to read image from resource: " + name); + return null; + } + } + + public static Icon createIcon(Class clazz, String name) { + Image image = createImage(clazz, name); + if (image != null) { + return new ImageIcon(image); + } else { + return new EmptyIcon(16, 16); + } + } + + public static Icon createIcon(Class clazz, String name, int width, int height) { + Image image = createImage(clazz, name); + if (image != null) { + return new ImageIcon(image.getScaledInstance(width, height, Image.SCALE_SMOOTH)); + } else { + return new EmptyIcon(width, height); + } + } + + public static BufferedImage readBufferedImage(Class clazz, String path) { + InputStream in = null; + try { + in = clazz.getResourceAsStream(path); + if (in != null) { + return ImageIO.read(in); + } + } catch (IOException e) { + } finally { + closeQuietly(in); + } + return null; + } + + public static void setFrameIcon(JFrame frame, Class clazz, String path) { + BufferedImage image = readBufferedImage(clazz, path); + if (image != null) { + frame.setIconImage(image); + } + } + + /** + * Focus a component. + * + *

The focus call happens in {@link javax.swing.SwingUtilities#invokeLater(Runnable)}.

+ * + * @param component the component + */ + public static void focusLater(@NonNull final Component component) { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + if (component instanceof JTextComponent) { + ((JTextComponent) component).selectAll(); + } + component.requestFocusInWindow(); + } + }); + } + + public static void flattenJSplitPane(JSplitPane splitPane) { + splitPane.setBorder(BorderFactory.createEmptyBorder(1, 1, 1, 1)); + BasicSplitPaneUI flatDividerSplitPaneUI = new BasicSplitPaneUI() { + @Override + public BasicSplitPaneDivider createDefaultDivider() { + return new BasicSplitPaneDivider(this) { + @Override + public void setBorder(Border b) { + } + }; + } + }; + splitPane.setUI(flatDividerSplitPaneUI); + splitPane.setBorder(null); + } + + public static void addErrorDialogCallback(final Window owner, ListenableFuture future) { + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(Object result) { + } + + @Override + public void onFailure(Throwable t) { + if (t instanceof InterruptedException || t instanceof CancellationException) { + return; + } + + String message; + if (t instanceof LauncherException) { + message = t.getLocalizedMessage(); + t = t.getCause(); + } else { + message = t.getLocalizedMessage(); + if (message == null) { + message = SharedLocale.tr("errors.genericError"); + } + } + log.log(Level.WARNING, "Task failed", t); + SwingHelper.showErrorDialog(owner, message, SharedLocale.tr("errorTitle"), t); + } + }, SwingExecutor.INSTANCE); + } + + public static Component alignTabbedPane(Component component) { + JPanel container = new JPanel(); + container.setLayout(new BoxLayout(container, BoxLayout.Y_AXIS)); + container.add(component); + container.add(new Box.Filler(new Dimension(0, 0), new Dimension(0, 10000), new Dimension(0, 10000))); + SwingHelper.removeOpaqueness(container); + return container; + } + + public static void setTextAndResetCaret(JTextComponent component, String text) { + component.setText(text); + component.setCaretPosition(0); + } + + public static JScrollPane wrapScrollPane(Component component) { + JScrollPane scrollPane = new JScrollPane(component); + scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS); + return scrollPane; + } + + public static String listToLines(List lines) { + return SwingHelper.NEW_LINE_JOINER.join(lines); + } + + public static List linesToList(String text) { + String[] tokens = text.replace("\r", "\n").split("\n"); + List values = Lists.newArrayList(); + for (String token : tokens) { + String value = token.trim(); + if (!value.isEmpty()) { + values.add(value); + } + } + return values; + } + + public static void addActionListeners(AbstractButton button, ActionListener[] listeners) { + for (ActionListener listener : listeners) { + button.addActionListener(listener); + } + } + + public static boolean setLookAndFeel(String lookAndFeel) { + try { + UIManager.setLookAndFeel(lookAndFeel); + return true; + } catch (Exception e) { + log.log(Level.WARNING, "Failed to set look and feel to " + lookAndFeel, e); + return false; + } + } + + public static void setSwingProperties(String appName) { + UIManager.getDefaults().put("SplitPane.border", BorderFactory.createEmptyBorder()); + System.setProperty("com.apple.mrj.application.apple.menu.about.name", appName); + System.setProperty("apple.laf.useScreenMenuBar", "true"); + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/swing/TableColumnAdjuster.java b/launcher/src/main/java/com/skcraft/launcher/swing/TableColumnAdjuster.java new file mode 100644 index 0000000..da1cfc5 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/swing/TableColumnAdjuster.java @@ -0,0 +1,334 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.swing; + +import lombok.Getter; +import lombok.Setter; + +import javax.swing.*; +import javax.swing.event.TableModelEvent; +import javax.swing.event.TableModelListener; +import javax.swing.table.TableCellRenderer; +import javax.swing.table.TableColumn; +import javax.swing.table.TableColumnModel; +import javax.swing.table.TableModel; +import java.awt.*; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.HashMap; +import java.util.Map; + +/* + * Class to manage the widths of colunmns in a table. + * + * Various properties control how the width of the column is calculated. + * Another property controls whether column width calculation should be dynamic. + * Finally, various Actions will be added to the table to allow the user + * to customize the functionality. + * + * This class was designed to be used with tables that use an auto resize mode + * of AUTO_RESIZE_OFF. With all other modes you are constrained as the width + * of the columns must fit inside the table. So if you increase one column, one + * or more of the other columns must decrease. Because of this the resize mode + * of RESIZE_ALL_COLUMNS will work the best. + * + *

From https://tips4java.wordpress.com/2008/11/10/table-column-adjuster/

+ */ +public class TableColumnAdjuster implements PropertyChangeListener, TableModelListener { + private JTable table; + private int spacing; + private boolean isColumnHeaderIncluded; + private boolean isColumnDataIncluded; + private boolean isOnlyAdjustLarger; + private boolean isDynamicAdjustment; + @Getter @Setter + private int imageIconWidth = 25; + private int checkBoxWidth; + private Map columnSizes = new HashMap(); + + /* + * Specify the table and use default spacing + */ + public TableColumnAdjuster(JTable table) { + this(table, 16); + checkBoxWidth = (int) new JCheckBox().getPreferredSize().getWidth(); + } + + /* + * Specify the table and spacing + */ + public TableColumnAdjuster(JTable table, int spacing) { + this.table = table; + this.spacing = spacing; + setColumnHeaderIncluded(true); + setColumnDataIncluded(true); + setOnlyAdjustLarger(false); + setDynamicAdjustment(false); + } + + /** + * Set default column properties. + */ + public void setColumnProperties() { + TableModel tm = table.getModel(); + TableColumnModel tcm = table.getColumnModel(); + + for (int i = 0; i < tm.getColumnCount(); i++) { + TableColumn column = tcm.getColumn(i); + Class columnClass = tm.getColumnClass(i); + if (columnClass == ImageIcon.class) { + column.setWidth(imageIconWidth); + column.setPreferredWidth(imageIconWidth); + column.setMaxWidth(imageIconWidth); + column.setResizable(false); + } else if (columnClass == Boolean.class) { + column.setWidth(checkBoxWidth); + column.setPreferredWidth(checkBoxWidth); + column.setMaxWidth(checkBoxWidth); + column.setResizable(false); + } + } + } + + /* + * Adjust the widths of all the columns in the table + */ + public void adjustColumns() { + TableColumnModel tcm = table.getColumnModel(); + + for (int i = 0; i < tcm.getColumnCount(); i++) { + adjustColumn(i); + } + } + + /* + * Adjust the width of the specified column in the table + */ + public void adjustColumn(final int column) { + TableColumn tableColumn = table.getColumnModel().getColumn(column); + + if (!tableColumn.getResizable()) return; + + int columnHeaderWidth = getColumnHeaderWidth(column); + int columnDataWidth = getColumnDataWidth(column); + int preferredWidth = Math.max(columnHeaderWidth, columnDataWidth); + + updateTableColumn(column, preferredWidth); + } + + /* + * Calculated the width based on the column name + */ + private int getColumnHeaderWidth(int column) { + if (!isColumnHeaderIncluded) return 0; + + TableColumn tableColumn = table.getColumnModel().getColumn(column); + Object value = tableColumn.getHeaderValue(); + TableCellRenderer renderer = tableColumn.getHeaderRenderer(); + + if (renderer == null) { + renderer = table.getTableHeader().getDefaultRenderer(); + } + + Component c = renderer.getTableCellRendererComponent(table, value, false, false, -1, column); + return c.getPreferredSize().width; + } + + /* + * Calculate the width based on the widest cell renderer for the + * given column. + */ + private int getColumnDataWidth(int column) { + if (!isColumnDataIncluded) return 0; + + int preferredWidth = 0; + int maxWidth = table.getColumnModel().getColumn(column).getMaxWidth(); + + for (int row = 0; row < table.getRowCount(); row++) { + preferredWidth = Math.max(preferredWidth, getCellDataWidth(row, column)); + + // We've exceeded the maximum width, no need to check other rows + + if (preferredWidth >= maxWidth) + break; + } + + return preferredWidth; + } + + /* + * Get the preferred width for the specified cell + */ + private int getCellDataWidth(int row, int column) { + // Inovke the renderer for the cell to calculate the preferred width + + TableCellRenderer cellRenderer = table.getCellRenderer(row, column); + Component c = table.prepareRenderer(cellRenderer, row, column); + int width = c.getPreferredSize().width + table.getIntercellSpacing().width; + + return width; + } + + /* + * Update the TableColumn with the newly calculated width + */ + private void updateTableColumn(int column, int width) { + final TableColumn tableColumn = table.getColumnModel().getColumn(column); + + if (!tableColumn.getResizable()) return; + + width += spacing; + + // Don't shrink the column width + + if (isOnlyAdjustLarger) { + width = Math.max(width, tableColumn.getPreferredWidth()); + } + + columnSizes.put(tableColumn, tableColumn.getWidth()); + + table.getTableHeader().setResizingColumn(tableColumn); + tableColumn.setWidth(width); + } + + /* + * Restore the widths of the columns in the table to its previous width + */ + public void restoreColumns() { + TableColumnModel tcm = table.getColumnModel(); + + for (int i = 0; i < tcm.getColumnCount(); i++) { + restoreColumn(i); + } + } + + /* + * Restore the width of the specified column to its previous width + */ + private void restoreColumn(int column) { + TableColumn tableColumn = table.getColumnModel().getColumn(column); + Integer width = columnSizes.get(tableColumn); + + if (width != null) { + table.getTableHeader().setResizingColumn(tableColumn); + tableColumn.setWidth(width.intValue()); + } + } + + /* + * Indicates whether to include the header in the width calculation + */ + public void setColumnHeaderIncluded(boolean isColumnHeaderIncluded) { + this.isColumnHeaderIncluded = isColumnHeaderIncluded; + } + + /* + * Indicates whether to include the model data in the width calculation + */ + public void setColumnDataIncluded(boolean isColumnDataIncluded) { + this.isColumnDataIncluded = isColumnDataIncluded; + } + + /* + * Indicates whether columns can only be increased in size + */ + public void setOnlyAdjustLarger(boolean isOnlyAdjustLarger) { + this.isOnlyAdjustLarger = isOnlyAdjustLarger; + } + + /* + * Indicate whether changes to the model should cause the width to be + * dynamically recalculated. + */ + public void setDynamicAdjustment(boolean isDynamicAdjustment) { + // May need to add or remove the TableModelListener when changed + + if (this.isDynamicAdjustment != isDynamicAdjustment) { + if (isDynamicAdjustment) { + table.addPropertyChangeListener(this); + table.getModel().addTableModelListener(this); + } else { + table.removePropertyChangeListener(this); + table.getModel().removeTableModelListener(this); + } + } + + setColumnProperties(); + + this.isDynamicAdjustment = isDynamicAdjustment; + } + + // +// Implement the PropertyChangeListener +// + public void propertyChange(PropertyChangeEvent e) { + // When the TableModel changes we need to update the listeners + // and column widths + + if ("model".equals(e.getPropertyName())) { + TableModel model = (TableModel) e.getOldValue(); + model.removeTableModelListener(this); + + model = (TableModel) e.getNewValue(); + model.addTableModelListener(this); + + setColumnProperties(); + adjustColumns(); + + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + setColumnProperties(); + adjustColumns(); + } + }); + } + } + + // +// Implement the TableModelListener +// + public void tableChanged(final TableModelEvent e) { + if (!isColumnDataIncluded) return; + + // Needed when table is sorted. + + SwingUtilities.invokeLater(new Runnable() { + public void run() { + // A cell has been updated + + int column = table.convertColumnIndexToView(e.getColumn()); + + if (e.getType() == TableModelEvent.UPDATE && column != -1) { + // Only need to worry about an increase in width for this cell + + if (isOnlyAdjustLarger) { + int row = e.getFirstRow(); + TableColumn tableColumn = table.getColumnModel().getColumn(column); + + if (tableColumn.getResizable()) { + int width = getCellDataWidth(row, column); + updateTableColumn(column, width); + } + } + + // Could be an increase of decrease so check all rows + + else { + adjustColumn(column); + } + } + + // The update affected more than one column so adjust all columns + + else { + adjustColumns(); + } + } + }); + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/swing/TextFieldPopupMenu.java b/launcher/src/main/java/com/skcraft/launcher/swing/TextFieldPopupMenu.java new file mode 100644 index 0000000..4db5027 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/swing/TextFieldPopupMenu.java @@ -0,0 +1,74 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.swing; + +import com.skcraft.launcher.util.SharedLocale; + +import javax.swing.*; +import javax.swing.text.JTextComponent; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +public class TextFieldPopupMenu extends JPopupMenu implements ActionListener { + + public static final TextFieldPopupMenu INSTANCE = new TextFieldPopupMenu(); + + private final JMenuItem cutItem; + private final JMenuItem copyItem; + private final JMenuItem pasteItem; + private final JMenuItem deleteItem; + private final JMenuItem selectAllItem; + + private TextFieldPopupMenu() { + cutItem = addMenuItem(new JMenuItem(SharedLocale.tr("context.cut"), 'T')); + copyItem = addMenuItem(new JMenuItem(SharedLocale.tr("context.copy"), 'C')); + pasteItem = addMenuItem(new JMenuItem(SharedLocale.tr("context.paste"), 'P')); + deleteItem = addMenuItem(new JMenuItem(SharedLocale.tr("context.delete"), 'D')); + addSeparator(); + selectAllItem = addMenuItem(new JMenuItem(SharedLocale.tr("context.selectAll"), 'A')); + } + + private JMenuItem addMenuItem(JMenuItem item) { + item.addActionListener(this); + return add(item); + } + + @Override + public void show(Component invoker, int x, int y) { + JTextComponent textComponent = (JTextComponent) invoker; + boolean editable = textComponent.isEditable() && textComponent.isEnabled(); + cutItem.setVisible(editable); + pasteItem.setVisible(editable); + deleteItem.setVisible(editable); + super.show(invoker, x, y); + } + + @Override + public void actionPerformed(ActionEvent e) { + JTextComponent textComponent = (JTextComponent) getInvoker(); + textComponent.requestFocus(); + + boolean haveSelection = + textComponent.getSelectionStart() != textComponent.getSelectionEnd(); + + if (e.getSource() == cutItem) { + if (!haveSelection) textComponent.selectAll(); + textComponent.cut(); + } else if (e.getSource() == copyItem) { + if (!haveSelection) textComponent.selectAll(); + textComponent.copy(); + } else if (e.getSource() == pasteItem) { + textComponent.paste(); + } else if (e.getSource() == deleteItem) { + if (!haveSelection) textComponent.selectAll(); + textComponent.replaceSelection(""); + } else if (e.getSource() == selectAllItem) { + textComponent.selectAll(); + } + } +} \ No newline at end of file diff --git a/launcher/src/main/java/com/skcraft/launcher/swing/WebpageLayoutManager.java b/launcher/src/main/java/com/skcraft/launcher/swing/WebpageLayoutManager.java new file mode 100644 index 0000000..74ee2de --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/swing/WebpageLayoutManager.java @@ -0,0 +1,58 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.swing; + +import javax.swing.*; +import java.awt.*; + +public class WebpageLayoutManager implements LayoutManager { + + private static final int PROGRESS_WIDTH = 100; + + @Override + public void addLayoutComponent(String name, Component comp) { + } + + @Override + public void removeLayoutComponent(Component comp) { + throw new UnsupportedOperationException("Can't remove things!"); + } + + @Override + public Dimension preferredLayoutSize(Container parent) { + return new Dimension(0, 0); + } + + @Override + public Dimension minimumLayoutSize(Container parent) { + return new Dimension(0, 0); + } + + @Override + public void layoutContainer(Container parent) { + Insets insets = parent.getInsets(); + int maxWidth = parent.getWidth() - (insets.left + insets.right); + int maxHeight = parent.getHeight() - (insets.top + insets.bottom); + + int numComps = parent.getComponentCount(); + for (int i = 0 ; i < numComps ; i++) { + Component comp = parent.getComponent(i); + + if (comp instanceof JProgressBar) { + Dimension size = comp.getPreferredSize(); + comp.setLocation((parent.getWidth() - PROGRESS_WIDTH) / 2, + (int) (parent.getHeight() / 2.0 - size.height / 2.0)); + comp.setSize(PROGRESS_WIDTH, + (int) comp.getPreferredSize().height); + } else { + comp.setLocation(insets.left, insets.top); + comp.setSize(maxWidth, maxHeight); + } + } + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/swing/WebpagePanel.java b/launcher/src/main/java/com/skcraft/launcher/swing/WebpagePanel.java new file mode 100644 index 0000000..a8c554a --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/swing/WebpagePanel.java @@ -0,0 +1,299 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.swing; + +import com.skcraft.launcher.LauncherUtils; +import lombok.extern.java.Log; + +import javax.swing.*; +import javax.swing.border.Border; +import javax.swing.border.CompoundBorder; +import javax.swing.event.HyperlinkEvent; +import javax.swing.event.HyperlinkListener; +import javax.swing.text.html.HTMLDocument; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Enumeration; +import java.util.logging.Level; + +import static com.skcraft.launcher.LauncherUtils.checkInterrupted; + +@Log +public final class WebpagePanel extends JPanel { + + private final WebpagePanel self = this; + + private URL url; + private boolean activated; + private JEditorPane documentView; + private JScrollPane documentScroll; + private JProgressBar progressBar; + private Thread thread; + private Border browserBorder; + + public static WebpagePanel forURL(URL url, boolean lazy) { + return new WebpagePanel(url, lazy); + } + + public static WebpagePanel forHTML(String html) { + return new WebpagePanel(html); + } + + private WebpagePanel(URL url, boolean lazy) { + this.url = url; + + setLayout(new BorderLayout()); + + if (lazy) { + setPlaceholder(); + } else { + setDocument(); + fetchAndDisplay(url); + } + } + + private WebpagePanel(String text) { + this.url = null; + + setLayout(new BorderLayout()); + + setDocument(); + setDisplay(text, null); + } + + public WebpagePanel(boolean lazy) { + this.url = null; + + setLayout(new BorderLayout()); + + if (lazy) { + setPlaceholder(); + } else { + setDocument(); + } + } + + public Border getBrowserBorder() { + return browserBorder; + } + + public void setBrowserBorder(Border browserBorder) { + synchronized (this) { + this.browserBorder = browserBorder; + if (documentScroll != null) { + documentScroll.setBorder(browserBorder); + } + } + } + + private void setDocument() { + activated = true; + + JLayeredPane panel = new JLayeredPane(); + panel.setLayout(new WebpageLayoutManager()); + + documentView = new JEditorPane(); + documentView.setOpaque(false); + documentView.setBorder(null); + documentView.setEditable(false); + documentView.addHyperlinkListener(new HyperlinkListener() { + @Override + public void hyperlinkUpdate(HyperlinkEvent e) { + if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) { + if (e.getURL() != null) { + SwingHelper.openURL(e.getURL(), self); + } + } + } + }); + + documentScroll = new JScrollPane(documentView); + documentScroll.setOpaque(false); + panel.add(documentScroll, new Integer(1)); + documentScroll.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS); + synchronized (this) { + if (browserBorder != null) { + documentScroll.setBorder(browserBorder); + } + } + + progressBar = new JProgressBar(); + progressBar.setIndeterminate(true); + panel.add(progressBar, new Integer(2)); + + SwingHelper.removeOpaqueness(this); + SwingHelper.removeOpaqueness(documentView); + SwingHelper.removeOpaqueness(documentScroll); + + add(panel, BorderLayout.CENTER); + } + + private void setPlaceholder() { + activated = false; + + JLayeredPane panel = new JLayeredPane(); + panel.setBorder(new CompoundBorder( + BorderFactory.createEtchedBorder(), BorderFactory + .createEmptyBorder(4, 4, 4, 4))); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + + final JButton showButton = new JButton("Load page"); + showButton.setAlignmentX(Component.CENTER_ALIGNMENT); + showButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + showButton.setVisible(false); + setDocument(); + fetchAndDisplay(url); + } + }); + + // Center the button vertically. + panel.add(new Box.Filler( + new Dimension(0, 0), + new Dimension(0, 0), + new Dimension(1000, 1000))); + panel.add(showButton); + panel.add(new Box.Filler( + new Dimension(0, 0), + new Dimension(0, 0), + new Dimension(1000, 1000))); + + add(panel, BorderLayout.CENTER); + } + + /** + * Browse to a URL. + * + * @param url the URL + * @param onlyChanged true to only browse if the last URL was different + * @return true if only the URL was changed + */ + public boolean browse(URL url, boolean onlyChanged) { + if (onlyChanged && this.url != null && this.url.equals(url)) { + return false; + } + + this.url = url; + + if (activated) { + fetchAndDisplay(url); + } + + return true; + } + + /** + * Update the page. This has to be run in the Swing event thread. + * + * @param url the URL + */ + private synchronized void fetchAndDisplay(URL url) { + if (thread != null) { + thread.interrupt(); + } + + progressBar.setVisible(true); + + thread = new Thread(new FetchWebpage(url)); + thread.setDaemon(true); + thread.start(); + } + + private void setDisplay(String text, URL baseUrl) { + progressBar.setVisible(false); + documentView.setContentType("text/html"); + HTMLDocument document = (HTMLDocument) documentView.getDocument(); + + // Clear existing styles + Enumeration e = document.getStyleNames(); + while (e.hasMoreElements()) { + Object o = e.nextElement(); + document.removeStyle((String) o); + } + + document.setBase(baseUrl); + documentView.setText(text); + + documentView.setCaretPosition(0); + } + + private void setError(String text) { + progressBar.setVisible(false); + documentView.setContentType("text/plain"); + documentView.setText(text); + documentView.setCaretPosition(0); + } + + private class FetchWebpage implements Runnable { + private URL url; + + public FetchWebpage(URL url) { + this.url = url; + } + + @Override + public void run() { + HttpURLConnection conn = null; + + try { + conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setUseCaches(false); + conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Java) SKMCLauncher"); + conn.setDoInput(true); + conn.setDoOutput(false); + conn.setReadTimeout(5000); + + conn.connect(); + + checkInterrupted(); + + if (conn.getResponseCode() != 200) { + throw new IOException( + "Did not get expected 200 code, got " + + conn.getResponseCode()); + } + + BufferedReader reader = new BufferedReader( + new InputStreamReader(conn.getInputStream(), + "UTF-8")); + + StringBuilder s = new StringBuilder(); + char[] buf = new char[1024]; + int len = 0; + while ((len = reader.read(buf)) != -1) { + s.append(buf, 0, len); + } + String result = s.toString(); + + checkInterrupted(); + + setDisplay(result, LauncherUtils.concat(url, "")); + } catch (IOException e) { + if (Thread.interrupted()) { + return; + } + + log.log(Level.WARNING, "Failed to fetch page", e); + setError("Failed to fetch page: " + e.getMessage()); + } catch (InterruptedException e) { + } finally { + if (conn != null) + conn.disconnect(); + conn = null; + } + } + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/update/BaseUpdater.java b/launcher/src/main/java/com/skcraft/launcher/update/BaseUpdater.java new file mode 100644 index 0000000..15d1f87 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/update/BaseUpdater.java @@ -0,0 +1,321 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.update; + +import com.google.common.base.Strings; +import com.google.common.collect.Iterables; +import com.google.common.collect.Maps; +import com.skcraft.launcher.AssetsRoot; +import com.skcraft.launcher.Instance; +import com.skcraft.launcher.Launcher; +import com.skcraft.launcher.LauncherException; +import com.skcraft.launcher.dialog.FeatureSelectionDialog; +import com.skcraft.launcher.dialog.ProgressDialog; +import com.skcraft.launcher.install.*; +import com.skcraft.launcher.model.loader.LoaderManifest; +import com.skcraft.launcher.model.loader.LocalLoader; +import com.skcraft.launcher.model.minecraft.*; +import com.skcraft.launcher.model.modpack.DownloadableFile; +import com.skcraft.launcher.model.modpack.Feature; +import com.skcraft.launcher.model.modpack.Manifest; +import com.skcraft.launcher.model.modpack.ManifestEntry; +import com.skcraft.launcher.persistence.Persistence; +import com.skcraft.launcher.util.Environment; +import com.skcraft.launcher.util.FileUtils; +import com.skcraft.launcher.util.HttpRequest; +import com.skcraft.launcher.util.SharedLocale; +import lombok.NonNull; +import lombok.extern.java.Log; + +import javax.swing.*; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.*; +import java.util.logging.Level; + +import static com.skcraft.launcher.LauncherUtils.checkInterrupted; +import static com.skcraft.launcher.LauncherUtils.concat; +import static com.skcraft.launcher.util.HttpRequest.url; + +/** + * The base implementation of the various routines involved in downloading + * and updating Minecraft (including the launcher's modpacks), such as asset + * downloading, .jar downloading, and so on. + *

+ * Updating actually starts in {@link com.skcraft.launcher.update.Updater}, + * which is the update worker. This class exists to allow updaters that don't + * use the launcher's default modpack format to reuse these update + * routines. (It also makes the size of the Updater class smaller.) + */ +@Log +public abstract class BaseUpdater { + + private static final long JAR_SIZE_ESTIMATE = 5 * 1024 * 1024; + private static final long LIBRARY_SIZE_ESTIMATE = 3 * 1024 * 1024; + + private final Launcher launcher; + private final Environment environment = Environment.getInstance(); + private final List executeOnCompletion = new ArrayList(); + + protected BaseUpdater(@NonNull Launcher launcher) { + this.launcher = launcher; + } + + protected void complete() { + for (Runnable runnable : executeOnCompletion) { + runnable.run(); + } + } + + protected Manifest installPackage(@NonNull Installer installer, @NonNull Instance instance) throws Exception { + final File contentDir = instance.getContentDir(); + final File logPath = new File(instance.getDir(), "install_log.json"); + final File cachePath = new File(instance.getDir(), "update_cache.json"); + final File featuresPath = new File(instance.getDir(), "features.json"); + + // Make sure the temp dir exists + installer.getTempDir().mkdirs(); + + final InstallLog previousLog = Persistence.read(logPath, InstallLog.class); + final InstallLog currentLog = new InstallLog(); + currentLog.setBaseDir(contentDir); + final UpdateCache updateCache = Persistence.read(cachePath, UpdateCache.class); + final FeatureCache featuresCache = Persistence.read(featuresPath, FeatureCache.class); + + Manifest manifest = HttpRequest + .get(instance.getManifestURL()) + .execute() + .expectResponseCode(200) + .returnContent() + .saveContent(instance.getManifestPath()) + .asJson(Manifest.class); + + if (manifest.getMinimumVersion() > Launcher.PROTOCOL_VERSION) { + throw new LauncherException("Update required", SharedLocale.tr("errors.updateRequiredError")); + } + + if (manifest.getBaseUrl() == null) { + manifest.setBaseUrl(instance.getManifestURL()); + } + + final List features = manifest.getFeatures(); + if (!features.isEmpty()) { + for (Feature feature : features) { + Boolean last = featuresCache.getSelected().get(feature.getName()); + if (last != null) { + feature.setSelected(last); + } + } + + Collections.sort(features); + + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + new FeatureSelectionDialog(ProgressDialog.getLastDialog(), features, BaseUpdater.this) + .setVisible(true); + } + }); + + synchronized (this) { + this.wait(); + } + + for (Feature feature : features) { + featuresCache.getSelected().put(Strings.nullToEmpty(feature.getName()), feature.isSelected()); + } + } + + // Download any extra processing files for each loader + HashMap loaders = Maps.newHashMap(); + for (Map.Entry entry : manifest.getLoaders().entrySet()) { + HashMap localFilesMap = Maps.newHashMap(); + + for (DownloadableFile file : entry.getValue().getDownloadableFiles()) { + if (file.getSide() != Side.CLIENT) continue; + + DownloadableFile.LocalFile localFile = file.download(installer, manifest); + localFilesMap.put(localFile.getName(), localFile); + } + + loaders.put(entry.getKey(), new LocalLoader(entry.getValue(), localFilesMap)); + } + + InstallExtras extras = new InstallExtras(contentDir, loaders); + for (ManifestEntry entry : manifest.getTasks()) { + entry.install(installer, currentLog, updateCache, extras); + } + + executeOnCompletion.add(new Runnable() { + @Override + public void run() { + for (Map.Entry> entry : previousLog.getEntrySet()) { + for (String path : entry.getValue()) { + if (!currentLog.has(path)) { + new File(contentDir, path).delete(); + } + } + } + + writeDataFile(logPath, currentLog); + writeDataFile(cachePath, updateCache); + writeDataFile(featuresPath, featuresCache); + } + }); + + return manifest; + } + + protected void installJar(@NonNull Installer installer, + @NonNull VersionManifest.Artifact artifact, + @NonNull File jarFile, + @NonNull URL url) throws InterruptedException { + // If the JAR does not exist, install it + if (!jarFile.exists()) { + long size = artifact.getSize(); + if (size <= 0) size = JAR_SIZE_ESTIMATE; + + File tempFile = installer.getDownloader().download(url, "", size, jarFile.getName()); + installer.queue(new FileMover(tempFile, jarFile)); + if (artifact.getHash() != null) { + installer.queue(new FileVerify(jarFile, jarFile.getName(), artifact.getHash())); + } + log.info("Installing " + jarFile.getName() + " from " + url); + } + } + + protected void installAssets(@NonNull Installer installer, + @NonNull VersionManifest versionManifest, + @NonNull URL indexUrl, + @NonNull List sources) throws IOException, InterruptedException { + AssetsRoot assetsRoot = launcher.getAssets(); + + AssetsIndex index = HttpRequest + .get(indexUrl) + .execute() + .expectResponseCode(200) + .returnContent() + .saveContent(assetsRoot.getIndexPath(versionManifest)) + .asJson(AssetsIndex.class); + + // Keep track of duplicates + Set downloading = new HashSet(); + + for (Map.Entry entry : index.getObjects().entrySet()) { + checkInterrupted(); + + String hash = entry.getValue().getHash(); + String path = String.format("%s/%s", hash.subSequence(0, 2), hash); + File targetFile = assetsRoot.getObjectPath(entry.getValue()); + + if (!targetFile.exists() && !downloading.contains(path)) { + List urls = new ArrayList(); + for (URL sourceUrl : sources) { + try { + urls.add(concat(sourceUrl, path)); + } catch (MalformedURLException e) { + log.log(Level.WARNING, "Bad source URL for library: " + sourceUrl); + } + } + + File tempFile = installer.getDownloader().download( + urls, "", entry.getValue().getSize(), entry.getKey()); + installer.queue(new FileMover(tempFile, targetFile)); + log.info("Fetching " + path + " from " + urls); + downloading.add(path); + } + } + } + + protected void installLibraries(@NonNull Installer installer, + @NonNull Manifest manifest, + @NonNull File librariesDir, + @NonNull List sources) throws InterruptedException, IOException { + VersionManifest versionManifest = manifest.getVersionManifest(); + + Iterable allLibraries = versionManifest.getLibraries(); + for (LoaderManifest loader : manifest.getLoaders().values()) { + allLibraries = Iterables.concat(allLibraries, loader.getLibraries()); + } + + for (Library library : allLibraries) { + if (library.isGenerated()) continue; // Skip generated libraries. + + if (library.matches(environment)) { + checkInterrupted(); + + Library.Artifact artifact = library.getArtifact(environment); + String path = artifact.getPath(); + + long size = artifact.getSize(); + if (size <= 0) size = LIBRARY_SIZE_ESTIMATE; + + File targetFile = new File(librariesDir, path); + + if (!targetFile.exists()) { + List urls = new ArrayList(); + for (URL sourceUrl : sources) { + try { + urls.add(concat(sourceUrl, path)); + } catch (MalformedURLException e) { + log.log(Level.WARNING, "Bad source URL for library: " + sourceUrl); + } + } + + File tempFile = installer.getDownloader().download(urls, "", size, + library.getName() + ".jar"); + log.info("Fetching " + path + " from " + urls); + installer.queue(new FileMover(tempFile, targetFile)); + if (artifact.getSha1() != null) { + installer.queue(new FileVerify(targetFile, library.getName(), artifact.getSha1())); + } + } + } + } + + // Use our custom logging config depending on what the manifest specifies + if (versionManifest.getLogging() != null && versionManifest.getLogging().getClient() != null) { + VersionManifest.LoggingConfig config = versionManifest.getLogging().getClient(); + + VersionManifest.Artifact file = config.getFile(); + File targetFile = new File(librariesDir, file.getId()); + InputStream embeddedConfig = Launcher.class.getResourceAsStream("logging/" + file.getId()); + + if (embeddedConfig == null) { + // No embedded config, just use whatever the server gives us + File tempFile = installer.getDownloader().download(url(file.getUrl()), file.getHash(), file.getSize(), file.getId()); + + log.info("Downloading logging config " + file.getId() + " from " + file.getUrl()); + installer.queue(new FileMover(tempFile, targetFile)); + } else if (!targetFile.exists() || FileUtils.getShaHash(targetFile).equals(file.getHash())) { + // Use our embedded replacement + + Path tempFile = installer.getTempDir().toPath().resolve(file.getId()); + Files.copy(embeddedConfig, tempFile, StandardCopyOption.REPLACE_EXISTING); + + log.info("Substituting embedded logging config " + file.getId()); + installer.queue(new FileMover(tempFile.toFile(), targetFile)); + } + } + } + + private static void writeDataFile(File path, Object object) { + try { + Persistence.write(path, object); + } catch (IOException e) { + log.log(Level.WARNING, "Failed to write to " + path.getAbsolutePath() + + " for object " + object.getClass().getCanonicalName(), e); + } + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/update/HardResetter.java b/launcher/src/main/java/com/skcraft/launcher/update/HardResetter.java new file mode 100644 index 0000000..5ec9b91 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/update/HardResetter.java @@ -0,0 +1,75 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.update; + +import com.skcraft.concurrency.ProgressObservable; +import com.skcraft.launcher.Instance; +import com.skcraft.launcher.LauncherUtils; +import com.skcraft.launcher.persistence.Persistence; +import lombok.NonNull; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.concurrent.Callable; + +import static com.skcraft.launcher.util.SharedLocale.tr; + +public class HardResetter implements Callable, ProgressObservable { + + private final Instance instance; + private File currentDir; + + public HardResetter(@NonNull Instance instance) { + this.instance = instance; + } + + @Override + public double getProgress() { + return -1; + } + + @Override + public String getStatus() { + return tr("instanceResetter.resetting", instance.getTitle()); + } + + @Override + public Instance call() throws Exception { + instance.setInstalled(false); + instance.setUpdatePending(true); + Persistence.commitAndForget(instance); + + new File(instance.getDir(), "update_cache.json").delete(); + + removeDir(new File(instance.getContentDir(), "config")); + removeDir(new File(instance.getContentDir(), "mods")); + + return instance; + } + + private void removeDir(File dir) throws IOException, InterruptedException { + try { + if (dir.isDirectory()) { + currentDir = dir; + LauncherUtils.interruptibleDelete(dir, new ArrayList()); + } + } finally { + currentDir = null; + } + } + + public String toString() { + File dir = currentDir; + if (dir != null) { + return "Removing " + dir.getAbsolutePath(); + } else { + return "Working..."; + } + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/update/Remover.java b/launcher/src/main/java/com/skcraft/launcher/update/Remover.java new file mode 100644 index 0000000..985a84a --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/update/Remover.java @@ -0,0 +1,70 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.update; + +import com.skcraft.concurrency.ProgressObservable; +import com.skcraft.launcher.Instance; +import com.skcraft.launcher.LauncherException; +import com.skcraft.launcher.LauncherUtils; +import com.skcraft.launcher.persistence.Persistence; +import lombok.NonNull; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; + +import static com.skcraft.launcher.LauncherUtils.checkInterrupted; +import static com.skcraft.launcher.util.SharedLocale.tr; + +public class Remover implements Callable, ProgressObservable { + + private final Instance instance; + + public Remover(@NonNull Instance instance) { + this.instance = instance; + } + + @Override + public double getProgress() { + return -1; + } + + @Override + public String getStatus() { + return tr("instanceDeleter.deleting", instance.getDir()); + } + + @Override + public Instance call() throws Exception { + instance.setInstalled(false); + instance.setUpdatePending(true); + Persistence.commitAndForget(instance); + + checkInterrupted(); + + Thread.sleep(2000); + + List failures = new ArrayList(); + + try { + LauncherUtils.interruptibleDelete(instance.getDir(), failures); + } catch (IOException e) { + Thread.sleep(1000); + LauncherUtils.interruptibleDelete(instance.getDir(), failures); + } + + if (failures.size() > 0) { + throw new LauncherException(failures.size() + " failed to delete", + tr("instanceDeleter.failures", failures.size())); + } + + return instance; + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/update/UpdateManager.java b/launcher/src/main/java/com/skcraft/launcher/update/UpdateManager.java new file mode 100644 index 0000000..e24e8f3 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/update/UpdateManager.java @@ -0,0 +1,113 @@ +/* + * SKCraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.update; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.skcraft.concurrency.ObservableFuture; +import com.skcraft.launcher.Launcher; +import com.skcraft.launcher.dialog.ProgressDialog; +import com.skcraft.launcher.selfupdate.LatestVersionInfo; +import com.skcraft.launcher.selfupdate.SelfUpdater; +import com.skcraft.launcher.selfupdate.UpdateChecker; +import com.skcraft.launcher.swing.SwingHelper; +import com.skcraft.launcher.util.SharedLocale; +import com.skcraft.launcher.util.SwingExecutor; +import lombok.Getter; + +import javax.swing.*; +import javax.swing.event.SwingPropertyChangeSupport; +import java.awt.*; +import java.beans.PropertyChangeListener; +import java.io.File; +import java.net.URL; + +public class UpdateManager { + + @Getter + private final SwingPropertyChangeSupport propertySupport = new SwingPropertyChangeSupport(this); + private final Launcher launcher; + private LatestVersionInfo pendingUpdate; + + public UpdateManager(Launcher launcher) { + this.launcher = launcher; + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + propertySupport.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + propertySupport.removePropertyChangeListener(listener); + } + + public boolean getPendingUpdate() { + return pendingUpdate != null; + } + + public void checkForUpdate(final Window window) { + ListenableFuture future = launcher.getExecutor().submit(new UpdateChecker(launcher)); + + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(LatestVersionInfo result) { + if (result != null) { + requestUpdate(result); + } + } + + @Override + public void onFailure(Throwable t) { + // Error handler attached below. + } + }, SwingExecutor.INSTANCE); + + SwingHelper.addErrorDialogCallback(window, future); + } + + public void performUpdate(final Window window) { + final URL url = pendingUpdate.getUrl(); + + if (url != null) { + SelfUpdater downloader = new SelfUpdater(launcher, url); + ObservableFuture future = new ObservableFuture( + launcher.getExecutor().submit(downloader), downloader); + + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(File result) { + propertySupport.firePropertyChange("pendingUpdate", true, false); + UpdateManager.this.pendingUpdate = null; + + SwingHelper.showMessageDialog( + window, + SharedLocale.tr("launcher.selfUpdateComplete"), + SharedLocale.tr("launcher.selfUpdateCompleteTitle"), + null, + JOptionPane.INFORMATION_MESSAGE); + } + + @Override + public void onFailure(Throwable t) { + } + }, SwingExecutor.INSTANCE); + + ProgressDialog.showProgress(window, future, SharedLocale.tr("launcher.selfUpdatingTitle"), SharedLocale.tr("launcher.selfUpdatingStatus")); + SwingHelper.addErrorDialogCallback(window, future); + } else { + propertySupport.firePropertyChange("pendingUpdate", false, false); + } + } + + private void requestUpdate(LatestVersionInfo url) { + propertySupport.firePropertyChange("pendingUpdate", getPendingUpdate(), url != null); + this.pendingUpdate = url; + } + + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/update/Updater.java b/launcher/src/main/java/com/skcraft/launcher/update/Updater.java new file mode 100644 index 0000000..745c20a --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/update/Updater.java @@ -0,0 +1,237 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.update; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.skcraft.concurrency.DefaultProgress; +import com.skcraft.concurrency.ProgressFilter; +import com.skcraft.concurrency.ProgressObservable; +import com.skcraft.launcher.Instance; +import com.skcraft.launcher.Launcher; +import com.skcraft.launcher.LauncherException; +import com.skcraft.launcher.install.Installer; +import com.skcraft.launcher.model.minecraft.ReleaseList; +import com.skcraft.launcher.model.minecraft.Version; +import com.skcraft.launcher.model.minecraft.VersionManifest; +import com.skcraft.launcher.model.modpack.Manifest; +import com.skcraft.launcher.persistence.Persistence; +import com.skcraft.launcher.util.HttpRequest; +import com.skcraft.launcher.util.SharedLocale; +import lombok.Getter; +import lombok.NonNull; +import lombok.Setter; +import lombok.extern.java.Log; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.logging.Level; + +import static com.skcraft.launcher.util.HttpRequest.url; + +@Log +public class Updater extends BaseUpdater implements Callable, ProgressObservable { + + private final ObjectMapper mapper = new ObjectMapper(); + private final Installer installer; + private final Launcher launcher; + private final Instance instance; + + @Getter @Setter + private boolean online; + + private List librarySources = new ArrayList(); + private List assetsSources = new ArrayList(); + + private ProgressObservable progress = new DefaultProgress(-1, SharedLocale.tr("instanceUpdater.preparingUpdate")); + + public Updater(@NonNull Launcher launcher, @NonNull Instance instance) { + super(launcher); + + this.installer = new Installer(launcher.getInstallerDir()); + this.launcher = launcher; + this.instance = instance; + + librarySources.add(launcher.propUrl("librariesSource")); + assetsSources.add(launcher.propUrl("assetsSource")); + } + + @Override + public Instance call() throws Exception { + log.info("Checking for an update for '" + instance.getName() + "'..."); + + // Force the directory to be created + instance.getContentDir(); + + boolean updateRequired = !instance.isInstalled(); + boolean updateDesired = (instance.isUpdatePending() || updateRequired); + boolean updateCapable = (instance.getManifestURL() != null); + + if (!online && updateRequired) { + log.info("Can't update " + instance.getTitle() + " because offline"); + String message = SharedLocale.tr("updater.updateRequiredButOffline"); + throw new LauncherException("Update required but currently offline", message); + } + + if (updateDesired && !updateCapable) { + if (updateRequired) { + log.info("Update required for " + instance.getTitle() + " but there is no manifest"); + String message = SharedLocale.tr("updater.updateRequiredButNoManifest"); + throw new LauncherException("Update required but no manifest", message); + } else { + log.info("Can't update " + instance.getTitle() + ", but update is not required"); + return instance; // Can't update + } + } + + if (updateDesired) { + log.info("Updating " + instance.getTitle() + "..."); + update(instance); + } else { + log.info("No update found for " + instance.getTitle()); + } + + return instance; + } + + /** + * Check whether the package manifest contains an embedded version manifest, + * otherwise we'll have to download the one for the given Minecraft version. + * + * BACKWARDS COMPATIBILITY: + * Old manifests have an embedded version manifest without the minecraft JARs list present. + * If we find a manifest without that jar list, fetch the newer copy from launchermeta and use the list from that. + * We can't just replace the manifest outright because library versions might differ and that screws up Runner. + */ + private VersionManifest readVersionManifest(Manifest manifest) throws IOException, InterruptedException { + VersionManifest version = manifest.getVersionManifest(); + URL url = url(launcher.getProperties().getProperty("versionManifestUrl")); + + if (version == null) { + version = fetchVersionManifest(url, manifest); + } + + if (version.getDownloads().isEmpty()) { + // Backwards compatibility hack + VersionManifest otherManifest = fetchVersionManifest(url, manifest); + + version.setDownloads(otherManifest.getDownloads()); + version.setAssetIndex(otherManifest.getAssetIndex()); + } + + mapper.writeValue(instance.getVersionPath(), version); + return version; + } + + private static VersionManifest fetchVersionManifest(URL url, Manifest manifest) throws IOException, InterruptedException { + ReleaseList releases = HttpRequest.get(url) + .execute() + .expectResponseCode(200) + .returnContent() + .asJson(ReleaseList.class); + + Version relVersion = releases.find(manifest.getGameVersion()); + return HttpRequest.get(url(relVersion.getUrl())) + .execute() + .expectResponseCode(200) + .returnContent() + .asJson(VersionManifest.class); + } + + /** + * Update the given instance. + * + * @param instance the instance + * @throws IOException thrown on I/O error + * @throws InterruptedException thrown on interruption + * @throws ExecutionException thrown on execution error + */ + protected void update(Instance instance) throws Exception { + // Mark this instance as local + instance.setLocal(true); + Persistence.commitAndForget(instance); + + // Read manifest + log.info("Reading package manifest..."); + progress = new DefaultProgress(-1, SharedLocale.tr("instanceUpdater.readingManifest")); + Manifest manifest = installPackage(installer, instance); + + // Update instance from manifest + manifest.update(instance); + + // Read version manifest + log.info("Reading version manifest..."); + progress = new DefaultProgress(-1, SharedLocale.tr("instanceUpdater.readingVersion")); + VersionManifest version = readVersionManifest(manifest); + + progress = new DefaultProgress(-1, SharedLocale.tr("instanceUpdater.buildingDownloadList")); + + // Install the .jar + File jarPath = launcher.getJarPath(version); + VersionManifest.Artifact clientJar = version.getDownloads().get("client"); + URL jarSource = url(clientJar.getUrl()); + log.info("JAR at " + jarPath.getAbsolutePath() + ", fetched from " + jarSource); + installJar(installer, clientJar, jarPath, jarSource); + + // Download libraries + log.info("Enumerating libraries to download..."); + + URL url = manifest.getLibrariesUrl(); + if (url != null) { + log.info("Added library source: " + url); + librarySources.add(0, url); + } + + progress = new DefaultProgress(-1, SharedLocale.tr("instanceUpdater.collectingLibraries")); + installLibraries(installer, manifest, launcher.getLibrariesDir(), librarySources); + + // Download assets + log.info("Enumerating assets to download..."); + progress = new DefaultProgress(-1, SharedLocale.tr("instanceUpdater.collectingAssets")); + installAssets(installer, version, url(version.getAssetIndex().getUrl()), assetsSources); + + log.info("Executing download phase..."); + progress = ProgressFilter.between(installer.getDownloader(), 0, 0.98); + installer.download(); + + log.info("Executing install phase..."); + progress = ProgressFilter.between(installer, 0.98, 1); + installer.execute(launcher); + + installer.executeLate(launcher); + + log.info("Completing..."); + complete(); + + // Update the instance's information + log.info("Writing instance information..."); + instance.setVersion(manifest.getVersion()); + instance.setUpdatePending(false); + instance.setInstalled(true); + instance.setLocal(true); + Persistence.commitAndForget(instance); + + log.log(Level.INFO, instance.getName() + + " has been updated to version " + manifest.getVersion() + "."); + } + + @Override + public double getProgress() { + return progress.getProgress(); + } + + @Override + public String getStatus() { + return progress.getStatus(); + } + + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/util/Environment.java b/launcher/src/main/java/com/skcraft/launcher/util/Environment.java new file mode 100644 index 0000000..916cf30 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/util/Environment.java @@ -0,0 +1,57 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.util; + +import lombok.Data; + +/** + * Represents information about the current environment. + */ +@Data +public class Environment { + + private final Platform platform; + private final String platformVersion; + private final String arch; + + /** + * Get an instance of the current environment. + * + * @return the current environment + */ + public static Environment getInstance() { + return new Environment(detectPlatform(), System.getProperty("os.version"), System.getProperty("os.arch")); + } + + public String getArchBits() { + return arch.contains("64") ? "64" : "32"; + } + + /** + * Detect the current platform. + * + * @return the current platform + */ + public static Platform detectPlatform() { + String osName = System.getProperty("os.name").toLowerCase(); + if (osName.contains("win")) + return Platform.WINDOWS; + if (osName.contains("mac")) + return Platform.MAC_OS_X; + if (osName.contains("solaris") || osName.contains("sunos")) + return Platform.SOLARIS; + if (osName.contains("linux")) + return Platform.LINUX; + if (osName.contains("unix")) + return Platform.LINUX; + if (osName.contains("bsd")) + return Platform.LINUX; + + return Platform.UNKNOWN; + } + +} 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 0000000..2afcd3a --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/util/EnvironmentParser.java @@ -0,0 +1,144 @@ +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 { + StringBuilder buffer = new StringBuilder(); + + try { + 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); + } + } + } catch (EOFException e) { + // No terminating newline. bad! + return buffer.toString(); + } + } + + 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/java/com/skcraft/launcher/util/FileUtils.java b/launcher/src/main/java/com/skcraft/launcher/util/FileUtils.java new file mode 100644 index 0000000..35a9227 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/util/FileUtils.java @@ -0,0 +1,43 @@ +package com.skcraft.launcher.util; + +import com.google.common.hash.HashFunction; +import com.google.common.hash.Hashing; +import com.google.common.io.ByteStreams; +import com.google.common.io.Files; +import com.skcraft.launcher.model.modpack.DownloadableFile; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +public class FileUtils { + public static DownloadableFile saveStreamToObjectsDir(InputStream stream, File outputDir) throws IOException { + byte[] input = ByteStreams.toByteArray(stream); + HashFunction hf = Hashing.sha1(); + + String fileHash = hf.hashBytes(input).toString(); + String filePath = fileHash.substring(0, 2) + "/" + fileHash.substring(2, 4) + "/" + fileHash; + + File dest = new File(outputDir, filePath); + dest.getParentFile().mkdirs(); + + Files.write(input, dest); + + DownloadableFile entry = new DownloadableFile(); + entry.setLocation(filePath); + entry.setHash(fileHash); + entry.setSize(input.length); + + return entry; + } + + public static String getShaHash(File file) throws IOException { + FileInputStream stream = new FileInputStream(file); + byte[] input = ByteStreams.toByteArray(stream); + String res = Hashing.sha1().hashBytes(input).toString(); + + stream.close(); + return res; + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/util/HttpFunction.java b/launcher/src/main/java/com/skcraft/launcher/util/HttpFunction.java new file mode 100644 index 0000000..67788e8 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/util/HttpFunction.java @@ -0,0 +1,8 @@ +package com.skcraft.launcher.util; + +import java.io.IOException; + +@FunctionalInterface +public interface HttpFunction { + V call(T arg) throws IOException, InterruptedException; +} diff --git a/launcher/src/main/java/com/skcraft/launcher/util/HttpRequest.java b/launcher/src/main/java/com/skcraft/launcher/util/HttpRequest.java new file mode 100644 index 0000000..e1b94d4 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/util/HttpRequest.java @@ -0,0 +1,653 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.util; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.skcraft.concurrency.ProgressObservable; +import lombok.Data; +import lombok.Getter; +import lombok.extern.java.Log; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Unmarshaller; +import java.io.*; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLEncoder; +import java.util.*; + +import static com.skcraft.launcher.LauncherUtils.checkInterrupted; +import static org.apache.commons.io.IOUtils.closeQuietly; + +/** + * A simple fluent interface for performing HTTP requests that uses + * {@link java.net.HttpURLConnection} or {@link javax.net.ssl.HttpsURLConnection}. + */ +@Log +public class HttpRequest implements Closeable, ProgressObservable { + + private static final int READ_TIMEOUT = 1000 * 60 * 10; + private static final int READ_BUFFER_SIZE = 1024 * 8; + + private final ObjectMapper mapper = new ObjectMapper(); + private final Map headers = new HashMap(); + private String method; + @Getter + private final URL url; + private String contentType; + private byte[] body; + private HttpURLConnection conn; + private InputStream inputStream; + private int redirectCount; + + private PartialDownloadInfo resumeInfo = null; + private long contentLength = -1; + private long readBytes = 0; + + /** + * Create a new HTTP request. + * + * @param method the method + * @param url the URL + */ + private HttpRequest(String method, URL url) { + this.method = method; + this.url = url; + } + + /** + * Set the content body to a JSON object with the content type of "application/json". + * + * @param object the object to serialize as JSON + * @return this object + * @throws java.io.IOException if the object can't be mapped + */ + public HttpRequest bodyJson(Object object) throws IOException { + contentType = "application/json"; + body = mapper.writeValueAsBytes(object); + return this; + } + + /** + * Submit form data. + * + * @param form the form + * @return this object + */ + public HttpRequest bodyForm(Form form) { + contentType = "application/x-www-form-urlencoded"; + body = form.toString().getBytes(); + return this; + } + + /** + * Add a header. + * + * @param key the header key + * @param value the header value + * @return this object + */ + public HttpRequest header(String key, String value) { + headers.put(key, value); + return this; + } + + /** + * Execute the request. + *

+ * After execution, {@link #close()} should be called. + * + * @return this object + * @throws java.io.IOException on I/O error + */ + public HttpRequest execute() throws IOException { + boolean successful = false; + + try { + if (conn != null) { + throw new IllegalArgumentException("Connection already executed"); + } + + conn = this.runRequest(url); + + inputStream = isSuccessCode() ? conn.getInputStream() : conn.getErrorStream(); + + successful = true; + } finally { + if (!successful) { + close(); + } + } + + return this; + } + + private HttpURLConnection runRequest(URL url) throws IOException { + if (redirectCount > 20) { + throw new IOException("Too many redirects!"); + } + + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Java) SKMCLauncher"); + conn.setInstanceFollowRedirects(false); + + if (body != null) { + conn.setRequestProperty("Content-Type", contentType); + conn.setRequestProperty("Content-Length", Integer.toString(body.length)); + conn.setDoInput(true); + } + + if (resumeInfo != null) { + conn.setRequestProperty("Range", String.format("bytes=%d-", resumeInfo.currentLength)); + } + + for (Map.Entry entry : headers.entrySet()) { + conn.setRequestProperty(entry.getKey(), entry.getValue()); + } + + conn.setRequestMethod(method); + conn.setUseCaches(false); + conn.setDoOutput(true); + conn.setReadTimeout(READ_TIMEOUT); + + conn.connect(); + + if (body != null) { + DataOutputStream out = new DataOutputStream(conn.getOutputStream()); + out.write(body); + out.flush(); + out.close(); + } + + switch (conn.getResponseCode()) { + case HttpURLConnection.HTTP_SEE_OTHER: + method = "GET"; + body = null; + case HttpURLConnection.HTTP_MOVED_PERM: + case HttpURLConnection.HTTP_MOVED_TEMP: + case HttpURLConnection.HTTP_ACCEPTED: + case 307: + case 308: + String location = conn.getHeaderField("Location"); + redirectCount++; + + return runRequest(new URL(this.url, location)); + default: + break; + } + + return conn; + } + + /** + * Require that the response code is one of the given response codes. + * + * @param codes a list of codes + * @return this object + * @throws java.io.IOException if there is an I/O error or the response code is not expected + */ + public HttpRequest expectResponseCode(int... codes) throws IOException { + int responseCode = getResponseCode(); + + for (int code : codes) { + if (code == responseCode) { + return this; + } + } + + if (resumeInfo != null && responseCode == 206) { + // Allow 206 Partial Content for resumed requests + return this; + } + + close(); + throw new IOException("Did not get expected response code, got " + responseCode + " for " + url); + } + + /** + * Continue if the response code matches, otherwise call the provided function + * to generate an exception. + * + * @param code HTTP status code to continue on. + * @param onError Function invoked when the code does not match, should return an error that will be thrown. + * @return this object if successful + * @throws Exception either an {@link IOException} on I/O error or a user-defined {@link Exception} subclass + * if the code does not match. + */ + public HttpRequest expectResponseCodeOr(int code, HttpFunction onError) + throws E, IOException, InterruptedException { + int responseCode = getResponseCode(); + + if (code == responseCode) return this; + + E exc = onError.call(this); + close(); + throw exc; + } + + /** + * Continue if the content type matches, otherwise throw an exception + * + * @param expectedTypes Expected content-type(s) + * @return this object + * @throws IOException Unexpected content-type or other error + */ + public HttpRequest expectContentType(String... expectedTypes) throws IOException { + if (conn == null) throw new IllegalArgumentException("No connection has been made!"); + + String contentType = conn.getHeaderField("Content-Type"); + for (String expectedType : expectedTypes) { + if (expectedType.equals(contentType)) { + return this; + } + } + + close(); + throw new IOException(String.format("Did not get expected content type '%s', instead got '%s'.", + String.join(" | ", expectedTypes), contentType)); + } + + /** + * Get the response code. + * + * @return the response code + * @throws java.io.IOException on I/O error + */ + public int getResponseCode() throws IOException { + if (conn == null) { + throw new IllegalArgumentException("No connection has been made"); + } + + return conn.getResponseCode(); + } + + /** + * Check if the response code indicates a successful request. + * @return True if response code is 2xx, false otherwise. + * @throws IOException on I/O error getting the response code. + */ + public boolean isSuccessCode() throws IOException { + int code = getResponseCode(); + return code >= 200 && code < 300; + } + + /** + * Get the input stream. + * + * @return the input stream + */ + public InputStream getInputStream() { + return inputStream; + } + + /** + * Check if a connection was ever made + * + * @return True if a connection is available, false otherwise + */ + public boolean isConnected() { + return conn != null; + } + + /** + * Buffer the returned response. + * + * @return the buffered response + * @throws java.io.IOException on I/O error + * @throws InterruptedException on interruption + */ + public BufferedResponse returnContent() throws IOException, InterruptedException { + if (inputStream == null) { + throw new IllegalArgumentException("No input stream available"); + } + + try { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + int b = 0; + while ((b = inputStream.read()) != -1) { + checkInterrupted(); + bos.write(b); + } + return new BufferedResponse(bos.toByteArray()); + } finally { + close(); + } + } + + /** + * Save the result to a file. + * + * @param file the file + * @return this object + * @throws java.io.IOException on I/O error + * @throws InterruptedException on interruption + */ + public HttpRequest saveContent(File file) throws IOException, InterruptedException { + FileOutputStream fos = null; + BufferedOutputStream bos = null; + boolean shouldAppend = resumeInfo != null && getResponseCode() == 206; + + try { + fos = new FileOutputStream(file, shouldAppend); + bos = new BufferedOutputStream(fos); + + saveContent(bos); + } finally { + closeQuietly(bos); + closeQuietly(fos); + } + + return this; + } + + /** + * Save the result to an output stream. + * + * @param out the output stream + * @return this object + * @throws java.io.IOException on I/O error + * @throws InterruptedException on interruption + */ + public HttpRequest saveContent(OutputStream out) throws IOException, InterruptedException { + BufferedInputStream bis; + + try { + String field = conn.getHeaderField("Content-Length"); + if (field != null) { + long len = Long.parseLong(field); + if (len >= 0) { // Let's just not deal with really big numbers + contentLength = len; + } + } + } catch (NumberFormatException e) { + } + + try { + bis = new BufferedInputStream(inputStream); + + byte[] data = new byte[READ_BUFFER_SIZE]; + int len = 0; + while ((len = bis.read(data, 0, READ_BUFFER_SIZE)) >= 0) { + out.write(data, 0, len); + readBytes += len; + checkInterrupted(); + } + + if (contentLength >= 0 && contentLength != readBytes) { + throw new IOException(String.format("Connection closed with %d bytes transferred, expected %d", + readBytes, contentLength)); + } + } finally { + close(); + } + + return this; + } + + public Optional canRetryPartial() { + if (conn == null) { + return Optional.empty(); + } + + if ("bytes".equals(conn.getHeaderField("Accept-Ranges"))) { + return Optional.of(new PartialDownloadInfo(contentLength, readBytes)); + } + + return Optional.empty(); + } + + public HttpRequest setResumeInfo(PartialDownloadInfo info) { + this.resumeInfo = info; + + return this; + } + + public boolean isResumedRequest() { + return resumeInfo != null; + } + + @Override + public double getProgress() { + if (contentLength >= 0) { + return readBytes / (double) contentLength; + } else { + return -1; + } + } + + @Override + public String getStatus() { + return null; + } + + @Override + public void close() throws IOException { + if (conn != null) conn.disconnect(); + } + + /** + * Perform a GET request. + * + * @param url the URL + * @return a new request object + */ + public static HttpRequest get(URL url) { + return request("GET", url); + } + + /** + * Perform a POST request. + * + * @param url the URL + * @return a new request object + */ + public static HttpRequest post(URL url) { + return request("POST", url); + } + + /** + * Perform a request. + * + * @param method the method + * @param url the URL + * @return a new request object + */ + public static HttpRequest request(String method, URL url) { + return new HttpRequest(method, url); + } + + /** + * Create a new {@link java.net.URL} and throw a {@link RuntimeException} if the URL + * is not valid. + * + * @param url the url + * @return a URL object + * @throws RuntimeException if the URL is invalid + */ + public static URL url(String url) { + try { + return new URL(url); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } + + /** + * Used with {@link #bodyForm(Form)}. + */ + public final static class Form { + public final List elements = new ArrayList(); + + private Form() { + } + + /** + * Add a key/value to the form. + * + * @param key the key + * @param value the value + * @return this object + */ + public Form add(String key, String value) { + try { + elements.add(URLEncoder.encode(key, "UTF-8") + + "=" + URLEncoder.encode(value, "UTF-8")); + return this; + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + boolean first = true; + for (String element : elements) { + if (first) { + first = false; + } else { + builder.append("&"); + } + builder.append(element); + } + return builder.toString(); + } + + /** + * Create a new form. + * + * @return a new form + */ + public static Form form() { + return new Form(); + } + } + + /** + * Used to buffer the response in memory. + */ + public class BufferedResponse { + private final byte[] data; + + private BufferedResponse(byte[] data) { + this.data = data; + } + + /** + * Return the result as bytes. + * + * @return the data + */ + public byte[] asBytes() { + return data; + } + + /** + * Return the result as a string. + * + * @param encoding the encoding + * @return the string + * @throws java.io.IOException on I/O error + */ + public String asString(String encoding) throws IOException { + return new String(data, encoding); + } + + /** + * Return the result as an instance of the given class that has been + * deserialized from a JSON payload. + * + * @param cls the class + * @return the object + * @throws java.io.IOException on I/O error + */ + public T asJson(Class cls) throws IOException { + return mapper.readValue(asString("UTF-8"), cls); + } + + /** + * Return the result as an instance of the given type that has been + * deserialized from a JSON payload. + * + * @param type the type reference + * @return the object + * @throws java.io.IOException on I/O error + */ + public T asJson(TypeReference type) throws IOException { + return mapper.readValue(asString("UTF-8"), type); + } + + /** + * Return the result as an instance of the given class that has been + * deserialized from a XML payload. + * + * @return the object + * @throws java.io.IOException on I/O error + */ + @SuppressWarnings("unchecked") + public T asXml(Class cls) throws IOException { + try { + JAXBContext context = JAXBContext.newInstance(cls); + Unmarshaller um = context.createUnmarshaller(); + return (T) um.unmarshal(new ByteArrayInputStream(data)); + } catch (JAXBException e) { + throw new IOException(e); + } + } + + /** + * Save the result to a file. + * + * @param file the file + * @return this object + * @throws java.io.IOException on I/O error + * @throws InterruptedException on interruption + */ + public BufferedResponse saveContent(File file) throws IOException, InterruptedException { + FileOutputStream fos = null; + BufferedOutputStream bos = null; + + file.getParentFile().mkdirs(); + + try { + fos = new FileOutputStream(file); + bos = new BufferedOutputStream(fos); + + saveContent(bos); + } finally { + closeQuietly(bos); + closeQuietly(fos); + } + + return this; + } + + /** + * Save the result to an output stream. + * + * @param out the output stream + * @return this object + * @throws java.io.IOException on I/O error + * @throws InterruptedException on interruption + */ + public BufferedResponse saveContent(OutputStream out) throws IOException, InterruptedException { + out.write(data); + + return this; + } + } + + @Data + public static class PartialDownloadInfo { + private final long expectedLength; + private final long currentLength; + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/util/LimitLinesDocumentListener.java b/launcher/src/main/java/com/skcraft/launcher/util/LimitLinesDocumentListener.java new file mode 100644 index 0000000..6f9c09a --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/util/LimitLinesDocumentListener.java @@ -0,0 +1,124 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.util; + +import javax.swing.*; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.text.BadLocationException; +import javax.swing.text.Document; +import javax.swing.text.Element; + +/** + * From http://tips4java.wordpress.com/2008/10/15/limit-lines-in-document/ + * + * @author Rob Camick + */ +public class LimitLinesDocumentListener implements DocumentListener { + private int maximumLines; + private boolean isRemoveFromStart; + private volatile boolean isRemoving; + + /** + * Specify the number of lines to be stored in the Document. Extra lines + * will be removed from the start or end of the Document, depending on + * the boolean value specified. + * + * @param maximumLines number of lines + * @param isRemoveFromStart true to remove from the start + */ + public LimitLinesDocumentListener(int maximumLines, + boolean isRemoveFromStart) { + setLimitLines(maximumLines); + this.isRemoveFromStart = isRemoveFromStart; + this.isRemoving = false; + } + + /** + * Set the maximum number of lines to be stored in the Document + * + * @param maximumLines number of lines + */ + public void setLimitLines(int maximumLines) { + if (maximumLines < 1) { + throw new IllegalArgumentException("Maximum lines must be greater than 0"); + } + + this.maximumLines = maximumLines; + } + + @Override + public void insertUpdate(final DocumentEvent e) { + // Changes to the Document can not be done within the listener + // so we need to add the processing to the end of the EDT + + if (!this.isRemoving) { + this.isRemoving = true; + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + removeLines(e); + } + }); + } + } + + @Override + public void removeUpdate(DocumentEvent e) { + } + + @Override + public void changedUpdate(DocumentEvent e) { + } + + private void removeLines(DocumentEvent e) { + // The root Element of the Document will tell us the total number + // of line in the Document. + + try { + Document document = e.getDocument(); + Element root = document.getDefaultRootElement(); + int excess = root.getElementCount() - maximumLines; + + if (excess > 0) { + if (isRemoveFromStart) { + removeFromStart(document, root, excess); + } else { + removeFromEnd(document, root); + } + } + } finally { + this.isRemoving = false; + } + } + + private void removeFromStart(Document document, Element root, int excess) { + Element line = root.getElement(excess - 1); + int end = line.getEndOffset(); + + try { + document.remove(0, end); + } catch (BadLocationException ble) { + System.out.println(ble); + } + } + + private void removeFromEnd(Document document, Element root) { + // We use start minus 1 to make sure we remove the newline + // character of the previous line + + Element line = root.getElement(maximumLines); + int start = line.getStartOffset(); + int end = root.getEndOffset(); + + try { + document.remove(start - 1, end - start); + } catch (BadLocationException ble) { + System.out.println(ble); + } + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/util/LocaleEncodingControl.java b/launcher/src/main/java/com/skcraft/launcher/util/LocaleEncodingControl.java new file mode 100644 index 0000000..72e5932 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/util/LocaleEncodingControl.java @@ -0,0 +1,92 @@ +package com.skcraft.launcher.util; + +import lombok.extern.java.Log; + +import java.io.*; +import java.net.URL; +import java.net.URLConnection; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Locale; +import java.util.PropertyResourceBundle; +import java.util.ResourceBundle; + +@Log +public class LocaleEncodingControl extends ResourceBundle.Control { + @Override + public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader, boolean reload) throws IllegalAccessException, InstantiationException, IOException { + if (!format.equals("java.properties")) { + return super.newBundle(baseName, locale, format, loader, reload); + } + + String bundleName = this.toBundleName(baseName, locale); + String resourceName = this.toResourceName(bundleName, "properties"); + + InputStream is = this.getResourceAsStream(resourceName, loader, reload); + if (is == null) { + return null; + } + + BufferedInputStream bis = new BufferedInputStream(is); + + // Let's do the timewalk + boolean isUtf8; + bis.mark(3); + { + byte[] buf = new byte[3]; + readFully(bis, buf); + + // the BOM is 0xEF,0xBB,0xBF + isUtf8 = buf[0] == (byte) 0xEF && buf[1] == (byte) 0xBB && buf[2] == (byte) 0xBF; + } + bis.reset(); + + if (isUtf8) { + log.info("Found UTF-8 locale file " + resourceName); + } + + Charset charset = isUtf8 ? StandardCharsets.UTF_8 : StandardCharsets.ISO_8859_1; + Reader reader = new InputStreamReader(bis, charset); + + try { + PropertyResourceBundle bundle = new PropertyResourceBundle(reader); + reader.close(); + + return bundle; + } finally { + // Just in case of exception... + reader.close(); + } + } + + private InputStream getResourceAsStream(String resourceName, ClassLoader loader, boolean reload) throws IOException { + if (reload) { + URL url = loader.getResource(resourceName); + if (url != null) { + URLConnection conn = url.openConnection(); + if (conn != null) { + conn.setUseCaches(false); + + return conn.getInputStream(); + } + } + } + + return loader.getResourceAsStream(resourceName); + } + + private void readFully(InputStream is, byte[] buf) throws IOException { + int offset = 0; + int length = buf.length; + + while (length > 0) { + int n = is.read(buf, offset, length); + if (n == -1) { + throw new EOFException(); + } + + offset += n; + length -= n; + } + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/util/MorePaths.java b/launcher/src/main/java/com/skcraft/launcher/util/MorePaths.java new file mode 100644 index 0000000..65b78be --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/util/MorePaths.java @@ -0,0 +1,45 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.util; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; + +public final class MorePaths { + + private MorePaths() { + } + + public static boolean isSamePath(File a, File b) throws IOException { + return a.getCanonicalPath().equals(b.getCanonicalPath()); + } + + public static boolean isSubDirectory(File base, File child) throws IOException { + base = base.getCanonicalFile(); + child = child.getCanonicalFile(); + + File parentFile = child; + while (parentFile != null) { + if (base.equals(parentFile)) { + return true; + } + + parentFile = parentFile.getParentFile(); + } + + return false; + } + + public static String relativize(File base, File child) { + Path basePath = Paths.get(base.getAbsolutePath()); + Path childPath = Paths.get(child.getAbsolutePath()); + return basePath.relativize(childPath).toString(); + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/util/PastebinPoster.java b/launcher/src/main/java/com/skcraft/launcher/util/PastebinPoster.java new file mode 100644 index 0000000..285c595 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/util/PastebinPoster.java @@ -0,0 +1,114 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.util; + +import java.io.*; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; + +public class PastebinPoster { + private static final int CONNECT_TIMEOUT = 5000; + private static final int READ_TIMEOUT = 5000; + + public static void paste(String code, PasteCallback callback) { + PasteProcessor processor = new PasteProcessor(code, callback); + Thread thread = new Thread(processor); + thread.start(); + } + + public static interface PasteCallback { + public void handleSuccess(String url); + public void handleError(String err); + } + + private static class PasteProcessor implements Runnable { + private String code; + private PasteCallback callback; + + public PasteProcessor(String code, PasteCallback callback) { + this.code = code; + this.callback = callback; + } + + @Override + public void run() { + HttpURLConnection conn = null; + OutputStream out = null; + InputStream in = null; + + try { + URL url = new URL("https://pastebin.com/api/api_post.php"); + conn = (HttpURLConnection) url.openConnection(); + conn.setConnectTimeout(CONNECT_TIMEOUT); + conn.setReadTimeout(READ_TIMEOUT); + conn.setRequestMethod("POST"); + conn.addRequestProperty("Content-type", "application/x-www-form-urlencoded"); + conn.setInstanceFollowRedirects(false); + conn.setDoOutput(true); + out = conn.getOutputStream(); + + out.write(("api_option=paste" + + "&api_dev_key=" + URLEncoder.encode("4867eae74c6990dbdef07c543cf8f805", "utf-8") + + "&api_paste_code=" + URLEncoder.encode(code, "utf-8") + + "&api_paste_private=" + URLEncoder.encode("0", "utf-8") + + "&api_paste_name=" + URLEncoder.encode("", "utf-8") + + "&api_paste_expire_date=" + URLEncoder.encode("1D", "utf-8") + + "&api_paste_format=" + URLEncoder.encode("text", "utf-8") + + "&api_user_key=" + URLEncoder.encode("", "utf-8")).getBytes()); + out.flush(); + out.close(); + + if (conn.getResponseCode() == 200) { + in = conn.getInputStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(in)); + String line; + StringBuilder response = new StringBuilder(); + while ((line = reader.readLine()) != null) { + response.append(line); + response.append("\r\n"); + } + reader.close(); + + String result = response.toString().trim(); + + if (result.matches("^https?://.*")) { + callback.handleSuccess(result.trim()); + } else { + String err = result.trim(); + if (err.length() > 100) { + err = err.substring(0, 100); + } + callback.handleError(err); + } + } else { + callback.handleError("An error occurred while uploading the text."); + } + } catch (IOException e) { + callback.handleError(e.getMessage()); + } finally { + if (conn != null) { + conn.disconnect(); + } + if (in != null) { + try { + in.close(); + } catch (IOException ignored) { + } + } + if (out != null) { + try { + out.close(); + } catch (IOException ignored) { + } + } + } + } + + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/util/Platform.java b/launcher/src/main/java/com/skcraft/launcher/util/Platform.java new file mode 100644 index 0000000..a338351 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/util/Platform.java @@ -0,0 +1,20 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.util; + +import javax.xml.bind.annotation.XmlEnumValue; + +/** + * Indicates the platform. + */ +public enum Platform { + @XmlEnumValue("windows") WINDOWS, + @XmlEnumValue("mac_os_x") MAC_OS_X, + @XmlEnumValue("linux") LINUX, + @XmlEnumValue("solaris") SOLARIS, + @XmlEnumValue("unknown") UNKNOWN +} \ No newline at end of file diff --git a/launcher/src/main/java/com/skcraft/launcher/util/SharedLocale.java b/launcher/src/main/java/com/skcraft/launcher/util/SharedLocale.java new file mode 100644 index 0000000..a58b415 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/util/SharedLocale.java @@ -0,0 +1,106 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.util; + +import lombok.NonNull; +import lombok.extern.java.Log; + +import java.text.MessageFormat; +import java.util.Locale; +import java.util.MissingResourceException; +import java.util.ResourceBundle; +import java.util.logging.Level; + +/** + * Handles loading a shared message {@link java.util.ResourceBundle}. + */ +@Log +public class SharedLocale { + + private static Locale locale = Locale.getDefault(); + private static ResourceBundle bundle; + + /** + * Get the current locale. + * + * @return the current locale + */ + public static Locale getLocale() { + return locale; + } + + /** + * Get the current resource bundle. + * + * @return the current resource bundle, or null if not available + */ + public static ResourceBundle getBundle() { + return bundle; + } + + /** + * Translate a string. + * + *

If the string is not available, then ${key} will be returned.

+ * + * @param key the key + * @return the translated string + */ + public static String tr(String key) { + if (bundle != null) { + try { + return bundle.getString(key); + } catch (MissingResourceException e) { + log.log(Level.WARNING, "Failed to find message", e); + } + } + + return "${" + key + "}"; + } + + /** + * Format a translated string. + * + *

If the string is not available, then ${key}:args will be returned.

+ * + * @param key the key + * @param args arguments + * @return a translated string + */ + public static String tr(String key, Object... args) { + if (bundle != null) { + try { + MessageFormat formatter = new MessageFormat(tr(key)); + formatter.setLocale(getLocale()); + return formatter.format(args); + } catch (MissingResourceException e) { + log.log(Level.WARNING, "Failed to find message", e); + } + } + + return "${" + key + "}:" + args; + } + + /** + * Load a shared resource bundle. + * + * @param baseName the bundle name + * @param locale the locale + * @return true if loaded successfully + */ + public static boolean loadBundle(@NonNull String baseName, @NonNull Locale locale) { + try { + SharedLocale.locale = locale; + bundle = ResourceBundle.getBundle(baseName, locale, + SharedLocale.class.getClassLoader(), new LocaleEncodingControl()); + return true; + } catch (MissingResourceException e) { + log.log(Level.SEVERE, "Failed to load resource bundle", e); + return false; + } + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/util/SimpleLogFormatter.java b/launcher/src/main/java/com/skcraft/launcher/util/SimpleLogFormatter.java new file mode 100644 index 0000000..4cd9cc4 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/util/SimpleLogFormatter.java @@ -0,0 +1,63 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.util; + +import lombok.extern.java.Log; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.logging.*; + +@Log +public final class SimpleLogFormatter extends Formatter { + + private static final String LINE_SEPARATOR = System.getProperty("line.separator"); + + @Override + public String format(LogRecord record) { + StringBuilder sb = new StringBuilder(); + + sb.append("[") + .append(record.getLevel().getLocalizedName().toLowerCase()) + .append("] ") + .append(formatMessage(record)) + .append(LINE_SEPARATOR); + + if (record.getThrown() != null) { + try { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + record.getThrown().printStackTrace(pw); + pw.close(); + sb.append(sw.toString()); + } catch (Exception e) { + } + } + + return sb.toString(); + } + + public static void configureGlobalLogger() { + Logger globalLogger = Logger.getLogger(""); + + // Set formatter + for (Handler handler : globalLogger.getHandlers()) { + handler.setFormatter(new SimpleLogFormatter()); + } + + // Set level + String logLevel = System.getProperty( + SimpleLogFormatter.class.getCanonicalName() + ".logLevel", "INFO"); + try { + Level level = Level.parse(logLevel); + globalLogger.setLevel(level); + } catch (IllegalArgumentException e) { + log.log(Level.WARNING, "Invalid log level of " + logLevel, e); + } + } + +} \ No newline at end of file diff --git a/launcher/src/main/java/com/skcraft/launcher/util/SwingExecutor.java b/launcher/src/main/java/com/skcraft/launcher/util/SwingExecutor.java new file mode 100644 index 0000000..39b9be2 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/util/SwingExecutor.java @@ -0,0 +1,51 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.util; + +import com.google.common.util.concurrent.AbstractListeningExecutorService; + +import javax.swing.*; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public final class SwingExecutor extends AbstractListeningExecutorService { + + public static final SwingExecutor INSTANCE = new SwingExecutor(); + + private SwingExecutor() { + } + + @Override + public void execute(Runnable runnable) { + SwingUtilities.invokeLater(runnable); + } + + @Override + public void shutdown() { + } + + @Override + public List shutdownNow() { + return new ArrayList(); + } + + @Override + public boolean isShutdown() { + return false; + } + + @Override + public boolean isTerminated() { + return false; + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + return false; + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/util/WinRegistry.java b/launcher/src/main/java/com/skcraft/launcher/util/WinRegistry.java new file mode 100644 index 0000000..d7ca542 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/util/WinRegistry.java @@ -0,0 +1,49 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.util; + +import com.google.common.collect.ImmutableList; +import com.sun.jna.platform.win32.Advapi32Util; +import com.sun.jna.platform.win32.WinReg; + +import java.util.List; + +/** + * Windows registry helper via JNA platform + */ +public class WinRegistry { + private WinRegistry() { + } + + /** + * Read a value from a path and value name + * + * @param hkey Hive key + * @param key Registry path + * @param valueName Value key + * @return String value from the registry + * @throws com.sun.jna.platform.win32.Win32Exception If an error occurs accessing the registry + * or the path does not exist + */ + public static String readString(WinReg.HKEY hkey, String key, String valueName) { + return Advapi32Util.registryGetStringValue(hkey, key, valueName); + } + + /** + * Get the subkeys of a given path + * + * @param hkey Hive key + * @param key Registry path + * @return the subkeys of a given path + * + * @throws com.sun.jna.platform.win32.Win32Exception If an error occurs accessing the registry + * or the path does not exist + */ + public static List readStringSubKeys(WinReg.HKEY hkey, String key) { + return ImmutableList.copyOf(Advapi32Util.registryGetKeys(hkey, key)); + } +} diff --git a/launcher/src/main/resources/com/skcraft/launcher/custom_instance_icon.png b/launcher/src/main/resources/com/skcraft/launcher/custom_instance_icon.png new file mode 100644 index 0000000..867849c Binary files /dev/null and b/launcher/src/main/resources/com/skcraft/launcher/custom_instance_icon.png differ diff --git a/launcher/src/main/resources/com/skcraft/launcher/default_skin.png b/launcher/src/main/resources/com/skcraft/launcher/default_skin.png new file mode 100644 index 0000000..4c885f1 Binary files /dev/null and b/launcher/src/main/resources/com/skcraft/launcher/default_skin.png differ diff --git a/launcher/src/main/resources/com/skcraft/launcher/download_icon.png b/launcher/src/main/resources/com/skcraft/launcher/download_icon.png new file mode 100644 index 0000000..7a76bcb Binary files /dev/null and b/launcher/src/main/resources/com/skcraft/launcher/download_icon.png differ diff --git a/launcher/src/main/resources/com/skcraft/launcher/icon.png b/launcher/src/main/resources/com/skcraft/launcher/icon.png new file mode 100644 index 0000000..8c74095 Binary files /dev/null and b/launcher/src/main/resources/com/skcraft/launcher/icon.png differ diff --git a/launcher/src/main/resources/com/skcraft/launcher/instance_icon.png b/launcher/src/main/resources/com/skcraft/launcher/instance_icon.png new file mode 100644 index 0000000..8c74095 Binary files /dev/null and b/launcher/src/main/resources/com/skcraft/launcher/instance_icon.png differ diff --git a/launcher/src/main/resources/com/skcraft/launcher/lang/Launcher.properties b/launcher/src/main/resources/com/skcraft/launcher/lang/Launcher.properties new file mode 100644 index 0000000..ef48f3c --- /dev/null +++ b/launcher/src/main/resources/com/skcraft/launcher/lang/Launcher.properties @@ -0,0 +1,210 @@ +# +# SK's Minecraft Launcher +# Copyright (C) 2010-2014 Albert Pham and contributors +# Please see LICENSE.txt for license information. +# + +errorTitle=An error has occurred +confirmTitle=Confirm + +context.cut=Cut +context.copy=Copy +context.paste=Paste +context.delete=Delete +context.selectAll=Select all + +errors.openUrlError=Failed to open URL\: {0} +errors.openDirError=Unable to open ''{0}''. Maybe it doesn''t exist? +errors.reportErrorPreface=To report this error, please provide\:\n\n +errors.genericError=An error has occurred. +errors.updateRequiredError=Please download a new version of the launcher to continue updating. See skcraft.com for more information. +errors.selfUpdateCheckError=Checking for an update to the launcher has failed. + +button.cancel=Cancel +button.ok=OK +button.save=Save +button.launchAnyway=Launch Anyway + +options.title = Options +options.useProxyCheck = Use following proxy in Minecraft +options.jvmPath=Fallback Java Runtime\: +options.jvmRuntime=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)\: +options.maxMemory=Maximum memory (MB)\: +options.permGen=PermGen (MB)\: +options.javaTab=Java +options.windowWidth=Window width\: +options.windowHeight=Window height\: +options.minecraftTab=Minecraft +options.proxyHost=Proxy host\: +options.proxyPort=Proxy port\: +options.proxyUsername=Proxy username\: +options.proxyPassword=Proxy password\: +options.proxyTab=Proxy +options.gameKey=Game key\: +options.advancedTab=Advanced +options.about=About +options.launcherConsole=Console + +instance.launch=Launch +instance.install=Install +instance.openFolder=View folder +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... +instance.confirmDelete=Are you sure that you wish to delete ALL THE FILES (screenshots, worlds, configs) for ''{0}''? +instance.deletingTitle=Deleting instance... +instance.deletingStatus=Deleting ''{0}'' and files... +instance.confirmHardUpdate=A hard force update will delete the contents of config/ and mods/ and then require an update. Are you sure that you want to continue? +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... +launcher.updateLauncher=Update launcher... +launcher.downloadUpdates=Download modpack updates +launcher.title=SKCraft Launcher (v{0}) +launcher.appTitle=SKCraft Launcher +launcher.refreshList=Refresh list +launcher.checkingTitle=Getting available modpacks... +launcher.checkingStatus=Getting available modpacks... Please wait. +launcher.selfUpdatingTitle=Updating launcher... +launcher.selfUpdatingStatus=Downloading launcher update... +launcher.selfUpdateComplete=Restart the launcher to use the new version. +launcher.selfUpdateCompleteTitle=Update complete +launcher.updatingTitle=Updating... +launcher.updatingStatus=Updating ''{0}''... Please wait. +launcher.noInstanceError=Please select a modpack to launch. +launcher.noInstanceTitle=No Modpack Selected +launcher.launchingTItle=Launching the game... +launcher.launchingStatus=Launching ''{0}''. Please wait. +launcher.javaMismatchTitle=Java version warning +launcher.modpackColumn=Modpack +launcher.notInstalledHint=(not installed) +launcher.requiresUpdateHint=(requires update) +launcher.updatePendingHint=(pending update) + +accounts.title=Select the account to play with +accounts.play=Play! +accounts.refreshingStatus=Refreshing login session... +accounts.addMojang=Add Mojang account +accounts.addMicrosoft=Add Microsoft account +accounts.removeSelected=Forget selected account +accounts.confirmForgetTitle=Forget account +accounts.confirmForget=Are you sure that you want to forget that account? + +login.login=Login... +login.recoverAccount=Forgot your login? +login.playOffline=Play offline +login.title=Minecraft Login +login.defaultMessage=Log in with your Mojang account +login.idEmail=ID/Email\: +login.password=Password\: +login.noPasswordError=Please enter a password. +login.noPasswordTitle=Missing Password +login.loggingInTitle=Logging in... +login.loggingInStatus=Logging in to Minecraft... +login.noLoginError=Please enter your account details. +login.noLoginTitle=Missing Account +login.minecraftNotOwnedError=Sorry, Minecraft is not owned on that account. +login.relogin=Please log in again. ({0}) + +login.microsoft.seeBrowser=Check your browser to login with Microsoft. +login.xbox.generic=Failed to authenticate with Xbox Live. +login.xbox.noXboxAccount=That account does not have an Xbox account associated! +login.xbox.isChild=The account is a child (under 18) and cannot proceed unless it is part of a Family. +login.xbox.unknown=An unknown error occurred while logging in with Xbox (XErr {0}) +login.xbox.errorMessage=An unknown error occurred while logging in with Xbox: {0} +login.minecraft.error=An error occurred while authorizing with Minecraft services: {0} + +console.title=Messages and Errors +console.launcherConsoleTitle=Launcher Messages +console.uploadLog=Upload Log +console.pasteUploading=Uploading {0} bytes...\n +console.pasteUploaded=Paste uploaded\: {0}\n +console.pasteFailed=Upload failed\: {0}\n +console.processEndCode=Process ended with code\: {0} +console.attachedToProcess=The game is running. Please wait. +console.forceClose=Force Close +console.trayTooltip=SKCraft Launcher +console.trayTitle=SKCraft Launcher +console.tray.showWindow=Show window +console.tray.forceClose=Force close... +console.closeWindow=Close Window +console.hideWindow=Hide Window +console.confirmKill=Are sure that you wish to close the game forcefully? You may lose data. +console.confirmKillTitle=Are you sure? +console.clearLog=Clear + +downloader.downloadingItem=Downloading {0}... +downloader.downloadingList=Downloading {0} files... ({1} remaining, {2} failed) +downloader.jobProgress={1,number}%\t{0} +downloader.jobPending=...\t{0} +downloader.noDownloads=No pending downloads. +downloader.failedCount=({0} have failed) + +progress.details=Details... +progress.less=Less... +progress.viewLog=View log +progress.confirmCancel=Are you sure that you wish to cancel? +progress.confirmCancelTitle=Cancel +progress.defaultStatus=Working... +progress.percentTitle=({0}%) {1} + +installer.installing=Installing... +installer.executing=Executing tasks... ({0} remaining) +installer.copyingFile=Copying from {0} to {1} +installer.movingFile=Moving {0} to {1} +installer.runningProcessor=Running processor {0}: {1} + +updater.updating=Updating launcher... +updater.updateRequiredButOffline=An update is required but you need to be in online mode. +updater.updateRequiredButNoManifest=An update is required but update information for this instance is no longer available. + +instanceUpdater.preparingUpdate=Preparing to update... +instanceUpdater.readingManifest=Reading package manifest... +instanceUpdater.readingVersion=Reading version manifest... +instanceUpdater.buildingDownloadList=Collecting files to download... +instanceUpdater.collectingLibraries=Collecting libraries to download... +instanceUpdater.collectingAssets=Collecting assets to download... + +instanceDeleter.deleting=Deleting {0}... +instanceDeleter.failures={0} file(s) could not be deleted. + +instanceResetter.resetting=Resetting {0}... +instanceLoader.loadingLocal=Loading local instances from disk... +instanceLoader.checkingRemote=Checking for new modpacks... + +runner.preparing=Preparing for launch... +runner.collectingArgs=Collecting process arguments... +runner.startingJava=Starting java... +runner.updateRequired=This instance must be updated before it can be run. +runner.missingLibrary={0} needs to be relaunched and updated because the library ''{1}'' is missing. +runner.missingAssetsIndex={0} needs to be relaunched and updated because its asset index is missing. +runner.corruptAssetsIndex={0} needs to be relaunched and updated because its asset index is corrupt. +runner.wrongJavaVersion=Instance ''{0}'' requires Java version {1}, but could only find {2}. + +assets.expanding1=Expanding {0} asset... ({1} remaining) +assets.expandingN=Expanding {0} assets... ({1} remaining) +assets.missingIndex=You need to update this instance because its index file at ''{0}'' is missing. +assets.missingObject=You need to update this instance because the file at ''{0}'' is missing. + +features.nameColumn=Feature +features.title=Select Features +features.install=OK +features.selectForInfo=Select a feature to see more information. +features.intro=Please select the optional features to install. +features.starred=(recommended) +features.avoid=(not recommended) diff --git a/launcher/src/main/resources/com/skcraft/launcher/launcher.properties b/launcher/src/main/resources/com/skcraft/launcher/launcher.properties new file mode 100644 index 0000000..1ff0c13 --- /dev/null +++ b/launcher/src/main/resources/com/skcraft/launcher/launcher.properties @@ -0,0 +1,26 @@ +# +# SK's Minecraft Launcher +# Copyright (C) 2010-2014 Albert Pham and contributors +# Please see LICENSE.txt for license information. +# + +# Leave the version as ${project.version} to pull it from the build.gradle, +# or replace it with a version string if you wish. +version=${project.version} + +# These can be left as-is or changed to your liking +launcherShortname=SKCLauncher +offlinePlayerName=Player + +# Only change these if you know what you're doing. +versionManifestUrl=https://launchermeta.mojang.com/mc/game/version_manifest.json +librariesSource=https://libraries.minecraft.net/ +assetsSource=https://resources.download.minecraft.net/ +yggdrasilAuthUrl=https://authserver.mojang.com/authenticate +microsoftClientId=d18bb4d8-a27f-4451-a87f-fe6de4436813 +resetPasswordUrl=https://minecraft.net/resetpassword + +# You MUST change these from the defaults. These URLs are provides as examples only. +newsUrl=https://update.skcraft.com/template/news.html?version=%s +packageListUrl=https://update.skcraft.com/template/packages.json?key=%s +selfUpdateUrl=https://update.skcraft.com/template/launcher/latest.json diff --git a/launcher/src/main/resources/com/skcraft/launcher/logging/client-1.12.xml b/launcher/src/main/resources/com/skcraft/launcher/logging/client-1.12.xml new file mode 100644 index 0000000..2e9ac98 --- /dev/null +++ b/launcher/src/main/resources/com/skcraft/launcher/logging/client-1.12.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/launcher/src/main/resources/com/skcraft/launcher/logging/client-1.7.xml b/launcher/src/main/resources/com/skcraft/launcher/logging/client-1.7.xml new file mode 100644 index 0000000..87f3dea --- /dev/null +++ b/launcher/src/main/resources/com/skcraft/launcher/logging/client-1.7.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/launcher/src/main/resources/com/skcraft/launcher/login.html b/launcher/src/main/resources/com/skcraft/launcher/login.html new file mode 100644 index 0000000..df0fcac --- /dev/null +++ b/launcher/src/main/resources/com/skcraft/launcher/login.html @@ -0,0 +1,25 @@ + + + + Login success + + + + +
+ icon.png +

Authentication successful, you may return to the launcher

+
+ + diff --git a/launcher/src/main/resources/com/skcraft/launcher/maven_repos.json b/launcher/src/main/resources/com/skcraft/launcher/maven_repos.json new file mode 100644 index 0000000..ec8063e --- /dev/null +++ b/launcher/src/main/resources/com/skcraft/launcher/maven_repos.json @@ -0,0 +1,7 @@ +[ + "https://libraries.minecraft.net/", + "https://repo1.maven.org/maven2/", + "https://files.minecraftforge.net/maven/", + "https://maven.fabricmc.net/", + "http://maven.apache.org/" +] diff --git a/launcher/src/main/resources/com/skcraft/launcher/tray_closed.png b/launcher/src/main/resources/com/skcraft/launcher/tray_closed.png new file mode 100644 index 0000000..5e70e5b Binary files /dev/null and b/launcher/src/main/resources/com/skcraft/launcher/tray_closed.png differ diff --git a/launcher/src/main/resources/com/skcraft/launcher/tray_ok.png b/launcher/src/main/resources/com/skcraft/launcher/tray_ok.png new file mode 100644 index 0000000..867849c Binary files /dev/null and b/launcher/src/main/resources/com/skcraft/launcher/tray_ok.png differ diff --git a/readme/configure_features.png b/readme/configure_features.png new file mode 100644 index 0000000..912d501 Binary files /dev/null and b/readme/configure_features.png differ diff --git a/readme/features.png b/readme/features.png new file mode 100644 index 0000000..aef9375 Binary files /dev/null and b/readme/features.png differ diff --git a/readme/launcher.png b/readme/launcher.png new file mode 100644 index 0000000..0e797df Binary files /dev/null and b/readme/launcher.png differ diff --git a/readme/launcher_skinned.png b/readme/launcher_skinned.png new file mode 100644 index 0000000..df9128e Binary files /dev/null and b/readme/launcher_skinned.png differ diff --git a/readme/log.png b/readme/log.png new file mode 100644 index 0000000..4484f2d Binary files /dev/null and b/readme/log.png differ diff --git a/readme/modpack_creator.png b/readme/modpack_creator.png new file mode 100644 index 0000000..d0a7d24 Binary files /dev/null and b/readme/modpack_creator.png differ diff --git a/readme/now_with_support.png b/readme/now_with_support.png new file mode 100644 index 0000000..929dfba Binary files /dev/null and b/readme/now_with_support.png differ diff --git a/readme/options.png b/readme/options.png new file mode 100644 index 0000000..f7423b1 Binary files /dev/null and b/readme/options.png differ diff --git a/readme/pack_manager.png b/readme/pack_manager.png new file mode 100644 index 0000000..5df4d38 Binary files /dev/null and b/readme/pack_manager.png differ diff --git a/readme/packages_generator.png b/readme/packages_generator.png new file mode 100644 index 0000000..9735b9a Binary files /dev/null and b/readme/packages_generator.png differ diff --git a/sample-files/README.txt b/sample-files/README.txt new file mode 100644 index 0000000..549cda4 --- /dev/null +++ b/sample-files/README.txt @@ -0,0 +1,32 @@ +These are sample files for a project. + +- lightpack/ is one modpack +- monsterpack/ is another modpack +- upload/ is what you'd upload to your website + +In upload: + +- "latest.json" was written by hand +- "packages.php" automatically generates the package listing, but your web server needs PHP installed to use it +- "packages.json" is what packages.php would have generated, but you can write it by hand instead +- "news.html" is your news page + +Commands +======== + +The following commands were run to generate the modpack files in upload/ + +java -jar ../launcher-builder/build/libs/launcher-builder-4.2.3-SNAPSHOT-all.jar --version 20100223 --input lightpack --output upload --manifest-dest "upload\lightpack.json" --pretty-print + +java -jar ../launcher-builder/build/libs/launcher-builder-4.2.3-SNAPSHOT-all.jar --version 20100223 --input monsterpack --output upload --manifest-dest "upload\monsterpack.json" --pretty-print + +launcher.properties +=================== + +If you had uploaded the contents of upload/ to http://example.com/launcher/, you would make launcher.properties look like this: + +newsUrl=http://example.com/launcher/news.html?version=%s +packageListUrl=http://example.com/launcher/packages.php?key=%s +selfUpdateUrl=http://example.com/launcher/latest.json + +(note: it uses the .php version here) \ No newline at end of file diff --git a/sample-files/lightpack/loaders/README.txt b/sample-files/lightpack/loaders/README.txt new file mode 100644 index 0000000..47b74de --- /dev/null +++ b/sample-files/lightpack/loaders/README.txt @@ -0,0 +1,7 @@ +You'd put Forge and LiteLoader installers here. + +i.e. forge-1.7.10-10.13.0.1208-installer.jar + +It should NOT be the Windows version of the installer. + +Any installer will work that has a supported "install_profile.json" in its .jar. \ No newline at end of file diff --git a/sample-files/lightpack/modpack.json b/sample-files/lightpack/modpack.json new file mode 100644 index 0000000..0906dfb --- /dev/null +++ b/sample-files/lightpack/modpack.json @@ -0,0 +1,17 @@ +{ + "name": "Light Modpack", + "title": "Light Modpack", + "gameVersion": "1.8", + "launch": { + "flags": [ + "-Dfml.ignoreInvalidMinecraftCertificates=true" + ] + }, + "userFiles": { + "include": [ + "options.txt", + "optionsshaders.txt", + "mods/VoxelMods/*" + ] + } +} \ No newline at end of file diff --git a/sample-files/lightpack/src/_CLIENT/options.txt b/sample-files/lightpack/src/_CLIENT/options.txt new file mode 100644 index 0000000..6308e4c --- /dev/null +++ b/sample-files/lightpack/src/_CLIENT/options.txt @@ -0,0 +1,11 @@ +soundCategory_master:1.0 +soundCategory_music:0.0 +soundCategory_record:1.0 +soundCategory_weather:0.1056338 +soundCategory_block:1.0 +soundCategory_hostile:1.0 +soundCategory_neutral:1.0 +soundCategory_player:1.0 +soundCategory_ambient:1.0 +resourcePacks:["Conquest (smaller).zip"] +enableVsync:false diff --git a/sample-files/lightpack/src/config/_SERVER/forgeChunkLoading.cfg b/sample-files/lightpack/src/config/_SERVER/forgeChunkLoading.cfg new file mode 100644 index 0000000..3051bdf --- /dev/null +++ b/sample-files/lightpack/src/config/_SERVER/forgeChunkLoading.cfg @@ -0,0 +1,47 @@ +# Configuration file + +########################################################################################################## +# Forge +#--------------------------------------------------------------------------------------------------------# +# Sample mod specific control section. +# Copy this section and rename the with the modid for the mod you wish to override. +# A value of zero in either entry effectively disables any chunkloading capabilities +# for that mod +########################################################################################################## + +Forge { + # Maximum chunks per ticket for the mod. + I:maximumChunksPerTicket=25 + + # Maximum ticket count for the mod. Zero disables chunkloading capabilities. + I:maximumTicketCount=200 +} + + +########################################################################################################## +# defaults +#--------------------------------------------------------------------------------------------------------# +# Default configuration for forge chunk loading control +########################################################################################################## + +defaults { + # Are mod overrides enabled? + B:enabled=true + + # The default maximum number of chunks a mod can force, per ticket, + # for a mod without an override. This is the maximum number of chunks a single ticket can force. + I:maximumChunksPerTicket=25 + + # The default maximum ticket count for a mod which does not have an override + # in this file. This is the number of chunk loading requests a mod is allowed to make. + I:maximumTicketCount=200 + + # The number of tickets a player can be assigned instead of a mod. This is shared across all mods and it is up to the mods to use it. + I:playerTicketCount=500 + + # Unloaded chunks can first be kept in a dormant cache for quicker + # loading times. Specify the size (in chunks) of that cache here + I:dormantChunkCacheSize=0 +} + + diff --git a/sample-files/lightpack/src/config/forge.cfg b/sample-files/lightpack/src/config/forge.cfg new file mode 100644 index 0000000..72eab30 --- /dev/null +++ b/sample-files/lightpack/src/config/forge.cfg @@ -0,0 +1,60 @@ +# Configuration file + +general { + # Set to true to disable Forge's version check mechanics. Forge queries a small json file on our server for version information. For more details see the ForgeVersion class in our github. + B:disableVersionCheck=false + + # Controls the number threshold at which Packet51 is preferred over Packet52, default and minimum 64, maximum 1024 + I:clumpingThreshold=64 + + # Set to true to enable the post initialization sorting of crafting recipes using Forge's sorter. May cause desyncing on conflicting recipies. MUST RESTART MINECRAFT IF CHANGED FROM THE CONFIG GUI. + B:sortRecipies=true + + # Set this to true to force a crash if more than one block attempts to link back to the same Fluid. Enabled by default. + B:forceDuplicateFluidBlockCrash=true + + # Set this to true to remove any Entity that throws an error in its update method instead of closing the server and reporting a crash log. BE WARNED THIS COULD SCREW UP EVERYTHING USE SPARINGLY WE ARE NOT RESPONSIBLE FOR DAMAGES. + B:removeErroringEntities=false + + # Set this to true to remove any TileEntity that throws an error in its update method instead of closing the server and reporting a crash log. BE WARNED THIS COULD SCREW UP EVERYTHING USE SPARINGLY WE ARE NOT RESPONSIBLE FOR DAMAGES. + B:removeErroringTileEntities=false + + # Set this to true to check the entire entity's collision bounding box for ladders instead of just the block they are in. Causes noticable differences in mechanics so default is vanilla behavior. Default: false + B:fullBoundingBoxLadders=false + + # Control the range of sky blending for colored skies in biomes. + I:biomeSkyBlendRange < + 2 + 4 + 6 + 8 + 10 + 12 + 14 + 16 + 18 + 20 + 22 + 24 + 26 + 28 + 30 + 32 + 34 + > + + # Base zombie summoning spawn chance. Allows changing the bonus zombie summoning mechanic. + D:zombieBaseSummonChance=0.1 + + # Chance that a zombie (or subclass) is a baby. Allows changing the zombie spawning mechanic. + D:zombieBabyChance=0.05 + + # Set to false to attempt to allocate 8 stencil bits when starting the GL display context. + B:enableStencilBits=true + + # The spawn fuzz when a player respawns in the world, this is controlable by WorldType, this config option is for the default overworld. + I:defaultSpawnFuzz=20 + B:enableGlobalConfig=false +} + + diff --git a/sample-files/lightpack/src/config/worldedit/worldedit.properties b/sample-files/lightpack/src/config/worldedit/worldedit.properties new file mode 100644 index 0000000..fb8afa6 --- /dev/null +++ b/sample-files/lightpack/src/config/worldedit/worldedit.properties @@ -0,0 +1,38 @@ +#Don't put comments; they get removed +#Fri Nov 14 20:36:13 PST 2014 +super-pickaxe-many-drop-items=true +super-pickaxe-drop-items=true +craftscript-dir=craftscripts +max-brush-radius=10 +scripting-timeout=3000 +no-double-slash=false +butcher-max-radius=-1 +profile=false +max-radius=-1 +default-max-polyhedron-points=-1 +use-inventory-creative-override=false +use-inventory=false +shell-save-type= +allow-extra-data-values=false +wand-item=271 +history-size=15 +register-help=true +butcher-default-radius=-1 +schematic-save-dir=schematics +nav-wand-item=345 +max-polygon-points=20 +default-max-polygon-points=-1 +snapshots-dir= +nav-use-glass=true +use-inventory-override=false +cheat-mode=false +max-changed-blocks=-1 +default-max-changed-blocks=-1 +max-polyhedron-points=20 +allow-symbolic-links=false +use-in-creative=false +disallowed-blocks=6,26,27,28,31,32,34,36,37,38,39,40,46,50,51,55,59,66,69,75,76,93,94,77,81,83,7,14,15,16,56 +log-commands=false +log-file=worldedit.log +max-super-pickaxe-size=5 +nav-wand-distance=50 diff --git a/sample-files/lightpack/src/mods/_CLIENT/CoolGrass.info.json b/sample-files/lightpack/src/mods/_CLIENT/CoolGrass.info.json new file mode 100644 index 0000000..f154bd9 --- /dev/null +++ b/sample-files/lightpack/src/mods/_CLIENT/CoolGrass.info.json @@ -0,0 +1,7 @@ +{ + "feature": { + "name": "CoolGrass", + "description": "CoolGrass gives you a cool grass.", + "selected": false + } +} diff --git a/sample-files/lightpack/src/mods/_CLIENT/SomeMinimap.info.json b/sample-files/lightpack/src/mods/_CLIENT/SomeMinimap.info.json new file mode 100644 index 0000000..1fc899e --- /dev/null +++ b/sample-files/lightpack/src/mods/_CLIENT/SomeMinimap.info.json @@ -0,0 +1,8 @@ +{ + "feature": { + "name": "SomeMinimap", + "description": "SomeMinimap gives you a cool minimap.", + "recommendation": "starred", + "selected": true + } +} diff --git a/sample-files/monsterpack/loaders/README.txt b/sample-files/monsterpack/loaders/README.txt new file mode 100644 index 0000000..47b74de --- /dev/null +++ b/sample-files/monsterpack/loaders/README.txt @@ -0,0 +1,7 @@ +You'd put Forge and LiteLoader installers here. + +i.e. forge-1.7.10-10.13.0.1208-installer.jar + +It should NOT be the Windows version of the installer. + +Any installer will work that has a supported "install_profile.json" in its .jar. \ No newline at end of file diff --git a/sample-files/monsterpack/modpack.json b/sample-files/monsterpack/modpack.json new file mode 100644 index 0000000..8818f15 --- /dev/null +++ b/sample-files/monsterpack/modpack.json @@ -0,0 +1,17 @@ +{ + "name": "Monster Modpack", + "title": "Monster Modpack", + "gameVersion": "1.8", + "launch": { + "flags": [ + "-Dfml.ignoreInvalidMinecraftCertificates=true" + ] + }, + "userFiles": { + "include": [ + "options.txt", + "optionsshaders.txt", + "mods/VoxelMods/*" + ] + } +} \ No newline at end of file diff --git a/sample-files/monsterpack/src/_CLIENT/options.txt b/sample-files/monsterpack/src/_CLIENT/options.txt new file mode 100644 index 0000000..6308e4c --- /dev/null +++ b/sample-files/monsterpack/src/_CLIENT/options.txt @@ -0,0 +1,11 @@ +soundCategory_master:1.0 +soundCategory_music:0.0 +soundCategory_record:1.0 +soundCategory_weather:0.1056338 +soundCategory_block:1.0 +soundCategory_hostile:1.0 +soundCategory_neutral:1.0 +soundCategory_player:1.0 +soundCategory_ambient:1.0 +resourcePacks:["Conquest (smaller).zip"] +enableVsync:false diff --git a/sample-files/monsterpack/src/config/_SERVER/forgeChunkLoading.cfg b/sample-files/monsterpack/src/config/_SERVER/forgeChunkLoading.cfg new file mode 100644 index 0000000..3051bdf --- /dev/null +++ b/sample-files/monsterpack/src/config/_SERVER/forgeChunkLoading.cfg @@ -0,0 +1,47 @@ +# Configuration file + +########################################################################################################## +# Forge +#--------------------------------------------------------------------------------------------------------# +# Sample mod specific control section. +# Copy this section and rename the with the modid for the mod you wish to override. +# A value of zero in either entry effectively disables any chunkloading capabilities +# for that mod +########################################################################################################## + +Forge { + # Maximum chunks per ticket for the mod. + I:maximumChunksPerTicket=25 + + # Maximum ticket count for the mod. Zero disables chunkloading capabilities. + I:maximumTicketCount=200 +} + + +########################################################################################################## +# defaults +#--------------------------------------------------------------------------------------------------------# +# Default configuration for forge chunk loading control +########################################################################################################## + +defaults { + # Are mod overrides enabled? + B:enabled=true + + # The default maximum number of chunks a mod can force, per ticket, + # for a mod without an override. This is the maximum number of chunks a single ticket can force. + I:maximumChunksPerTicket=25 + + # The default maximum ticket count for a mod which does not have an override + # in this file. This is the number of chunk loading requests a mod is allowed to make. + I:maximumTicketCount=200 + + # The number of tickets a player can be assigned instead of a mod. This is shared across all mods and it is up to the mods to use it. + I:playerTicketCount=500 + + # Unloaded chunks can first be kept in a dormant cache for quicker + # loading times. Specify the size (in chunks) of that cache here + I:dormantChunkCacheSize=0 +} + + diff --git a/sample-files/monsterpack/src/config/forge.cfg b/sample-files/monsterpack/src/config/forge.cfg new file mode 100644 index 0000000..72eab30 --- /dev/null +++ b/sample-files/monsterpack/src/config/forge.cfg @@ -0,0 +1,60 @@ +# Configuration file + +general { + # Set to true to disable Forge's version check mechanics. Forge queries a small json file on our server for version information. For more details see the ForgeVersion class in our github. + B:disableVersionCheck=false + + # Controls the number threshold at which Packet51 is preferred over Packet52, default and minimum 64, maximum 1024 + I:clumpingThreshold=64 + + # Set to true to enable the post initialization sorting of crafting recipes using Forge's sorter. May cause desyncing on conflicting recipies. MUST RESTART MINECRAFT IF CHANGED FROM THE CONFIG GUI. + B:sortRecipies=true + + # Set this to true to force a crash if more than one block attempts to link back to the same Fluid. Enabled by default. + B:forceDuplicateFluidBlockCrash=true + + # Set this to true to remove any Entity that throws an error in its update method instead of closing the server and reporting a crash log. BE WARNED THIS COULD SCREW UP EVERYTHING USE SPARINGLY WE ARE NOT RESPONSIBLE FOR DAMAGES. + B:removeErroringEntities=false + + # Set this to true to remove any TileEntity that throws an error in its update method instead of closing the server and reporting a crash log. BE WARNED THIS COULD SCREW UP EVERYTHING USE SPARINGLY WE ARE NOT RESPONSIBLE FOR DAMAGES. + B:removeErroringTileEntities=false + + # Set this to true to check the entire entity's collision bounding box for ladders instead of just the block they are in. Causes noticable differences in mechanics so default is vanilla behavior. Default: false + B:fullBoundingBoxLadders=false + + # Control the range of sky blending for colored skies in biomes. + I:biomeSkyBlendRange < + 2 + 4 + 6 + 8 + 10 + 12 + 14 + 16 + 18 + 20 + 22 + 24 + 26 + 28 + 30 + 32 + 34 + > + + # Base zombie summoning spawn chance. Allows changing the bonus zombie summoning mechanic. + D:zombieBaseSummonChance=0.1 + + # Chance that a zombie (or subclass) is a baby. Allows changing the zombie spawning mechanic. + D:zombieBabyChance=0.05 + + # Set to false to attempt to allocate 8 stencil bits when starting the GL display context. + B:enableStencilBits=true + + # The spawn fuzz when a player respawns in the world, this is controlable by WorldType, this config option is for the default overworld. + I:defaultSpawnFuzz=20 + B:enableGlobalConfig=false +} + + diff --git a/sample-files/monsterpack/src/config/worldedit/worldedit.properties b/sample-files/monsterpack/src/config/worldedit/worldedit.properties new file mode 100644 index 0000000..fb8afa6 --- /dev/null +++ b/sample-files/monsterpack/src/config/worldedit/worldedit.properties @@ -0,0 +1,38 @@ +#Don't put comments; they get removed +#Fri Nov 14 20:36:13 PST 2014 +super-pickaxe-many-drop-items=true +super-pickaxe-drop-items=true +craftscript-dir=craftscripts +max-brush-radius=10 +scripting-timeout=3000 +no-double-slash=false +butcher-max-radius=-1 +profile=false +max-radius=-1 +default-max-polyhedron-points=-1 +use-inventory-creative-override=false +use-inventory=false +shell-save-type= +allow-extra-data-values=false +wand-item=271 +history-size=15 +register-help=true +butcher-default-radius=-1 +schematic-save-dir=schematics +nav-wand-item=345 +max-polygon-points=20 +default-max-polygon-points=-1 +snapshots-dir= +nav-use-glass=true +use-inventory-override=false +cheat-mode=false +max-changed-blocks=-1 +default-max-changed-blocks=-1 +max-polyhedron-points=20 +allow-symbolic-links=false +use-in-creative=false +disallowed-blocks=6,26,27,28,31,32,34,36,37,38,39,40,46,50,51,55,59,66,69,75,76,93,94,77,81,83,7,14,15,16,56 +log-commands=false +log-file=worldedit.log +max-super-pickaxe-size=5 +nav-wand-distance=50 diff --git a/sample-files/monsterpack/src/mods/_CLIENT/CoolGrass.info.json b/sample-files/monsterpack/src/mods/_CLIENT/CoolGrass.info.json new file mode 100644 index 0000000..f154bd9 --- /dev/null +++ b/sample-files/monsterpack/src/mods/_CLIENT/CoolGrass.info.json @@ -0,0 +1,7 @@ +{ + "feature": { + "name": "CoolGrass", + "description": "CoolGrass gives you a cool grass.", + "selected": false + } +} diff --git a/sample-files/monsterpack/src/mods/_CLIENT/SomeMinimap.info.json b/sample-files/monsterpack/src/mods/_CLIENT/SomeMinimap.info.json new file mode 100644 index 0000000..1fc899e --- /dev/null +++ b/sample-files/monsterpack/src/mods/_CLIENT/SomeMinimap.info.json @@ -0,0 +1,8 @@ +{ + "feature": { + "name": "SomeMinimap", + "description": "SomeMinimap gives you a cool minimap.", + "recommendation": "starred", + "selected": true + } +} diff --git a/sample-files/upload/latest.json b/sample-files/upload/latest.json new file mode 100644 index 0000000..04e1d2e --- /dev/null +++ b/sample-files/upload/latest.json @@ -0,0 +1,4 @@ +{ + "version": "4.0.1", + "url": "http://example.com/launcher/versions/4.0.1.jar.pack" +} \ No newline at end of file diff --git a/sample-files/upload/libraries/README.txt b/sample-files/upload/libraries/README.txt new file mode 100644 index 0000000..4ac1dc4 --- /dev/null +++ b/sample-files/upload/libraries/README.txt @@ -0,0 +1 @@ +Normally, libraries would be generated for this folder if we were actually using a mod loader (Forge, LiteLoader, etc.). \ No newline at end of file diff --git a/sample-files/upload/lightpack.json b/sample-files/upload/lightpack.json new file mode 100644 index 0000000..871961f --- /dev/null +++ b/sample-files/upload/lightpack.json @@ -0,0 +1,266 @@ +{ + "title" : "Light Modpack", + "name" : "Light Modpack", + "version" : "20100223", + "minimumVersion" : 2, + "librariesLocation" : "libraries", + "objectsLocation" : "objects", + "gameVersion" : "1.8", + "features" : [ { + "name" : "CoolGrass", + "description" : "CoolGrass gives you a cool grass." + }, { + "name" : "SomeMinimap", + "description" : "SomeMinimap gives you a cool minimap.", + "recommendation" : "starred", + "selected" : true + } ], + "tasks" : [ { + "type" : "file", + "hash" : "8c35b619fb0169b7d7f087246293e26d3ec494a2", + "location" : "8c/35/8c35b619fb0169b7d7f087246293e26d3ec494a2", + "to" : "config/forge.cfg", + "size" : 2526 + }, { + "type" : "file", + "hash" : "e9c6c18a10a099f635858634afe794250fbb22b9", + "location" : "e9/c6/e9c6c18a10a099f635858634afe794250fbb22b9", + "to" : "config/worldedit/worldedit.properties", + "size" : 1031 + }, { + "type" : "file", + "hash" : "d64444560f4d81386d1a1230aebb03c1fb05624b", + "location" : "d6/44/d64444560f4d81386d1a1230aebb03c1fb05624b", + "to" : "mods/FlyingCars-2.5.jar", + "size" : 152 + }, { + "type" : "file", + "hash" : "d64444560f4d81386d1a1230aebb03c1fb05624b", + "location" : "d6/44/d64444560f4d81386d1a1230aebb03c1fb05624b", + "to" : "mods/IndustrialMachines-2.3.jar", + "size" : 152 + }, { + "type" : "file", + "hash" : "d64444560f4d81386d1a1230aebb03c1fb05624b", + "location" : "d6/44/d64444560f4d81386d1a1230aebb03c1fb05624b", + "to" : "mods/WorldEditor-1.2.jar", + "size" : 152 + }, { + "type" : "file", + "when" : { + "if" : "requireAny", + "features" : [ "CoolGrass" ] + }, + "hash" : "d64444560f4d81386d1a1230aebb03c1fb05624b", + "location" : "d6/44/d64444560f4d81386d1a1230aebb03c1fb05624b", + "to" : "mods/CoolGrass-1.0.jar", + "size" : 152 + }, { + "type" : "file", + "hash" : "d64444560f4d81386d1a1230aebb03c1fb05624b", + "location" : "d6/44/d64444560f4d81386d1a1230aebb03c1fb05624b", + "to" : "mods/RequiredClientLib-5.0.jar", + "size" : 152 + }, { + "type" : "file", + "when" : { + "if" : "requireAny", + "features" : [ "SomeMinimap" ] + }, + "hash" : "d64444560f4d81386d1a1230aebb03c1fb05624b", + "location" : "d6/44/d64444560f4d81386d1a1230aebb03c1fb05624b", + "to" : "mods/SomeMinimap-2.0.jar", + "size" : 152 + }, { + "type" : "file", + "hash" : "6c97a7f50fe0fa46ce5492f235b51eba8b69b191", + "location" : "6c/97/6c97a7f50fe0fa46ce5492f235b51eba8b69b191", + "to" : "options.txt", + "size" : 303, + "userFile" : true + }, { + "type" : "file", + "hash" : "d64444560f4d81386d1a1230aebb03c1fb05624b", + "location" : "d6/44/d64444560f4d81386d1a1230aebb03c1fb05624b", + "to" : "resourcepacks/SomeResourcePack.zip", + "size" : 152 + } ], + "versionManifest" : { + "id" : "1.8", + "time" : 1409646275000, + "releaseTime" : 1409646275000, + "assets" : "1.8", + "type" : "release", + "minecraftArguments" : "--username ${auth_player_name} --version ${version_name} --gameDir ${game_directory} --assetsDir ${assets_root} --assetIndex ${assets_index_name} --uuid ${auth_uuid} --accessToken ${auth_access_token} --userProperties ${user_properties} --userType ${user_type}", + "mainClass" : "net.minecraft.client.main.Main", + "minimumLauncherVersion" : 14, + "libraries" : [ { + "name" : "java3d:vecmath:1.5.2" + }, { + "name" : "net.sf.trove4j:trove4j:3.0.3" + }, { + "name" : "com.ibm.icu:icu4j-core-mojang:51.2" + }, { + "name" : "net.sf.jopt-simple:jopt-simple:4.6" + }, { + "name" : "com.paulscode:codecjorbis:20101023" + }, { + "name" : "com.paulscode:codecwav:20101023" + }, { + "name" : "com.paulscode:libraryjavasound:20101123" + }, { + "name" : "com.paulscode:librarylwjglopenal:20100824" + }, { + "name" : "com.paulscode:soundsystem:20120107" + }, { + "name" : "io.netty:netty-all:4.0.15.Final" + }, { + "name" : "com.google.guava:guava:17.0" + }, { + "name" : "org.apache.commons:commons-lang3:3.3.2" + }, { + "name" : "commons-io:commons-io:2.4" + }, { + "name" : "commons-codec:commons-codec:1.9" + }, { + "name" : "net.java.jinput:jinput:2.0.5" + }, { + "name" : "net.java.jutils:jutils:1.0.0" + }, { + "name" : "com.google.code.gson:gson:2.2.4" + }, { + "name" : "com.mojang:authlib:1.5.17" + }, { + "name" : "com.mojang:realms:1.6.1" + }, { + "name" : "org.apache.commons:commons-compress:1.8.1" + }, { + "name" : "org.apache.httpcomponents:httpclient:4.3.3" + }, { + "name" : "commons-logging:commons-logging:1.1.3" + }, { + "name" : "org.apache.httpcomponents:httpcore:4.3.2" + }, { + "name" : "org.apache.logging.log4j:log4j-api:2.0-beta9" + }, { + "name" : "org.apache.logging.log4j:log4j-core:2.0-beta9" + }, { + "name" : "org.lwjgl.lwjgl:lwjgl:2.9.1", + "rules" : [ { + "action" : "allow" + }, { + "action" : "disallow", + "os" : { + "name" : "osx" + } + } ] + }, { + "name" : "org.lwjgl.lwjgl:lwjgl_util:2.9.1", + "rules" : [ { + "action" : "allow" + }, { + "action" : "disallow", + "os" : { + "name" : "osx" + } + } ] + }, { + "name" : "org.lwjgl.lwjgl:lwjgl-platform:2.9.1", + "natives" : { + "linux" : "natives-linux", + "windows" : "natives-windows", + "osx" : "natives-osx" + }, + "extract" : { + "exclude" : [ "META-INF/" ] + }, + "rules" : [ { + "action" : "allow" + }, { + "action" : "disallow", + "os" : { + "name" : "osx" + } + } ] + }, { + "name" : "org.lwjgl.lwjgl:lwjgl:2.9.2-nightly-20140822", + "rules" : [ { + "action" : "allow", + "os" : { + "name" : "osx" + } + } ] + }, { + "name" : "org.lwjgl.lwjgl:lwjgl_util:2.9.2-nightly-20140822", + "rules" : [ { + "action" : "allow", + "os" : { + "name" : "osx" + } + } ] + }, { + "name" : "org.lwjgl.lwjgl:lwjgl-platform:2.9.2-nightly-20140822", + "natives" : { + "linux" : "natives-linux", + "windows" : "natives-windows", + "osx" : "natives-osx" + }, + "extract" : { + "exclude" : [ "META-INF/" ] + }, + "rules" : [ { + "action" : "allow", + "os" : { + "name" : "osx" + } + } ] + }, { + "name" : "net.java.jinput:jinput-platform:2.0.5", + "natives" : { + "linux" : "natives-linux", + "windows" : "natives-windows", + "osx" : "natives-osx" + }, + "extract" : { + "exclude" : [ "META-INF/" ] + } + }, { + "name" : "tv.twitch:twitch:6.5" + }, { + "name" : "tv.twitch:twitch-platform:6.5", + "natives" : { + "linux" : "natives-linux", + "windows" : "natives-windows-${arch}", + "osx" : "natives-osx" + }, + "extract" : { + "exclude" : [ "META-INF/" ] + }, + "rules" : [ { + "action" : "allow" + }, { + "action" : "disallow", + "os" : { + "name" : "linux" + } + } ] + }, { + "name" : "tv.twitch:twitch-external-platform:4.5", + "natives" : { + "windows" : "natives-windows-${arch}" + }, + "extract" : { + "exclude" : [ "META-INF/" ] + }, + "rules" : [ { + "action" : "allow", + "os" : { + "name" : "windows" + } + } ] + } ] + }, + "launch" : { + "flags" : [ "-Dfml.ignoreInvalidMinecraftCertificates=true" ] + } +} \ No newline at end of file diff --git a/sample-files/upload/monsterpack.json b/sample-files/upload/monsterpack.json new file mode 100644 index 0000000..e20d34e --- /dev/null +++ b/sample-files/upload/monsterpack.json @@ -0,0 +1,272 @@ +{ + "title" : "Monster Modpack", + "name" : "Monster Modpack", + "version" : "20100223", + "minimumVersion" : 2, + "librariesLocation" : "libraries", + "objectsLocation" : "objects", + "gameVersion" : "1.8", + "features" : [ { + "name" : "SomeMinimap", + "description" : "SomeMinimap gives you a cool minimap.", + "recommendation" : "starred", + "selected" : true + }, { + "name" : "CoolGrass", + "description" : "CoolGrass gives you a cool grass." + } ], + "tasks" : [ { + "type" : "file", + "hash" : "8c35b619fb0169b7d7f087246293e26d3ec494a2", + "location" : "8c/35/8c35b619fb0169b7d7f087246293e26d3ec494a2", + "to" : "config/forge.cfg", + "size" : 2526 + }, { + "type" : "file", + "hash" : "e9c6c18a10a099f635858634afe794250fbb22b9", + "location" : "e9/c6/e9c6c18a10a099f635858634afe794250fbb22b9", + "to" : "config/worldedit/worldedit.properties", + "size" : 1031 + }, { + "type" : "file", + "hash" : "d64444560f4d81386d1a1230aebb03c1fb05624b", + "location" : "d6/44/d64444560f4d81386d1a1230aebb03c1fb05624b", + "to" : "mods/FlyingCars-2.5.jar", + "size" : 152 + }, { + "type" : "file", + "hash" : "d64444560f4d81386d1a1230aebb03c1fb05624b", + "location" : "d6/44/d64444560f4d81386d1a1230aebb03c1fb05624b", + "to" : "mods/IndustrialMachines-2.3.jar", + "size" : 152 + }, { + "type" : "file", + "hash" : "d64444560f4d81386d1a1230aebb03c1fb05624b", + "location" : "d6/44/d64444560f4d81386d1a1230aebb03c1fb05624b", + "to" : "mods/SomeHeavyMod-1.4.jar", + "size" : 152 + }, { + "type" : "file", + "hash" : "d64444560f4d81386d1a1230aebb03c1fb05624b", + "location" : "d6/44/d64444560f4d81386d1a1230aebb03c1fb05624b", + "to" : "mods/WorldEditor-1.2.jar", + "size" : 152 + }, { + "type" : "file", + "when" : { + "if" : "requireAny", + "features" : [ "CoolGrass" ] + }, + "hash" : "d64444560f4d81386d1a1230aebb03c1fb05624b", + "location" : "d6/44/d64444560f4d81386d1a1230aebb03c1fb05624b", + "to" : "mods/CoolGrass-1.0.jar", + "size" : 152 + }, { + "type" : "file", + "hash" : "d64444560f4d81386d1a1230aebb03c1fb05624b", + "location" : "d6/44/d64444560f4d81386d1a1230aebb03c1fb05624b", + "to" : "mods/RequiredClientLib-5.0.jar", + "size" : 152 + }, { + "type" : "file", + "when" : { + "if" : "requireAny", + "features" : [ "SomeMinimap" ] + }, + "hash" : "d64444560f4d81386d1a1230aebb03c1fb05624b", + "location" : "d6/44/d64444560f4d81386d1a1230aebb03c1fb05624b", + "to" : "mods/SomeMinimap-2.0.jar", + "size" : 152 + }, { + "type" : "file", + "hash" : "6c97a7f50fe0fa46ce5492f235b51eba8b69b191", + "location" : "6c/97/6c97a7f50fe0fa46ce5492f235b51eba8b69b191", + "to" : "options.txt", + "size" : 303, + "userFile" : true + }, { + "type" : "file", + "hash" : "d64444560f4d81386d1a1230aebb03c1fb05624b", + "location" : "d6/44/d64444560f4d81386d1a1230aebb03c1fb05624b", + "to" : "resourcepacks/SomeResourcePack.zip", + "size" : 152 + } ], + "versionManifest" : { + "id" : "1.8", + "time" : 1409646275000, + "releaseTime" : 1409646275000, + "assets" : "1.8", + "type" : "release", + "minecraftArguments" : "--username ${auth_player_name} --version ${version_name} --gameDir ${game_directory} --assetsDir ${assets_root} --assetIndex ${assets_index_name} --uuid ${auth_uuid} --accessToken ${auth_access_token} --userProperties ${user_properties} --userType ${user_type}", + "mainClass" : "net.minecraft.client.main.Main", + "minimumLauncherVersion" : 14, + "libraries" : [ { + "name" : "java3d:vecmath:1.5.2" + }, { + "name" : "net.sf.trove4j:trove4j:3.0.3" + }, { + "name" : "com.ibm.icu:icu4j-core-mojang:51.2" + }, { + "name" : "net.sf.jopt-simple:jopt-simple:4.6" + }, { + "name" : "com.paulscode:codecjorbis:20101023" + }, { + "name" : "com.paulscode:codecwav:20101023" + }, { + "name" : "com.paulscode:libraryjavasound:20101123" + }, { + "name" : "com.paulscode:librarylwjglopenal:20100824" + }, { + "name" : "com.paulscode:soundsystem:20120107" + }, { + "name" : "io.netty:netty-all:4.0.15.Final" + }, { + "name" : "com.google.guava:guava:17.0" + }, { + "name" : "org.apache.commons:commons-lang3:3.3.2" + }, { + "name" : "commons-io:commons-io:2.4" + }, { + "name" : "commons-codec:commons-codec:1.9" + }, { + "name" : "net.java.jinput:jinput:2.0.5" + }, { + "name" : "net.java.jutils:jutils:1.0.0" + }, { + "name" : "com.google.code.gson:gson:2.2.4" + }, { + "name" : "com.mojang:authlib:1.5.17" + }, { + "name" : "com.mojang:realms:1.6.1" + }, { + "name" : "org.apache.commons:commons-compress:1.8.1" + }, { + "name" : "org.apache.httpcomponents:httpclient:4.3.3" + }, { + "name" : "commons-logging:commons-logging:1.1.3" + }, { + "name" : "org.apache.httpcomponents:httpcore:4.3.2" + }, { + "name" : "org.apache.logging.log4j:log4j-api:2.0-beta9" + }, { + "name" : "org.apache.logging.log4j:log4j-core:2.0-beta9" + }, { + "name" : "org.lwjgl.lwjgl:lwjgl:2.9.1", + "rules" : [ { + "action" : "allow" + }, { + "action" : "disallow", + "os" : { + "name" : "osx" + } + } ] + }, { + "name" : "org.lwjgl.lwjgl:lwjgl_util:2.9.1", + "rules" : [ { + "action" : "allow" + }, { + "action" : "disallow", + "os" : { + "name" : "osx" + } + } ] + }, { + "name" : "org.lwjgl.lwjgl:lwjgl-platform:2.9.1", + "natives" : { + "linux" : "natives-linux", + "windows" : "natives-windows", + "osx" : "natives-osx" + }, + "extract" : { + "exclude" : [ "META-INF/" ] + }, + "rules" : [ { + "action" : "allow" + }, { + "action" : "disallow", + "os" : { + "name" : "osx" + } + } ] + }, { + "name" : "org.lwjgl.lwjgl:lwjgl:2.9.2-nightly-20140822", + "rules" : [ { + "action" : "allow", + "os" : { + "name" : "osx" + } + } ] + }, { + "name" : "org.lwjgl.lwjgl:lwjgl_util:2.9.2-nightly-20140822", + "rules" : [ { + "action" : "allow", + "os" : { + "name" : "osx" + } + } ] + }, { + "name" : "org.lwjgl.lwjgl:lwjgl-platform:2.9.2-nightly-20140822", + "natives" : { + "linux" : "natives-linux", + "windows" : "natives-windows", + "osx" : "natives-osx" + }, + "extract" : { + "exclude" : [ "META-INF/" ] + }, + "rules" : [ { + "action" : "allow", + "os" : { + "name" : "osx" + } + } ] + }, { + "name" : "net.java.jinput:jinput-platform:2.0.5", + "natives" : { + "linux" : "natives-linux", + "windows" : "natives-windows", + "osx" : "natives-osx" + }, + "extract" : { + "exclude" : [ "META-INF/" ] + } + }, { + "name" : "tv.twitch:twitch:6.5" + }, { + "name" : "tv.twitch:twitch-platform:6.5", + "natives" : { + "linux" : "natives-linux", + "windows" : "natives-windows-${arch}", + "osx" : "natives-osx" + }, + "extract" : { + "exclude" : [ "META-INF/" ] + }, + "rules" : [ { + "action" : "allow" + }, { + "action" : "disallow", + "os" : { + "name" : "linux" + } + } ] + }, { + "name" : "tv.twitch:twitch-external-platform:4.5", + "natives" : { + "windows" : "natives-windows-${arch}" + }, + "extract" : { + "exclude" : [ "META-INF/" ] + }, + "rules" : [ { + "action" : "allow", + "os" : { + "name" : "windows" + } + } ] + } ] + }, + "launch" : { + "flags" : [ "-Dfml.ignoreInvalidMinecraftCertificates=true" ] + } +} \ No newline at end of file diff --git a/sample-files/upload/news.html b/sample-files/upload/news.html new file mode 100644 index 0000000..235b201 --- /dev/null +++ b/sample-files/upload/news.html @@ -0,0 +1,20 @@ + + + +News Page + + + + +

News

+

Recently, two new modpacks were added.

+ + \ No newline at end of file diff --git a/sample-files/upload/objects/6c/97/6c97a7f50fe0fa46ce5492f235b51eba8b69b191 b/sample-files/upload/objects/6c/97/6c97a7f50fe0fa46ce5492f235b51eba8b69b191 new file mode 100644 index 0000000..6308e4c --- /dev/null +++ b/sample-files/upload/objects/6c/97/6c97a7f50fe0fa46ce5492f235b51eba8b69b191 @@ -0,0 +1,11 @@ +soundCategory_master:1.0 +soundCategory_music:0.0 +soundCategory_record:1.0 +soundCategory_weather:0.1056338 +soundCategory_block:1.0 +soundCategory_hostile:1.0 +soundCategory_neutral:1.0 +soundCategory_player:1.0 +soundCategory_ambient:1.0 +resourcePacks:["Conquest (smaller).zip"] +enableVsync:false diff --git a/sample-files/upload/objects/8c/35/8c35b619fb0169b7d7f087246293e26d3ec494a2 b/sample-files/upload/objects/8c/35/8c35b619fb0169b7d7f087246293e26d3ec494a2 new file mode 100644 index 0000000..72eab30 --- /dev/null +++ b/sample-files/upload/objects/8c/35/8c35b619fb0169b7d7f087246293e26d3ec494a2 @@ -0,0 +1,60 @@ +# Configuration file + +general { + # Set to true to disable Forge's version check mechanics. Forge queries a small json file on our server for version information. For more details see the ForgeVersion class in our github. + B:disableVersionCheck=false + + # Controls the number threshold at which Packet51 is preferred over Packet52, default and minimum 64, maximum 1024 + I:clumpingThreshold=64 + + # Set to true to enable the post initialization sorting of crafting recipes using Forge's sorter. May cause desyncing on conflicting recipies. MUST RESTART MINECRAFT IF CHANGED FROM THE CONFIG GUI. + B:sortRecipies=true + + # Set this to true to force a crash if more than one block attempts to link back to the same Fluid. Enabled by default. + B:forceDuplicateFluidBlockCrash=true + + # Set this to true to remove any Entity that throws an error in its update method instead of closing the server and reporting a crash log. BE WARNED THIS COULD SCREW UP EVERYTHING USE SPARINGLY WE ARE NOT RESPONSIBLE FOR DAMAGES. + B:removeErroringEntities=false + + # Set this to true to remove any TileEntity that throws an error in its update method instead of closing the server and reporting a crash log. BE WARNED THIS COULD SCREW UP EVERYTHING USE SPARINGLY WE ARE NOT RESPONSIBLE FOR DAMAGES. + B:removeErroringTileEntities=false + + # Set this to true to check the entire entity's collision bounding box for ladders instead of just the block they are in. Causes noticable differences in mechanics so default is vanilla behavior. Default: false + B:fullBoundingBoxLadders=false + + # Control the range of sky blending for colored skies in biomes. + I:biomeSkyBlendRange < + 2 + 4 + 6 + 8 + 10 + 12 + 14 + 16 + 18 + 20 + 22 + 24 + 26 + 28 + 30 + 32 + 34 + > + + # Base zombie summoning spawn chance. Allows changing the bonus zombie summoning mechanic. + D:zombieBaseSummonChance=0.1 + + # Chance that a zombie (or subclass) is a baby. Allows changing the zombie spawning mechanic. + D:zombieBabyChance=0.05 + + # Set to false to attempt to allocate 8 stencil bits when starting the GL display context. + B:enableStencilBits=true + + # The spawn fuzz when a player respawns in the world, this is controlable by WorldType, this config option is for the default overworld. + I:defaultSpawnFuzz=20 + B:enableGlobalConfig=false +} + + diff --git a/sample-files/upload/objects/d6/44/d64444560f4d81386d1a1230aebb03c1fb05624b b/sample-files/upload/objects/d6/44/d64444560f4d81386d1a1230aebb03c1fb05624b new file mode 100644 index 0000000..3b75a9b Binary files /dev/null and b/sample-files/upload/objects/d6/44/d64444560f4d81386d1a1230aebb03c1fb05624b differ diff --git a/sample-files/upload/objects/e9/c6/e9c6c18a10a099f635858634afe794250fbb22b9 b/sample-files/upload/objects/e9/c6/e9c6c18a10a099f635858634afe794250fbb22b9 new file mode 100644 index 0000000..fb8afa6 --- /dev/null +++ b/sample-files/upload/objects/e9/c6/e9c6c18a10a099f635858634afe794250fbb22b9 @@ -0,0 +1,38 @@ +#Don't put comments; they get removed +#Fri Nov 14 20:36:13 PST 2014 +super-pickaxe-many-drop-items=true +super-pickaxe-drop-items=true +craftscript-dir=craftscripts +max-brush-radius=10 +scripting-timeout=3000 +no-double-slash=false +butcher-max-radius=-1 +profile=false +max-radius=-1 +default-max-polyhedron-points=-1 +use-inventory-creative-override=false +use-inventory=false +shell-save-type= +allow-extra-data-values=false +wand-item=271 +history-size=15 +register-help=true +butcher-default-radius=-1 +schematic-save-dir=schematics +nav-wand-item=345 +max-polygon-points=20 +default-max-polygon-points=-1 +snapshots-dir= +nav-use-glass=true +use-inventory-override=false +cheat-mode=false +max-changed-blocks=-1 +default-max-changed-blocks=-1 +max-polyhedron-points=20 +allow-symbolic-links=false +use-in-creative=false +disallowed-blocks=6,26,27,28,31,32,34,36,37,38,39,40,46,50,51,55,59,66,69,75,76,93,94,77,81,83,7,14,15,16,56 +log-commands=false +log-file=worldedit.log +max-super-pickaxe-size=5 +nav-wand-distance=50 diff --git a/sample-files/upload/packages.json b/sample-files/upload/packages.json new file mode 100644 index 0000000..9fe041f --- /dev/null +++ b/sample-files/upload/packages.json @@ -0,0 +1,19 @@ +{ + "minimumVersion": 1, + "packages": [ + { + "name": "Light Modpack", + "title": "Light Modpack", + "version": "20100223", + "location": "lightpack.json", + "priority": 1 + }, + { + "name": "Monster Modpack", + "title": "Monster Modpack", + "version": "20100223", + "location": "monsterpack.json", + "priority": 1 + } + ] +} \ No newline at end of file diff --git a/sample-files/upload/packages.php b/sample-files/upload/packages.php new file mode 100644 index 0000000..77e3d9e --- /dev/null +++ b/sample-files/upload/packages.php @@ -0,0 +1,29 @@ + 1, + 'packages' => [], +]; + +foreach ($files as $file) { + $data = json_decode(file_get_contents($file)); + if (isset($data->name) && isset($data->version)) { + $document['packages'][] = [ + 'name' => $data->name, + 'title' => isset($data->title) ? $data->title : $data->name, + 'version' => $data->version, + 'location' => basename($file), + 'priority' => 1, + ]; + } +} + +header("Content-Type: text/plain"); +echo json_encode($document, JSON_PRETTY_PRINT); \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..18c4ab3 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,3 @@ +rootProject.name = 'launcher-parent' + +include 'launcher', 'launcher-fancy', 'launcher-builder', 'launcher-bootstrap', 'creator-tools'