diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7e2d1f7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# Eclipse stuff +/.classpath +/.project +/.settings + +# netbeans +/nbproject + +# we use maven! +/build.xml + +# maven +/target + +# vim +.*.sw[a-p] + +# various other potential build files +/build +/bin +/dist +/manifest.mf + +# Mac filesystem dust +/.DS_Store + +# intellij +*.iml +*.ipr +*.iws +.idea/ + +/dependency-reduced-pom.xml +/*.pack \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..c2f5a43 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,259 @@ +The following license applies files of the project, other than graphics files +and non-XML resources included within src/main/resources: + + 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. + + +JNBT License +------------ + +Copyright (c) 2010 Graham Edgecombe +All rights reserved. + +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. + + * Neither the name of the JNBT team nor the names of its + contributors may 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. + + +JChronic License +---------------- + +The MIT License + +Copyright (c) 2009 Mike Schrag, Sam Tingleff + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +------------------------------------------------------------------------------- + +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 new file mode 100644 index 0000000..1981b2c --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +SKCraft Launcher +================ + +License +------- + +The launcher is licensed under the GNU General Public License, version 3. + +Contributions by third parties must be dual licensed under the two licenses +described within LICENSE.txt (GNU General Public License, version 3, and the +3-clause BSD license). diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..9cacad6 --- /dev/null +++ b/pom.xml @@ -0,0 +1,120 @@ + + + + 4.0.0 + + com.skcraft + launcher + 4.0.0-SNAPSHOT + + + + com.fasterxml.jackson.core + jackson-databind + 2.3.0 + + + commons-lang + commons-lang + 2.6 + + + commons-io + commons-io + 1.2 + + + org.projectlombok + lombok + 1.12.2 + + + com.google.guava + guava + 15.0 + + + com.beust + jcommander + 1.32 + + + + + SKCraftLauncher + + + + src/main/resources + true + + **/*.properties + + + + src/main/resources + false + + **/*.properties + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.3.2 + + 1.6 + 1.6 + + + + org.apache.maven.plugins + maven-jar-plugin + 2.3.1 + + + false + + com.skcraft.launcher.Launcher + + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 2.0 + + + package + + shade + + + + + org.projectlombok:lombok + + + + + + + false + + + + + + \ No newline at end of file diff --git a/src/main/java/com/skcraft/concurrency/ObservableFuture.java b/src/main/java/com/skcraft/concurrency/ObservableFuture.java new file mode 100644 index 0000000..c4fee92 --- /dev/null +++ b/src/main/java/com/skcraft/concurrency/ObservableFuture.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.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 double getProgress() { + return observable.getProgress(); + } + + @Override + public String toString() { + return observable.toString(); + } + +} diff --git a/src/main/java/com/skcraft/concurrency/ProgressObservable.java b/src/main/java/com/skcraft/concurrency/ProgressObservable.java new file mode 100644 index 0000000..590a56a --- /dev/null +++ b/src/main/java/com/skcraft/concurrency/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.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(); + +} diff --git a/src/main/java/com/skcraft/launcher/AssetsRoot.java b/src/main/java/com/skcraft/launcher/AssetsRoot.java new file mode 100644 index 0000000..5bd2da9 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/AssetsRoot.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; + +import com.google.common.io.Files; +import com.skcraft.launcher.model.minecraft.Asset; +import com.skcraft.launcher.model.minecraft.AssetsIndex; +import com.skcraft.launcher.persistence.Persistence; +import lombok.Getter; +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 AssetsRoot { + + @Getter + private final File dir; + + public AssetsRoot(File dir) { + this.dir = dir; + } + + public File getIndexPath(String indexId) { + return new File(dir, "indexes/" + indexId + ".json"); + } + + public File getObjectPath(Asset asset) { + String hash = asset.getHash(); + return new File(dir, "objects/" + hash.substring(0, 2) + "/" + hash); + } + + public File buildAssetTree(String indexId) throws IOException { + log.info("Building asset virtual tree for '" + indexId + "'"); + + AssetsIndex index = Persistence.read(getIndexPath(indexId), AssetsIndex.class); + File treeDir = new File(dir, "virtual/" + indexId); + treeDir.mkdirs(); + + for (Map.Entry entry : index.getObjects().entrySet()) { + File objectPath = getObjectPath(entry.getValue()); + File virtualPath = new File(treeDir, entry.getKey()); + virtualPath.getParentFile().mkdirs(); + if (!virtualPath.exists()) { + log.log(Level.INFO, "Copying {0} to {1}...", new Object[] { + objectPath.getAbsolutePath(), virtualPath.getAbsolutePath()}); + Files.copy(objectPath, virtualPath); + } + } + + return treeDir; + } + +} diff --git a/src/main/java/com/skcraft/launcher/Configuration.java b/src/main/java/com/skcraft/launcher/Configuration.java new file mode 100644 index 0000000..0d08d66 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/Configuration.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; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class Configuration { + + private boolean offlineEnabled = false; + private String jvmPath; + private String jvmArgs; + private int minMemory = 1024; + private int maxMemory = 1024; + private int permGen = 128; + private int windowWidth = 854; + private int widowHeight = 480; + private boolean proxyEnabled = false; + private String proxyHost = "localhost"; + private int proxyPort = 8080; + private String proxyUsername; + private String proxyPassword; + private String gameKey; + + @Override + public boolean equals(Object o) { + return super.equals(o); + } + + @Override + public int hashCode() { + return super.hashCode(); + } + +} diff --git a/src/main/java/com/skcraft/launcher/Instance.java b/src/main/java/com/skcraft/launcher/Instance.java new file mode 100644 index 0000000..6fc5af3 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/Instance.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; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; + +import java.io.File; +import java.net.URL; +import java.util.Date; + +@Data +public class Instance implements Comparable { + + private String title; + private String name; + private String version; + private boolean updatePending; + private boolean installed; + private Date lastAccessed; + + @JsonIgnore private File dir; + @JsonIgnore private URL manifestURL; + @JsonIgnore private int priority; + @JsonIgnore private boolean selected; + @JsonIgnore private boolean local; + + public String getTitle() { + return title != null ? title : name; + } + + @JsonIgnore + public File getContentDir() { + return new File(dir, "minecraft"); + } + + @JsonIgnore + public File getManifestPath() { + return new File(dir, "manifest.json"); + } + + @JsonIgnore + public File getVersionManifestPath() { + return new File(dir, "version.json"); + } + + @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/src/main/java/com/skcraft/launcher/InstanceList.java b/src/main/java/com/skcraft/launcher/InstanceList.java new file mode 100644 index 0000000..b6326a4 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/InstanceList.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.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 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; +import static com.skcraft.launcher.util.SharedLocale._; + +@Log +public class InstanceList { + + private static int PARSER_VERSION = 1; + private final Launcher launcher; + @Getter private final List instances = new ArrayList(); + + public InstanceList(@NonNull Launcher launcher) { + this.launcher = launcher; + } + + public synchronized Instance get(int index) { + return instances.get(index); + } + + public synchronized int size() { + return instances.size(); + } + + public Enumerator createEnumerator() { + return new Enumerator(); + } + + public synchronized List getSelected() { + List selected = new ArrayList(); + for (Instance instance : instances) { + if (instance.isSelected()) { + selected.add(instance); + } + } + + return selected; + } + + public synchronized void sort() { + Collections.sort(instances); + } + + public final class Enumerator implements Callable, ProgressObservable { + private Enumerator() { + } + + @Override + public InstanceList call() throws Exception { + log.info("Enumerating instance list..."); + + 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()); + } + } + + try { + URL packagesURL = launcher.getPackagesURL(); + + PackageList packages = HttpRequest + .get(packagesURL) + .execute() + .expectResponseCode(200) + .returnContent() + .asJson(PackageList.class); + + if (packages.getMinimumVersion() > PARSER_VERSION) { + throw new LauncherException("Update required", _("errors.updateRequiredError")); + } + + for (ManifestInfo manifest : packages.getPackages()) { + boolean foundLocal = false; + + for (Instance instance : local) { + if (instance.getName().equals(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; + } + } +} diff --git a/src/main/java/com/skcraft/launcher/Launcher.java b/src/main/java/com/skcraft/launcher/Launcher.java new file mode 100644 index 0000000..2b1d303 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/Launcher.java @@ -0,0 +1,320 @@ +/* + * 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.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import com.skcraft.launcher.auth.AccountList; +import com.skcraft.launcher.auth.LoginService; +import com.skcraft.launcher.auth.YggdrasilLoginService; +import com.skcraft.launcher.dialog.LauncherFrame; +import com.skcraft.launcher.model.minecraft.VersionManifest; +import com.skcraft.launcher.persistence.Persistence; +import com.skcraft.launcher.swing.SwingHelper; +import com.skcraft.launcher.util.HttpRequest; +import com.skcraft.launcher.util.SharedLocale; +import com.skcraft.launcher.util.SimpleLogFormatter; +import lombok.Getter; +import lombok.NonNull; +import lombok.extern.java.Log; +import org.apache.commons.io.FileUtils; + +import javax.swing.*; +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +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; + +/** + * The main entry point for the launcher. + */ +@Log +public final class Launcher { + + @Getter + private final ListeningExecutorService executor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool()); + @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; + + /** + * 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 { + SharedLocale.loadBundle("com.skcraft.launcher.lang.Launcher", Locale.getDefault()); + + this.baseDir = baseDir; + this.properties = LauncherUtils.loadProperties(Launcher.class, "launcher.properties"); + this.instances = new InstanceList(this); + this.assets = new AssetsRoot(new File(baseDir, "assets")); + this.config = Persistence.load(new File(baseDir, "config.json"), Configuration.class); + this.accounts = Persistence.load(new File(baseDir, "accounts.dat"), AccountList.class); + + if (accounts.getSize() > 0) { + accounts.setSelectedItem(accounts.getElementAt(0)); + } + + executor.submit(new Runnable() { + @Override + public void run() { + cleanupExtractDir(); + } + }); + } + + /** + * 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 a login service. + * + * @return a login service + */ + public LoginService getLoginService() { + return new YggdrasilLoginService(HttpRequest.url(getProperties().getProperty("yggdrasilAuthUrl"))); + } + + /** + * 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"); + } + + /** + * 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); + } + } + + /** + * Bootstrap. + * + * @param args args + */ + public static void main(String[] args) { + SimpleLogFormatter.configureGlobalLogger(); + + LauncherArguments options = new LauncherArguments(); + try { + new JCommander(options, args); + } catch (ParameterException e) { + System.err.print(e.getMessage()); + System.exit(1); + return; + } + + Integer bsVersion = options.getBootstrapVersion(); + log.info(bsVersion != null ? "Bootstrap version " + bsVersion + " detected" : "Not bootstrapped"); + + File dir = options.getDir(); + if (dir != null) { + log.info("Using given base directory " + dir.getAbsolutePath()); + } else { + dir = new File("."); + log.info("Using current directory " + dir.getAbsolutePath()); + } + + final File baseDir = dir; + + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + UIManager.getDefaults().put("SplitPane.border", BorderFactory.createEmptyBorder()); + Launcher launcher = new Launcher(baseDir); + new LauncherFrame(launcher).setVisible(true); + } 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/src/main/java/com/skcraft/launcher/LauncherArguments.java b/src/main/java/com/skcraft/launcher/LauncherArguments.java new file mode 100644 index 0000000..967850c --- /dev/null +++ b/src/main/java/com/skcraft/launcher/LauncherArguments.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; + +import com.beust.jcommander.Parameter; +import lombok.Data; + +import java.io.File; + +@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/src/main/java/com/skcraft/launcher/LauncherException.java b/src/main/java/com/skcraft/launcher/LauncherException.java new file mode 100644 index 0000000..dea9d41 --- /dev/null +++ b/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/src/main/java/com/skcraft/launcher/LauncherUtils.java b/src/main/java/com/skcraft/launcher/LauncherUtils.java new file mode 100644 index 0000000..3a18751 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/LauncherUtils.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; + +import com.google.common.io.Closer; + +import java.io.*; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Properties; +import java.util.regex.Pattern; + +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) throws IOException { + Closer closer = Closer.create(); + Properties prop = new Properties(); + try { + InputStream in = closer.register(clazz.getResourceAsStream(name)); + prop.load(in); + } 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) throws IOException, InterruptedException { + checkInterrupted(); + + 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); + } + + file.delete(); + } else { + if (!file.exists()) { + throw new FileNotFoundException("Does not exist: " + file); + } + + file.delete(); + } + } + +} diff --git a/src/main/java/com/skcraft/launcher/auth/Account.java b/src/main/java/com/skcraft/launcher/auth/Account.java new file mode 100644 index 0000000..e99bade --- /dev/null +++ b/src/main/java/com/skcraft/launcher/auth/Account.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.auth; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.google.common.base.Strings; +import lombok.Data; +import lombok.NonNull; + +import java.util.Date; + +/** + * A user account that can be stored and loaded. + */ +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class Account implements Comparable { + + private String id; + private String password; + private Date lastUsed; + + /** + * Create a new account. + */ + public Account() { + } + + /** + * Create a new account with the given ID. + * + * @param id the ID + */ + public Account(String id) { + setId(id); + } + + /** + * Set the account's stored password, that may be stored to disk. + * + * @param password the password + */ + public void setPassword(String password) { + if (password != null && password.isEmpty()) { + password = null; + } + this.password = Strings.emptyToNull(password); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Account account = (Account) o; + + if (!id.equalsIgnoreCase(account.id)) return false; + + return true; + } + + @Override + public int hashCode() { + return id.toLowerCase().hashCode(); + } + + @Override + public int compareTo(@NonNull Account o) { + Date otherDate = o.getLastUsed(); + + if (otherDate == null && lastUsed == null) { + return 0; + } else if (otherDate == null) { + return -1; + } else if (lastUsed == null) { + return 1; + } else { + return -lastUsed.compareTo(otherDate); + } + } + + @Override + public String toString() { + return getId(); + } + +} diff --git a/src/main/java/com/skcraft/launcher/auth/AccountList.java b/src/main/java/com/skcraft/launcher/auth/AccountList.java new file mode 100644 index 0000000..ddf9cb0 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/auth/AccountList.java @@ -0,0 +1,134 @@ +/* + * 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.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.skcraft.launcher.persistence.Scrambled; +import lombok.Getter; +import lombok.NonNull; + +import javax.swing.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +/** + * A list of accounts that can be stored to disk. + */ +@Scrambled("ACCOUNT_LIST") +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonAutoDetect( + getterVisibility = JsonAutoDetect.Visibility.NONE, + setterVisibility = JsonAutoDetect.Visibility.NONE, + fieldVisibility = JsonAutoDetect.Visibility.NONE) +public class AccountList extends AbstractListModel implements ComboBoxModel { + + @JsonProperty + @Getter + private List accounts = new ArrayList(); + private transient Account selected; + + /** + * Add a new account. + * + *

If there is already an existing account with the same ID, then the + * new account will not be added.

+ * + * @param account the account to add + */ + public synchronized void add(@NonNull Account account) { + if (!accounts.contains(account)) { + accounts.add(account); + Collections.sort(accounts); + fireContentsChanged(this, 0, accounts.size()); + } + } + + /** + * Remove an account. + * + * @param account the account + */ + public synchronized void remove(@NonNull Account account) { + Iterator it = accounts.iterator(); + while (it.hasNext()) { + Account other = it.next(); + if (other.equals(account)) { + it.remove(); + fireContentsChanged(this, 0, accounts.size() + 1); + break; + } + } + } + + /** + * Set the list of accounts. + * + * @param accounts the list of accounts + */ + public synchronized void setAccounts(@NonNull List accounts) { + this.accounts = accounts; + Collections.sort(accounts); + } + + @Override + @JsonIgnore + public synchronized int getSize() { + return accounts.size(); + } + + @Override + public synchronized Account getElementAt(int index) { + try { + return accounts.get(index); + } catch (IndexOutOfBoundsException e) { + return null; + } + } + + @Override + public void setSelectedItem(Object item) { + if (item == null) { + selected = null; + return; + } + + if (item instanceof Account) { + this.selected = (Account) item; + } else { + String id = String.valueOf(item).trim(); + Account account = new Account(id); + for (Account test : accounts) { + if (test.equals(account)) { + account = test; + break; + } + } + selected = account; + } + + if (selected.getId() == null || selected.getId().isEmpty()) { + selected = null; + } + } + + @Override + @JsonIgnore + public Account getSelectedItem() { + return selected; + } + + public synchronized void forgetPasswords() { + for (Account account : accounts) { + account.setPassword(null); + } + } +} diff --git a/src/main/java/com/skcraft/launcher/auth/AuthenticationException.java b/src/main/java/com/skcraft/launcher/auth/AuthenticationException.java new file mode 100644 index 0000000..4dc3f4f --- /dev/null +++ b/src/main/java/com/skcraft/launcher/auth/AuthenticationException.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.auth; + +import com.skcraft.launcher.LauncherException; + +/** + * Thrown on authentication error. + */ +public class AuthenticationException extends LauncherException { + + public AuthenticationException(String message, String localizedMessage) { + super(message, localizedMessage); + } + + public AuthenticationException(Throwable cause, String localizedMessage) { + super(cause, localizedMessage); + } +} diff --git a/src/main/java/com/skcraft/launcher/auth/LoginService.java b/src/main/java/com/skcraft/launcher/auth/LoginService.java new file mode 100644 index 0000000..60419ed --- /dev/null +++ b/src/main/java/com/skcraft/launcher/auth/LoginService.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.auth; + +import java.io.IOException; +import java.util.List; + +/** + * A service for creating authenticated sessions. + */ +public interface LoginService { + + /** + * Attempt to login with the given details. + * + * @param agent the game to authenticate for, such as "Minecraft" + * @param id the login ID + * @param password the password + * @return a list of authenticated sessions, which corresponds to identities + * @throws IOException thrown on I/O error + * @throws InterruptedException thrown if interrupted + * @throws AuthenticationException thrown on an authentication error + */ + List login(String agent, String id, String password) + throws IOException, InterruptedException, AuthenticationException; + +} diff --git a/src/main/java/com/skcraft/launcher/auth/OfflineSession.java b/src/main/java/com/skcraft/launcher/auth/OfflineSession.java new file mode 100644 index 0000000..04223df --- /dev/null +++ b/src/main/java/com/skcraft/launcher/auth/OfflineSession.java @@ -0,0 +1,65 @@ +/* + * 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 getClientToken() { + return "0"; + } + + @Override + public String getAccessToken() { + return "0"; + } + + @Override + public Map getUserProperties() { + return dummyProperties; + } + + @Override + public String getSessionToken() { + return "-"; + } + + @Override + public UserType getUserType() { + return UserType.LEGACY; + } + +} diff --git a/src/main/java/com/skcraft/launcher/auth/Session.java b/src/main/java/com/skcraft/launcher/auth/Session.java new file mode 100644 index 0000000..5aa5adf --- /dev/null +++ b/src/main/java/com/skcraft/launcher/auth/Session.java @@ -0,0 +1,67 @@ +/* + * 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 client token. + * + * @return client token + */ + String getClientToken(); + + /** + * 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(); + +} diff --git a/src/main/java/com/skcraft/launcher/auth/UserType.java b/src/main/java/com/skcraft/launcher/auth/UserType.java new file mode 100644 index 0000000..7dfeff6 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/auth/UserType.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.auth; + +/** + * Represents the type of user for the account. + */ +public enum UserType { + + /** + * Legacy accounts login with an account username. + */ + LEGACY, + /** + * Mojang accounts login with an email address. + */ + MOJANG; + + /** + * Return a lowercase version of the enum type. + * + * @return the lowercase name + */ + public String getName() { + return name().toLowerCase(); + } + + +} diff --git a/src/main/java/com/skcraft/launcher/auth/YggdrasilLoginService.java b/src/main/java/com/skcraft/launcher/auth/YggdrasilLoginService.java new file mode 100644 index 0000000..ca97a47 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/auth/YggdrasilLoginService.java @@ -0,0 +1,123 @@ +/* + * 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.NonNull; +import lombok.ToString; + +import java.io.IOException; +import java.net.URL; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * Creates authenticated sessions using the Mojang Yggdrasil login protocol. + */ +public class YggdrasilLoginService implements LoginService { + + private final URL authUrl; + + /** + * Create a new login service with the given authentication URL. + * + * @param authUrl the authentication URL + */ + public YggdrasilLoginService(@NonNull URL authUrl) { + this.authUrl = authUrl; + } + + @Override + public List login(String agent, String id, String password) + throws IOException, InterruptedException, AuthenticationException { + Object payload = new AuthenticatePayload(new Agent(agent), id, password); + + HttpRequest request = HttpRequest + .post(authUrl) + .bodyJson(payload) + .execute(); + + if (request.getResponseCode() != 200) { + ErrorResponse error = request.returnContent().asJson(ErrorResponse.class); + throw new AuthenticationException(error.getErrorMessage(), error.getErrorMessage()); + } else { + AuthenticateResponse response = request.returnContent().asJson(AuthenticateResponse.class); + return response.getAvailableProfiles(); + } + } + + @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; + } + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + private static class AuthenticateResponse { + private String accessToken; + private String clientToken; + @JsonManagedReference private List availableProfiles; + 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; + @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 getClientToken() { + return response.getClientToken(); + } + + @Override + @JsonIgnore + public String getAccessToken() { + return response.getAccessToken(); + } + + @Override + @JsonIgnore + public UserType getUserType() { + return legacy ? UserType.LEGACY : UserType.MOJANG; + } + } + +} diff --git a/src/main/java/com/skcraft/launcher/builder/ClientFileCollector.java b/src/main/java/com/skcraft/launcher/builder/ClientFileCollector.java new file mode 100644 index 0000000..f2efdd2 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/builder/ClientFileCollector.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.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 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 File destDir; + private HashFunction hf = Hashing.sha1(); + + /** + * Create a new collector. + * + * @param manifest the manifest + * @param destDir the destination directory to copy the hashed objects + */ + public ClientFileCollector(@NonNull Manifest manifest, @NonNull File destDir) { + this.manifest = manifest; + this.destDir = destDir; + } + + @Override + public DirectoryBehavior getBehavior(@NonNull String name) { + if (name.equals("_SERVER")) { + return DirectoryBehavior.SKIP; + } else if (name.equals("_CLIENT")) { + return DirectoryBehavior.IGNORE; + } else { + return DirectoryBehavior.CONTINUE; + } + } + + @Override + protected void onFile(File file, String relPath) throws IOException { + FileInstall task = new FileInstall(); + String hash = Files.hash(file, hf).toString(); + String hashedPath = hash.substring(0, 2) + "/" + hash.substring(2, 4) + "/" + hash; + File destPath = new File(destDir, hashedPath); + task.setHash(hash); + task.setLocation(hashedPath); + task.setTo(relPath); + destPath.getParentFile().mkdirs(); + ClientFileCollector.log.info(String.format("Adding %s from %s...", relPath, file.getAbsolutePath())); + Files.copy(file, destPath); + manifest.getTasks().add(task); + } + +} diff --git a/src/main/java/com/skcraft/launcher/builder/DirectoryWalker.java b/src/main/java/com/skcraft/launcher/builder/DirectoryWalker.java new file mode 100644 index 0000000..cf8ea95 --- /dev/null +++ b/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 + */ + public 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/src/main/java/com/skcraft/launcher/builder/PackageBuilder.java b/src/main/java/com/skcraft/launcher/builder/PackageBuilder.java new file mode 100644 index 0000000..e80aa31 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/builder/PackageBuilder.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.beust.jcommander.JCommander; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.skcraft.launcher.model.minecraft.VersionManifest; +import com.skcraft.launcher.model.modpack.Manifest; +import com.skcraft.launcher.util.SimpleLogFormatter; +import lombok.NonNull; +import lombok.extern.java.Log; + +import java.io.File; +import java.io.IOException; + +/** + * Builds packages for the launcher. + */ +@Log +public class PackageBuilder { + + private final ObjectMapper mapper; + private final Manifest manifest; + + /** + * Create a new package builder. + * + * @param mapper the mapper + * @param manifest the manifest + */ + public PackageBuilder(@NonNull ObjectMapper mapper, @NonNull Manifest manifest) { + this.mapper = mapper; + this.manifest = manifest; + } + + /** + * Add the files in the given directory. + * + * @param dir the directory + * @param destDir the directory to copy the files to + * @throws IOException thrown on I/O error + */ + private void addFiles(File dir, File destDir) throws IOException { + ClientFileCollector collector = new ClientFileCollector(this.manifest, destDir); + collector.walk(dir); + } + + /** + * Write the manifest to a file. + * + * @param path the path + * @throws IOException thrown on I/O error + */ + public void writeManifest(@NonNull File path) throws IOException { + path.getParentFile().mkdirs(); + mapper.writeValue(path, manifest); + } + + private static PackageOptions parseArgs(String[] args) { + PackageOptions options = new PackageOptions(); + new JCommander(options, args); + return options; + } + + /** + * Build a package given the arguments. + * + * @param args arguments + * @throws IOException thrown on I/O error + */ + public static void main(String[] args) throws IOException { + // May throw error here + PackageOptions options = parseArgs(args); + + SimpleLogFormatter.configureGlobalLogger(); + ObjectMapper mapper = new ObjectMapper(); + mapper.setSerializationInclusion(JsonInclude.Include.NON_DEFAULT); + + Manifest manifest = new Manifest(); + manifest.setName(options.getName()); + manifest.setTitle(options.getTitle()); + manifest.setVersion(options.getVersion()); + manifest.setGameVersion(options.getGameVersion()); + manifest.setLibrariesLocation(options.getLibrariesLocation()); + manifest.setObjectsLocation(options.getObjectsLocation()); + + File path = options.getVersionManifestPath(); + if (path != null) { + manifest.setVersionManifest(mapper.readValue(path, VersionManifest.class)); + } + + PackageBuilder builder = new PackageBuilder(mapper, manifest); + log.info("Adding files..."); + builder.addFiles(options.getFilesDir(), options.getObjectsDir()); + builder.writeManifest(options.getManifestPath()); + log.info("Wrote manifest to " + options.getManifestPath().getAbsolutePath()); + log.info("Done."); + } + +} diff --git a/src/main/java/com/skcraft/launcher/builder/PackageOptions.java b/src/main/java/com/skcraft/launcher/builder/PackageOptions.java new file mode 100644 index 0000000..f724c06 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/builder/PackageOptions.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.builder; + +import com.beust.jcommander.Parameter; +import lombok.Data; + +import java.io.File; + +@Data +public class PackageOptions { + + @Parameter(names = "--name", required = true) + private String name; + + @Parameter(names = "--title", required = true) + private String title; + + @Parameter(names = "--version", required = true) + private String version; + + @Parameter(names = "--mc-version", required = true) + private String gameVersion; + + @Parameter(names = "--manifest-path", required = true) + private File manifestPath; + + @Parameter(names = "--objects-dest", required = true) + private File objectsDir; + + @Parameter(names = "--files", required = true) + private File filesDir; + + @Parameter(names = "--version-file") + private File versionManifestPath; + + @Parameter(names = "--libs-url") + private String librariesLocation; + + @Parameter(names = "--objects-url") + private String objectsLocation; + +} diff --git a/src/main/java/com/skcraft/launcher/dialog/ConfigurationDialog.java b/src/main/java/com/skcraft/launcher/dialog/ConfigurationDialog.java new file mode 100644 index 0000000..ae7fffa --- /dev/null +++ b/src/main/java/com/skcraft/launcher/dialog/ConfigurationDialog.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.dialog; + +import com.skcraft.launcher.Configuration; +import com.skcraft.launcher.Launcher; +import com.skcraft.launcher.swing.*; +import com.skcraft.launcher.persistence.Persistence; +import lombok.NonNull; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import static com.skcraft.launcher.util.SharedLocale._; + +/** + * 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 JTextField jvmPathText = new JTextField(); + 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(_("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(_("button.ok")); + private final JButton cancelButton = new JButton(_("button.cancel")); + + /** + * 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); + + setTitle(_("options.title")); + initComponents(); + setDefaultCloseOperation(DISPOSE_ON_CLOSE); + setSize(new Dimension(400, 500)); + setResizable(false); + setLocationRelativeTo(owner); + + mapper.map(jvmPathText, "jvmPath"); + mapper.map(jvmArgsText, "jvmArgs"); + mapper.map(minMemorySpinner, "minMemory"); + mapper.map(maxMemorySpinner, "maxMemory"); + mapper.map(permGenSpinner, "permGen"); + mapper.map(widthSpinner, "windowWidth"); + mapper.map(heightSpinner, "widowHeight"); + 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(_("options.jvmPath")), jvmPathText); + javaSettingsPanel.addRow(new JLabel(_("options.jvmArguments")), jvmArgsText); + javaSettingsPanel.addRow(Box.createVerticalStrut(15)); + javaSettingsPanel.addRow(new JLabel(_("options.64BitJavaWarning"))); + javaSettingsPanel.addRow(new JLabel(_("options.minMemory")), minMemorySpinner); + javaSettingsPanel.addRow(new JLabel(_("options.maxMemory")), maxMemorySpinner); + javaSettingsPanel.addRow(new JLabel(_("options.permGen")), permGenSpinner); + SwingHelper.removeOpaqueness(javaSettingsPanel); + tabbedPane.addTab(_("options.javaTab"), SwingHelper.alignTabbedPane(javaSettingsPanel)); + + gameSettingsPanel.addRow(new JLabel(_("options.windowWidth")), widthSpinner); + gameSettingsPanel.addRow(new JLabel(_("options.windowHeight")), heightSpinner); + SwingHelper.removeOpaqueness(gameSettingsPanel); + tabbedPane.addTab(_("options.minecraftTab"), SwingHelper.alignTabbedPane(gameSettingsPanel)); + + proxySettingsPanel.addRow(useProxyCheck); + proxySettingsPanel.addRow(new JLabel(_("options.proxyHost")), proxyHostText); + proxySettingsPanel.addRow(new JLabel(_("options.proxyPort")), proxyPortText); + proxySettingsPanel.addRow(new JLabel(_("options.proxyUsername")), proxyUsernameText); + proxySettingsPanel.addRow(new JLabel(_("options.proxyPassword")), proxyPasswordText); + SwingHelper.removeOpaqueness(proxySettingsPanel); + tabbedPane.addTab(_("options.proxyTab"), SwingHelper.alignTabbedPane(proxySettingsPanel)); + + advancedPanel.addRow(new JLabel(_("options.gameKey")), gameKeyText); + SwingHelper.removeOpaqueness(advancedPanel); + tabbedPane.addTab(_("options.advancedTab"), SwingHelper.alignTabbedPane(advancedPanel)); + + 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)); + + okButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + save(); + } + }); + } + + /** + * Save the configuration and close the dialog. + */ + public void save() { + mapper.copyFromSwing(); + Persistence.commitAndForget(config); + dispose(); + } +} diff --git a/src/main/java/com/skcraft/launcher/dialog/ConsoleFrame.java b/src/main/java/com/skcraft/launcher/dialog/ConsoleFrame.java new file mode 100644 index 0000000..ff7df48 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/dialog/ConsoleFrame.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.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 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._; + +/** + * A frame capable of showing messages. + */ +public class ConsoleFrame extends JFrame { + + @Getter private final Image trayRunningIcon; + @Getter private final Image trayClosedIcon; + + @Getter private final MessageLog messageLog; + @Getter private LinedBoxPanel buttonsPanel; + + /** + * 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(_("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.readIconImage(Launcher.class, "tray_ok.png"); + trayClosedIcon = SwingHelper.readIconImage(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(_("console.uploadLog")); + buttonsPanel = new LinedBoxPanel(true); + + buttonsPanel.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); + buttonsPanel.addElement(pastebinButton); + + add(buttonsPanel, BorderLayout.NORTH); + add(messageLog, BorderLayout.CENTER); + + pastebinButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + pastebinLog(); + } + }); + } + + /** + * Attempt to perform window close. + */ + protected void performClose() { + messageLog.detachGlobalHandler(); + dispose(); + } + + /** + * Send the contents of the message log to a pastebin. + */ + private void pastebinLog() { + String text = messageLog.getPastableText(); + // Not really bytes! + messageLog.log(_("console.pasteUploading", text.length()), messageLog.asHighlighted()); + + PastebinPoster.paste(text, new PastebinPoster.PasteCallback() { + @Override + public void handleSuccess(String url) { + messageLog.log(_("console.pasteUploaded", url), messageLog.asHighlighted()); + SwingHelper.openURL(url, messageLog); + } + + @Override + public void handleError(String err) { + messageLog.log(_("console.pasteFailed", err), messageLog.asError()); + } + }); + } + +} diff --git a/src/main/java/com/skcraft/launcher/dialog/LauncherFrame.java b/src/main/java/com/skcraft/launcher/dialog/LauncherFrame.java new file mode 100644 index 0000000..eb14c6e --- /dev/null +++ b/src/main/java/com/skcraft/launcher/dialog/LauncherFrame.java @@ -0,0 +1,505 @@ +/* + * 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.launcher.Instance; +import com.skcraft.launcher.InstanceList; +import com.skcraft.launcher.Launcher; +import com.skcraft.launcher.auth.Session; +import com.skcraft.launcher.launch.InstanceLauncher; +import com.skcraft.launcher.launch.LaunchProcessHandler; +import com.skcraft.launcher.persistence.Persistence; +import com.skcraft.launcher.selfupdate.LauncherUpdateChecker; +import com.skcraft.launcher.selfupdate.LauncherUpdater; +import com.skcraft.launcher.swing.*; +import com.skcraft.launcher.update.InstanceDeleter; +import com.skcraft.launcher.update.InstanceResetter; +import com.skcraft.launcher.update.InstanceUpdater; +import com.skcraft.launcher.util.SwingExecutor; +import lombok.NonNull; +import lombok.extern.java.Log; +import org.apache.commons.io.FileUtils; + +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.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.Date; +import java.util.logging.Level; + +import static com.google.common.util.concurrent.MoreExecutors.sameThreadExecutor; +import static com.skcraft.launcher.util.SharedLocale._; + +/** + * The main launcher frame. + */ +@Log +public class LauncherFrame extends JFrame { + + private final Launcher launcher; + + private final HeaderPanel header = new HeaderPanel(); + private final InstanceTable instancesTable = new InstanceTable(); + private final InstanceTableModel instancesModel; + private final JScrollPane instanceScroll = new JScrollPane(instancesTable); + private WebpagePanel webView; + private JSplitPane splitPane; + private final JPanel container = new JPanel(); + private final LinedBoxPanel buttonsPanel = new LinedBoxPanel(true).fullyPadded(); + private final JButton launchButton = new JButton(_("launcher.launch")); + private final JButton refreshButton = new JButton(_("launcher.checkForUpdates")); + private final JButton optionsButton = new JButton(_("launcher.options")); + private final JButton selfUpdateButton = new JButton(_("launcher.updateLauncher")); + private final JCheckBox updateCheck = new JCheckBox(_("launcher.downloadUpdates")); + private URL updateUrl; + + /** + * Create a new frame. + * + * @param launcher the launcher + */ + public LauncherFrame(@NonNull Launcher launcher) { + super(_("launcher.title", launcher.getVersion())); + + this.launcher = launcher; + instancesModel = new InstanceTableModel(launcher.getInstances()); + + setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); + setSize(700, 450); + setMinimumSize(new Dimension(400, 300)); + initComponents(); + setLocationRelativeTo(null); + + SwingHelper.setIconImage(this, Launcher.class, "icon.png"); + + loadInstances(); + checkLauncherUpdate(); + } + + private void initComponents() { + webView = WebpagePanel.forURL(launcher.getNewsURL(), false); + splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, instanceScroll, webView); + selfUpdateButton.setVisible(false); + + updateCheck.setSelected(true); + instancesTable.setModel(instancesModel); + launchButton.setFont(launchButton.getFont().deriveFont(Font.BOLD)); + splitPane.setDividerLocation(200); + splitPane.setDividerSize(4); + SwingHelper.flattenJSplitPane(splitPane); + buttonsPanel.addElement(refreshButton); + buttonsPanel.addElement(updateCheck); + buttonsPanel.addGlue(); + buttonsPanel.addElement(selfUpdateButton); + buttonsPanel.addElement(optionsButton); + buttonsPanel.addElement(launchButton); + container.setLayout(new BorderLayout()); + container.setBorder(BorderFactory.createEmptyBorder(10, 10, 0, 10)); + container.add(splitPane, BorderLayout.CENTER); + add(buttonsPanel, BorderLayout.SOUTH); + 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(); + } + }); + + selfUpdateButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + selfUpdate(); + } + }); + + 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); + } + }); + } + + private void checkLauncherUpdate() { + ListenableFuture future = launcher.getExecutor().submit(new LauncherUpdateChecker(launcher)); + + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(URL result) { + requestUpdate(result); + } + + @Override + public void onFailure(Throwable t) { + + } + }, SwingExecutor.INSTANCE); + } + + private void selfUpdate() { + URL url = updateUrl; + if (url != null) { + LauncherUpdater downloader = new LauncherUpdater(launcher, url); + ObservableFuture future = new ObservableFuture( + launcher.getExecutor().submit(downloader), downloader); + + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(File result) { + selfUpdateButton.setVisible(false); + SwingHelper.showMessageDialog( + LauncherFrame.this, + _("launcher.selfUpdateComplete"), + _("launcher.selfUpdateCompleteTitle"), + null, + JOptionPane.INFORMATION_MESSAGE); + } + + @Override + public void onFailure(Throwable t) { + } + }, SwingExecutor.INSTANCE); + + ProgressDialog.showProgress(this, future, _("launcher.selfUpdatingTitle"), _("launcher.selfUpdatingStatus")); + SwingHelper.addErrorDialogCallback(this, future); + + } else { + selfUpdateButton.setVisible(false); + } + } + + private void requestUpdate(URL url) { + this.updateUrl = url; + selfUpdateButton.setVisible(true); + } + + /** + * 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() ? "Install" : "Launch"); + menuItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + launch(); + } + }); + popup.add(menuItem); + + if (selected.isLocal()) { + popup.addSeparator(); + + menuItem = new JMenuItem(_("instance.openFolder")); + menuItem.addActionListener(ActionListeners.browseDir( + LauncherFrame.this, selected.getContentDir(), true)); + popup.add(menuItem); + + menuItem = new JMenuItem(_("instance.openSaves")); + menuItem.addActionListener(ActionListeners.browseDir( + LauncherFrame.this, new File(selected.getContentDir(), "saves"), true)); + popup.add(menuItem); + + menuItem = new JMenuItem(_("instance.openResourcePacks")); + menuItem.addActionListener(ActionListeners.browseDir( + LauncherFrame.this, new File(selected.getContentDir(), "resourcepacks"), true)); + popup.add(menuItem); + + menuItem = new JMenuItem(_("instance.openScreenshots")); + menuItem.addActionListener(ActionListeners.browseDir( + LauncherFrame.this, new File(selected.getContentDir(), "screenshots"), true)); + popup.add(menuItem); + + menuItem = new JMenuItem(_("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); + + popup.addSeparator(); + + if (!selected.isUpdatePending()) { + menuItem = new JMenuItem(_("instance.forceUpdate")); + menuItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + selected.setUpdatePending(true); + launch(); + instancesModel.update(); + } + }); + popup.add(menuItem); + } + + menuItem = new JMenuItem(_("instance.hardForceUpdate")); + menuItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + confirmHardUpdate(selected); + } + }); + popup.add(menuItem); + + menuItem = new JMenuItem(_("instance.deleteFiles")); + menuItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + confirmDelete(selected); + } + }); + popup.add(menuItem); + } + + popup.addSeparator(); + } + + menuItem = new JMenuItem(_("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, + _("instance.confirmDelete", instance.getTitle()), _("confirmTitle"))) { + return; + } + + // Execute the deleter + InstanceDeleter resetter = new InstanceDeleter(instance); + ObservableFuture future = new ObservableFuture( + launcher.getExecutor().submit(resetter), resetter); + + // Show progress + ProgressDialog.showProgress( + this, future, _("instance.deletingTitle"), _("instance.deletingStatus", instance.getTitle())); + SwingHelper.addErrorDialogCallback(this, future); + + // 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, _("instance.confirmHardUpdate"), _("confirmTitle"))) { + return; + } + + // Execute the resetter + InstanceResetter resetter = new InstanceResetter(instance); + ObservableFuture future = new ObservableFuture( + launcher.getExecutor().submit(resetter), resetter); + + // Show progress + ProgressDialog.showProgress( this, future, _("instance.resettingTitle"), + _("instance.resettingStatus", instance.getTitle())); + SwingHelper.addErrorDialogCallback(this, future); + + // Update the list of instances after updating + future.addListener(new Runnable() { + @Override + public void run() { + launch(); + instancesModel.update(); + } + }, SwingExecutor.INSTANCE); + } + + private void loadInstances() { + InstanceList.Enumerator loader = launcher.getInstances().createEnumerator(); + ObservableFuture future = new ObservableFuture( + launcher.getExecutor().submit(loader), loader); + + 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, _("launcher.checkingTitle"), _("launcher.checkingStatus")); + SwingHelper.addErrorDialogCallback(this, future); + } + + private void showOptions() { + ConfigurationDialog configDialog = new ConfigurationDialog(this, launcher); + configDialog.setVisible(true); + } + + private void launch() { + try { + final Instance instance = launcher.getInstances().get(instancesTable.getSelectedRow()); + boolean update = updateCheck.isSelected() && instance.isUpdatePending(); + + // Store last access date + Date now = new Date(); + instance.setLastAccessed(now); + Persistence.commitAndForget(instance); + + // Perform login + final Session session = LoginDialog.showLoginRequest(this, launcher); + if (session == null) { + return; + } + + // If we have to update, we have to update + if (!instance.isInstalled()) { + update = true; + } + + if (update) { + // Execute the updater + InstanceUpdater updater = new InstanceUpdater(launcher, instance); + ObservableFuture future = new ObservableFuture( + launcher.getExecutor().submit(updater), updater); + + // Show progress + ProgressDialog.showProgress( + this, future, _("launcher.updatingTitle"), _("launcher.updatingStatus", instance.getTitle())); + SwingHelper.addErrorDialogCallback(this, future); + + // Update the list of instances after updating + future.addListener(new Runnable() { + @Override + public void run() { + instancesModel.update(); + } + }, SwingExecutor.INSTANCE); + + // On success, launch also + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(Instance result) { + launch(instance, session); + } + + @Override + public void onFailure(Throwable t) { + } + }, SwingExecutor.INSTANCE); + } else { + launch(instance, session); + } + } catch (ArrayIndexOutOfBoundsException e) { + SwingHelper.showErrorDialog(this, _("launcher.noInstanceError"), _("launcher.noInstanceTitle")); + } + } + + private void launch(Instance instance, Session session) { + final File extractDir = launcher.createExtractDir(); + + // Get the process + InstanceLauncher task = new InstanceLauncher(launcher, instance, session, extractDir); + ObservableFuture processFuture = new ObservableFuture( + launcher.getExecutor().submit(task), task); + + // Show process for the process retrieval + ProgressDialog.showProgress( + this, processFuture, _("launcher.launchingTItle"), _("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) { + dispose(); + } + + @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(new Runnable() { + @Override + public void run() { + 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()); + } + +} diff --git a/src/main/java/com/skcraft/launcher/dialog/LoginDialog.java b/src/main/java/com/skcraft/launcher/dialog/LoginDialog.java new file mode 100644 index 0000000..2b0ef0d --- /dev/null +++ b/src/main/java/com/skcraft/launcher/dialog/LoginDialog.java @@ -0,0 +1,352 @@ +/* + * 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.base.Strings; +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.*; +import com.skcraft.launcher.swing.*; +import com.skcraft.launcher.persistence.Persistence; +import com.skcraft.launcher.util.SwingExecutor; +import lombok.Getter; +import lombok.NonNull; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.*; +import java.io.IOException; +import java.util.Date; +import java.util.List; +import java.util.concurrent.Callable; + +import static com.skcraft.launcher.util.SharedLocale._; + +/** + * The login dialog. + */ +public class LoginDialog extends JDialog { + + private final Launcher launcher; + @Getter private final AccountList accounts; + @Getter private Session session; + + private final JComboBox idCombo = new JComboBox(); + private final JPasswordField passwordText = new JPasswordField(); + private final JCheckBox rememberIdCheck = new JCheckBox(_("login.rememberId")); + private final JCheckBox rememberPassCheck = new JCheckBox(_("login.rememberPassword")); + private final JButton loginButton = new JButton(_("login.login")); + private final LinkButton recoverButton = new LinkButton(_("login.recoverAccount")); + private final JButton offlineButton = new JButton(_("login.playOffline")); + private final JButton cancelButton = new JButton(_("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) { + super(owner, ModalityType.DOCUMENT_MODAL); + + this.launcher = launcher; + this.accounts = launcher.getAccounts(); + + setTitle(_("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) { + removeListeners(); + dispose(); + } + }); + } + + private void removeListeners() { + idCombo.setModel(new DefaultComboBoxModel()); + } + + private void initComponents() { + idCombo.setModel(getAccounts()); + updateSelection(); + + rememberIdCheck.setBorder(BorderFactory.createEmptyBorder()); + rememberPassCheck.setBorder(BorderFactory.createEmptyBorder()); + idCombo.setEditable(true); + idCombo.getEditor().selectAll(); + + loginButton.setFont(loginButton.getFont().deriveFont(Font.BOLD)); + + formPanel.addRow(new JLabel(_("login.idPassword")), idCombo); + formPanel.addRow(new JLabel(_("login.password")), passwordText); + formPanel.addRow(new JLabel(), rememberIdCheck); + formPanel.addRow(new JLabel(), rememberPassCheck); + buttonsPanel.setBorder(BorderFactory.createEmptyBorder(26, 13, 13, 13)); + + if (launcher.getConfig().isOfflineEnabled()) { + buttonsPanel.addElement(offlineButton); + buttonsPanel.addElement(Box.createHorizontalStrut(2)); + } + 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); + + idCombo.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + updateSelection(); + } + }); + + idCombo.getEditor().getEditorComponent().addMouseListener(new PopupMouseAdapter() { + @Override + protected void showPopup(MouseEvent e) { + popupManageMenu(e.getComponent(), e.getX(), e.getY()); + } + }); + + recoverButton.addActionListener( + ActionListeners.openURL(recoverButton, launcher.getProperties().getProperty("resetPasswordUrl"))); + + loginButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + prepareLogin(); + } + }); + + offlineButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + setResult(new OfflineSession(launcher.getProperties().getProperty("offlinePlayerName"))); + removeListeners(); + dispose(); + } + }); + + cancelButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + removeListeners(); + dispose(); + } + }); + + rememberPassCheck.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + if (rememberPassCheck.isSelected()) { + rememberIdCheck.setSelected(true); + } + } + }); + + rememberIdCheck.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + if (!rememberIdCheck.isSelected()) { + rememberPassCheck.setSelected(false); + } + } + }); + } + + private void popupManageMenu(Component component, int x, int y) { + Object selected = idCombo.getSelectedItem(); + JPopupMenu popup = new JPopupMenu(); + JMenuItem menuItem; + + if (selected != null && selected instanceof Account) { + final Account account = (Account) selected; + + menuItem = new JMenuItem(_("login.forgetUser")); + menuItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + accounts.remove(account); + Persistence.commitAndForget(accounts); + } + }); + popup.add(menuItem); + + if (!Strings.isNullOrEmpty(account.getPassword())) { + menuItem = new JMenuItem(_("login.forgetPassword")); + menuItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + account.setPassword(null); + Persistence.commitAndForget(accounts); + } + }); + popup.add(menuItem); + } + } + + menuItem = new JMenuItem(_("login.forgetAllPasswords")); + menuItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + if (SwingHelper.confirmDialog(LoginDialog.this, + _("login.confirmForgetAllPasswords"), + _("login.forgetAllPasswordsTitle"))) { + accounts.forgetPasswords(); + Persistence.commitAndForget(accounts); + } + } + }); + popup.add(menuItem); + + popup.show(component, x, y); + } + + private void updateSelection() { + Object selected = idCombo.getSelectedItem(); + + if (selected != null && selected instanceof Account) { + Account account = (Account) selected; + String password = account.getPassword(); + + rememberIdCheck.setSelected(true); + if (!Strings.isNullOrEmpty(password)) { + rememberPassCheck.setSelected(true); + passwordText.setText(password); + } else { + rememberPassCheck.setSelected(false); + } + } else { + passwordText.setText(""); + rememberIdCheck.setSelected(true); + rememberPassCheck.setSelected(false); + } + } + + @SuppressWarnings("deprecation") + private void prepareLogin() { + Object selected = idCombo.getSelectedItem(); + + if (selected != null && selected instanceof Account) { + Account account = (Account) selected; + String password = passwordText.getText(); + + if (password == null || password.isEmpty()) { + SwingHelper.showErrorDialog(this, _("login.noPasswordError"), _("login.noPasswordTitle")); + } else { + if (rememberPassCheck.isSelected()) { + account.setPassword(password); + } else { + account.setPassword(null); + } + + if (rememberIdCheck.isSelected()) { + accounts.add(account); + } else { + accounts.remove(account); + } + + account.setLastUsed(new Date()); + + Persistence.commitAndForget(accounts); + + attemptLogin(account, password); + } + } else { + SwingHelper.showErrorDialog(this, _("login.noLoginError"), _("login.noLoginTitle")); + } + } + + private void attemptLogin(Account account, String password) { + LoginCallable callable = new LoginCallable(account, 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, _("login.loggingInTitle"), _("login.loggingInStatus")); + SwingHelper.addErrorDialogCallback(this, future); + } + + private void setResult(Session session) { + this.session = session; + removeListeners(); + dispose(); + } + + public static Session showLoginRequest(Window owner, Launcher launcher) { + LoginDialog dialog = new LoginDialog(owner, launcher); + dialog.setVisible(true); + return dialog.getSession(); + } + + private class LoginCallable implements Callable,ProgressObservable { + private final Account account; + private final String password; + + private LoginCallable(Account account, String password) { + this.account = account; + this.password = password; + } + + @Override + public Session call() throws AuthenticationException, IOException, InterruptedException { + LoginService service = launcher.getLoginService(); + List identities = service.login(launcher.getProperties().getProperty("agentName"), account.getId(), password); + + // The list of identities (profiles in Mojang terms) corresponds to whether the account + // owns the game, so we need to check that + if (identities.size() > 0) { + // Set offline enabled flag to true + Configuration config = launcher.getConfig(); + if (!config.isOfflineEnabled()) { + config.setOfflineEnabled(true); + Persistence.commitAndForget(config); + } + + Persistence.commitAndForget(getAccounts()); + return identities.get(0); + } else { + throw new AuthenticationException("Minecraft not owned", _("login.minecraftNotOwnedError")); + } + } + + @Override + public double getProgress() { + return -1; + } + } + +} diff --git a/src/main/java/com/skcraft/launcher/dialog/ProcessConsoleFrame.java b/src/main/java/com/skcraft/launcher/dialog/ProcessConsoleFrame.java new file mode 100644 index 0000000..7f7ab6d --- /dev/null +++ b/src/main/java/com/skcraft/launcher/dialog/ProcessConsoleFrame.java @@ -0,0 +1,230 @@ +/* + * 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 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._; + +/** + * 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(_("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(_("console.processEndCode", lastProcess.exitValue())); + } + } catch (IllegalThreadStateException e) { + } + + if (process != null) { + processOut.println(_("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(_("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(_("console.trayTooltip")); + + trayIcon.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + reshow(); + } + }); + + PopupMenu popup = new PopupMenu(); + MenuItem item; + + popup.add(item = new MenuItem(_("console.trayTitle"))); + item.setEnabled(false); + + popup.add(item = new MenuItem(_("console.tray.showWindow"))); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + reshow(); + } + }); + + popup.add(item = new MenuItem(_("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(_("console.closeWindow")); + } else { + minimizeButton.setText(_("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() { + return SwingHelper.confirmDialog(this, _("console.confirmKill"), _("console.confirmKillTitle")); + } + + private void minimize() { + setVisible(false); + } + + private void reshow() { + setVisible(true); + requestFocus(); + } + +} diff --git a/src/main/java/com/skcraft/launcher/dialog/ProgressDialog.java b/src/main/java/com/skcraft/launcher/dialog/ProgressDialog.java new file mode 100644 index 0000000..da2a951 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/dialog/ProgressDialog.java @@ -0,0 +1,184 @@ +/* + * 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.swing.LinedBoxPanel; +import com.skcraft.launcher.swing.SwingHelper; +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.util.Timer; +import java.util.TimerTask; + +import static com.skcraft.launcher.util.SharedLocale._; + +@Log +public class ProgressDialog extends JDialog { + + 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 cancelButton = new JButton(_("button.cancel")); + + public ProgressDialog(Window owner, String title, String message) { + super(owner, title, ModalityType.DOCUMENT_MODAL); + setResizable(false); + initComponents(); + label.setText(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("Details..."); + setMinimumSize(new Dimension(400, 100)); + pack(); + } + + private void setDetailsSize() { + detailsButton.setText("Less..."); + setSize(400, 350); + } + + private void initComponents() { + buttonsPanel.addElement(detailsButton); + 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()); + + progressBar.setIndeterminate(true); + 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)); + 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(); + } + }); + } + + private boolean confirmCancel() { + return SwingHelper.confirmDialog(this, "Are you sure that you wish to cancel?", "Cancel"); + } + + 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) { + final ProgressDialog dialog = new ProgressDialog(owner, title, message) { + @Override + protected void cancel() { + future.cancel(true); + } + }; + + final Timer timer = new Timer(); + timer.scheduleAtFixedRate(new UpdateProgress(dialog, future), 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); + } + + 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() { + dialog.logText.setText(String.valueOf(observable)); + dialog.logText.setCaretPosition(0); + } + }); + } + } + +} diff --git a/src/main/java/com/skcraft/launcher/launch/InstanceLauncher.java b/src/main/java/com/skcraft/launcher/launch/InstanceLauncher.java new file mode 100644 index 0000000..845ee3f --- /dev/null +++ b/src/main/java/com/skcraft/launcher/launch/InstanceLauncher.java @@ -0,0 +1,304 @@ +/* + * 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.io.Files; +import com.skcraft.concurrency.ProgressObservable; +import com.skcraft.launcher.AssetsRoot; +import com.skcraft.launcher.Configuration; +import com.skcraft.launcher.Instance; +import com.skcraft.launcher.Launcher; +import com.skcraft.launcher.auth.Session; +import com.skcraft.launcher.model.minecraft.AssetsIndex; +import com.skcraft.launcher.model.minecraft.Library; +import com.skcraft.launcher.model.minecraft.VersionManifest; +import com.skcraft.launcher.update.ZipExtract; +import com.skcraft.launcher.util.Environment; +import com.skcraft.launcher.util.Platform; +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.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; + +import static com.skcraft.launcher.LauncherUtils.checkInterrupted; + +/** + * Handles the launching of an instance. + */ +@Log +public class InstanceLauncher implements Callable, ProgressObservable { + + private final ObjectMapper mapper = new ObjectMapper(); + private final Launcher launcher; + private final Instance instance; + private final Session session; + private final File extractDir; + @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; + + /** + * Create a new instance launcher. + * + * @param launcher the launcher + * @param instance the instance + * @param session the session + * @param extractDir the directory to extract to + */ + public InstanceLauncher(@NonNull Launcher launcher, + @NonNull Instance instance, + @NonNull Session session, + @NonNull File extractDir) { + this.launcher = launcher; + this.instance = instance; + this.session = session; + this.extractDir = extractDir; + } + + /** + * 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 { + config = launcher.getConfig(); + builder = new JavaProcessBuilder(); + assetsRoot = launcher.getAssets(); + + // Load versionManifest and assets index + versionManifest = mapper.readValue(instance.getVersionManifestPath(), VersionManifest.class); + assetsIndex = mapper.readValue(assetsRoot.getIndexPath(versionManifest.getId()), AssetsIndex.class); + + // Copy over assets to the tree + virtualAssetsDir = assetsRoot.buildAssetTree(versionManifest.getAssetsIndex()); + + addJvmArgs(); + addLibraries(); + addJarArgs(); + addProxyArgs(); + addWindowArgs(); + addPlatformArgs(); + + builder.classPath(getJarPath()); + builder.setMainClass(versionManifest.getMainClass()); + + ProcessBuilder processBuilder = new ProcessBuilder(builder.buildCommand()); + processBuilder.directory(instance.getContentDir()); + log.info("Launching: " + builder); + checkInterrupted(); + + return processBuilder.start(); + } + + /** + * 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"); + } + } + + // Windows arguments + if (getEnvironment().getPlatform() == Platform.WINDOWS) { + builder.getFlags().add("-XX:HeapDumpPath=MojangTricksIntelDriversForPerformance_javaw.exe_minecraft.exe.heapdump"); + } + } + + /** + * Add libraries. + */ + private void addLibraries() { + // Add libraries to classpath or extract the libraries as necessary + for (Library library : versionManifest.getLibraries()) { + 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); + } + } + } + + builder.getFlags().add("-Djava.library.path=" + extractDir.getAbsoluteFile()); + } + + /** + * Add JVM arguments. + * + * @throws IOException on I/O error + */ + private void addJvmArgs() throws IOException { + int minMemory = config.getMinMemory(); + int maxMemory = 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); + + String rawJvmPath = config.getJvmPath(); + if (!Strings.isNullOrEmpty(rawJvmPath)) { + builder.tryJvmPath(new File(rawJvmPath)); + } + + String rawJvmArgs = config.getJvmArgs(); + if (!Strings.isNullOrEmpty(rawJvmArgs)) { + List flags = builder.getFlags(); + + for (String arg : JavaProcessBuilder.splitArgs(rawJvmArgs)) { + flags.add(arg); + } + } + } + + /** + * Add arguments for the application. + * + * @throws JsonProcessingException on error + */ + private void addJarArgs() throws JsonProcessingException { + List args = builder.getArgs(); + + String[] rawArgs = versionManifest.getMinecraftArguments().split(" +"); + StrSubstitutor substitutor = new StrSubstitutor(getCommandSubstitutions()); + for (String arg : rawArgs) { + args.add(substitutor.replace(arg)); + } + } + + /** + * 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 window arguments. + */ + private void addWindowArgs() { + List args = builder.getArgs(); + int width = config.getWindowWidth(); + int height = config.getWidowHeight(); + + if (width >= 10) { + args.add("--width"); + args.add(String.valueOf(width)); + args.add("--height"); + args.add(String.valueOf(height)); + } + } + + /** + * 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("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().getName()); + 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.getAssetsIndex()); + + return map; + } + + @Override + public double getProgress() { + return -1; + } + +} diff --git a/src/main/java/com/skcraft/launcher/launch/JavaProcessBuilder.java b/src/main/java/com/skcraft/launcher/launch/JavaProcessBuilder.java new file mode 100644 index 0000000..61d1dc1 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/launch/JavaProcessBuilder.java @@ -0,0 +1,129 @@ +/* + * 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 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; + +@ToString +public class JavaProcessBuilder { + + private static final Pattern argsPattern = Pattern.compile("(?:([^\"]\\S*)|\"(.+?)\")\\s*"); + + @Getter @Setter private File jvmPath = JavaRuntimeFinder.findBestJavaPath(); + @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; + + public void tryJvmPath(File path) throws IOException { + // 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; + } + + setJvmPath(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() { + List command = new ArrayList(); + + command.add(getJvmPath() + File.separator + "java"); + + for (String flag : flags) { + command.add(flag); + } + + if (minMemory > 0) { + command.add("-Xms" + String.valueOf(minMemory) + "M"); + } + + if (maxMemory > 0) { + command.add("-Xmx" + String.valueOf(maxMemory) + "M"); + } + + if (permGen > 0) { + command.add("-XX:MaxPermSize=" + String.valueOf(permGen) + "M"); + } + + command.add("-cp"); + command.add(buildClassPath()); + + command.add(mainClass); + + for (String arg : args) { + command.add(arg); + } + + 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/src/main/java/com/skcraft/launcher/launch/JavaRuntimeFinder.java b/src/main/java/com/skcraft/launcher/launch/JavaRuntimeFinder.java new file mode 100644 index 0000000..6926176 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/launch/JavaRuntimeFinder.java @@ -0,0 +1,136 @@ +/* + * 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.util.Environment; +import com.skcraft.launcher.util.Platform; +import com.skcraft.launcher.util.WinRegistry; + +import java.io.File; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Finds the best Java runtime to use. + */ +public final class JavaRuntimeFinder { + + private JavaRuntimeFinder() { + } + + /** + * Return the path to the best found JVM location. + * + * @return the JVM location, or null + */ + public static File findBestJavaPath() { + if (Environment.getInstance().getPlatform() != Platform.WINDOWS) { + return null; + } + + List entries = new ArrayList(); + try { + getEntriesFromRegistry(entries, "SOFTWARE\\JavaSoft\\Java Runtime Environment"); + getEntriesFromRegistry(entries, "SOFTWARE\\JavaSoft\\Java Development Kit"); + } catch (Throwable e) { + } + Collections.sort(entries); + + if (entries.size() > 0) { + return new File(entries.get(0).dir, "bin"); + } + + return null; + } + + private static void getEntriesFromRegistry(List entries, String basePath) + throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { + List subKeys = WinRegistry.readStringSubKeys( + WinRegistry.HKEY_LOCAL_MACHINE, basePath); + for (String subKey : subKeys) { + JREEntry entry = getEntryFromRegistry(basePath, subKey); + if (entry != null) { + entries.add(entry); + } + } + } + + private static JREEntry getEntryFromRegistry(String basePath, String version) + throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { + String regPath = basePath + "\\" + version; + String path = WinRegistry.readString( + WinRegistry.HKEY_LOCAL_MACHINE, regPath, "JavaHome"); + File dir = new File(path); + if (dir.exists() && new File(dir, "bin/java.exe").exists()) { + JREEntry entry = new JREEntry(); + entry.dir = dir; + entry.version = version; + entry.is64Bit = guessIf64Bit(dir); + return entry; + } else { + return null; + } + } + + private static boolean guessIf64Bit(File path) { + String programFilesX86 = System.getenv("ProgramFiles(x86)"); + if (programFilesX86 == null) { + return true; + } + return !path.toString().startsWith(new File(programFilesX86).toString()); + } + + private static class JREEntry implements Comparable { + private File dir; + private String version; + private boolean is64Bit; + + @Override + public int compareTo(JREEntry o) { + if (is64Bit && !o.is64Bit) { + return -1; + } else if (!is64Bit && o.is64Bit) { + return 1; + } + + String[] a = version.split("[\\._]"); + String[] b = o.version.split("[\\._]"); + int min = Math.min(a.length, b.length); + + for (int i = 0; i < min; i++) { + int first, second; + + try { + first = Integer.parseInt(a[i]); + } catch (NumberFormatException e) { + return -1; + } + + try { + second = Integer.parseInt(b[i]); + } catch (NumberFormatException e) { + return 1; + } + + if (first > second) { + return -1; + } else if (first < second) { + return 1; + } + } + + if (a.length == b.length) { + return 0; // Same + } + + return a.length > b.length ? -1 : 1; + } + } + +} diff --git a/src/main/java/com/skcraft/launcher/launch/LaunchProcessHandler.java b/src/main/java/com/skcraft/launcher/launch/LaunchProcessHandler.java new file mode 100644 index 0000000..0e4934e --- /dev/null +++ b/src/main/java/com/skcraft/launcher/launch/LaunchProcessHandler.java @@ -0,0 +1,79 @@ +/* + * 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() { + new LauncherFrame(launcher).setVisible(true); + + if (consoleFrame != null) { + consoleFrame.setProcess(null); + consoleFrame.requestFocus(); + } + } + }); + + return consoleFrame; + } + +} diff --git a/src/main/java/com/skcraft/launcher/model/minecraft/Asset.java b/src/main/java/com/skcraft/launcher/model/minecraft/Asset.java new file mode 100644 index 0000000..122e9ce --- /dev/null +++ b/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/src/main/java/com/skcraft/launcher/model/minecraft/AssetsIndex.java b/src/main/java/com/skcraft/launcher/model/minecraft/AssetsIndex.java new file mode 100644 index 0000000..53f56a0 --- /dev/null +++ b/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/src/main/java/com/skcraft/launcher/model/minecraft/Library.java b/src/main/java/com/skcraft/launcher/model/minecraft/Library.java new file mode 100644 index 0000000..923b913 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/model/minecraft/Library.java @@ -0,0 +1,197 @@ +/* + * 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.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.skcraft.launcher.Launcher; +import com.skcraft.launcher.LauncherUtils; +import com.skcraft.launcher.util.Environment; +import com.skcraft.launcher.util.HttpRequest; +import com.skcraft.launcher.util.Platform; +import lombok.Data; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class Library { + + private String name; + private transient String group; + private transient String artifact; + private transient String version; + private Map natives; + private Extract extract; + private List rules; + + // Forge-added + private String comment; + + // Custom + private boolean locallyAvailable; + + public void setName(String name) { + this.name = name; + + if (name != null) { + String[] parts = name.split(":"); + this.group = parts[0]; + this.artifact = parts[1]; + this.version = parts[2]; + } else { + this.group = null; + this.artifact = null; + this.version = null; + } + } + + public boolean matches(Environment environment) { + boolean allow = false; + + if (getRules() != null) { + for (Rule rule : getRules()) { + if (rule.matches(environment)) { + allow = rule.getAction() == Action.ALLOW; + } + } + } else { + allow = true; + } + + return allow; + } + + @JsonIgnore + public String getGroup() { + return group; + } + + @JsonIgnore + public String getArtifact() { + return artifact; + } + + @JsonIgnore + public String getVersion() { + return version; + } + + public String getNativeString(Platform platform) { + if (getNatives() != null) { + switch (platform) { + case LINUX: + return getNatives().get("linux"); + case WINDOWS: + return getNatives().get("windows"); + case MAC_OS_X: + return getNatives().get("osx"); + default: + return null; + } + } else { + return null; + } + } + + public String getFilename(Environment environment) { + String nativeString = getNativeString(environment.getPlatform()); + if (nativeString != null) { + return String.format("%s-%s-%s.jar", + getArtifact(), getVersion(), nativeString); + } + + return String.format("%s-%s.jar", getArtifact(), getVersion()); + } + + public String getPath(Environment environment) { + StringBuilder builder = new StringBuilder(); + builder.append(getGroup().replace('.', '/')); + builder.append("/"); + builder.append(getArtifact()); + builder.append("/"); + builder.append(getVersion()); + builder.append("/"); + builder.append(getFilename(environment)); + String path = builder.toString(); + path = path.replace("${arch}", environment.getArchBits()); + return path; + } + + public URL getURL(Launcher launcher, Environment environment, URL baseURL) { + if (locallyAvailable && baseURL != null) { + try { + return LauncherUtils.concat(baseURL, getPath(environment)); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } else { + StringBuilder builder = new StringBuilder(); + builder.append(launcher.getProperties().getProperty("librariesUrl")); + builder.append(getPath(environment)); + return HttpRequest.url(builder.toString()); + } + } + + @Data + public static class Rule { + private Action action; + private OS os; + + public boolean matches(Environment environment) { + if (getOs() == null) { + return true; + } else { + return getOs().matches(environment); + } + } + } + + @Data + 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()); + } + } + + @Data + public static class Extract { + private List exclude; + } + + private 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/src/main/java/com/skcraft/launcher/model/minecraft/PlatformDeserializer.java b/src/main/java/com/skcraft/launcher/model/minecraft/PlatformDeserializer.java new file mode 100644 index 0000000..5eb8a51 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/model/minecraft/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; + +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/src/main/java/com/skcraft/launcher/model/minecraft/PlatformSerializer.java b/src/main/java/com/skcraft/launcher/model/minecraft/PlatformSerializer.java new file mode 100644 index 0000000..91a44b1 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/model/minecraft/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; + +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/src/main/java/com/skcraft/launcher/model/minecraft/ReleaseList.java b/src/main/java/com/skcraft/launcher/model/minecraft/ReleaseList.java new file mode 100644 index 0000000..4cfcc1d --- /dev/null +++ b/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/src/main/java/com/skcraft/launcher/model/minecraft/Version.java b/src/main/java/com/skcraft/launcher/model/minecraft/Version.java new file mode 100644 index 0000000..c01f6cb --- /dev/null +++ b/src/main/java/com/skcraft/launcher/model/minecraft/Version.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.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; + + 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/src/main/java/com/skcraft/launcher/model/minecraft/VersionManifest.java b/src/main/java/com/skcraft/launcher/model/minecraft/VersionManifest.java new file mode 100644 index 0000000..eee01d4 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/model/minecraft/VersionManifest.java @@ -0,0 +1,36 @@ +/* + * 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.Data; + +import java.util.Date; +import java.util.List; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class VersionManifest { + + private String id; + private Date time; + private Date releaseTime; + private String assets; + private String type; + private String processArguments; + private String minecraftArguments; + private String mainClass; + private int minimumLauncherVersion; + private List libraries; + + @JsonIgnore + public String getAssetsIndex() { + return getAssets() != null ? getAssets() : "legacy"; + } + +} diff --git a/src/main/java/com/skcraft/launcher/model/modpack/BaseManifest.java b/src/main/java/com/skcraft/launcher/model/modpack/BaseManifest.java new file mode 100644 index 0000000..3aa84ea --- /dev/null +++ b/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/src/main/java/com/skcraft/launcher/model/modpack/FileInstall.java b/src/main/java/com/skcraft/launcher/model/modpack/FileInstall.java new file mode 100644 index 0000000..c9c3287 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/model/modpack/FileInstall.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.model.modpack; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.skcraft.launcher.update.FileDistribute; +import com.skcraft.launcher.update.UpdateCache; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.apache.commons.io.FilenameUtils; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.skcraft.launcher.LauncherUtils.concat; + +@Data +@EqualsAndHashCode(callSuper = false) +public class FileInstall extends Task { + + private String version; + private String hash; + private String location; + private String to; + + @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 run() { + UpdateCache updateCache = getInstaller().getUpdateCache(); + String targetPath = getTargetPath(); + URL url; + + try { + url = concat(getManifest().getObjectsURL(), getLocation()); + } catch (MalformedURLException e) { + throw new RuntimeException("Invalid URL encountered", e); + } + + try { + if (updateCache.mark(FilenameUtils.normalize(targetPath), getImpliedVersion())) { + File targetFile = new File(getInstaller().getDestinationDir(), targetPath); + File sourceFile = getInstaller().download(url, getImpliedVersion()); + List targets = new ArrayList(); + targets.add(targetFile); + getInstaller().submit(new FileDistribute(sourceFile, targets)); + } + + getInstaller().getCurrentLog().add(to, to); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (IOException e) { + throw new RuntimeException("Failed to download " + url.toString(), e); + } + } + +} diff --git a/src/main/java/com/skcraft/launcher/model/modpack/Manifest.java b/src/main/java/com/skcraft/launcher/model/modpack/Manifest.java new file mode 100644 index 0000000..c07f273 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/model/modpack/Manifest.java @@ -0,0 +1,65 @@ +/* + * 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.google.common.base.Strings; +import com.skcraft.launcher.LauncherUtils; +import com.skcraft.launcher.model.minecraft.VersionManifest; +import com.skcraft.launcher.update.Installer; +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.List; + +@Data +@EqualsAndHashCode(callSuper = true) +public class Manifest extends BaseManifest { + + private URL baseUrl; + private String librariesLocation; + private String objectsLocation; + private String gameVersion; + @JsonManagedReference("manifest") + private List tasks = new ArrayList(); + @Getter @Setter @JsonIgnore + private Installer installer; + private VersionManifest versionManifest; + + @JsonIgnore + public URL getLibrariesURL() { + if (Strings.nullToEmpty(getLibrariesLocation()) == null) { + return baseUrl; + } + + 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); + } + } + +} diff --git a/src/main/java/com/skcraft/launcher/model/modpack/ManifestInfo.java b/src/main/java/com/skcraft/launcher/model/modpack/ManifestInfo.java new file mode 100644 index 0000000..e035579 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/model/modpack/ManifestInfo.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.modpack; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class ManifestInfo extends BaseManifest { + + private String location; + private int priority; + +} diff --git a/src/main/java/com/skcraft/launcher/model/modpack/PackageList.java b/src/main/java/com/skcraft/launcher/model/modpack/PackageList.java new file mode 100644 index 0000000..8385ff3 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/model/modpack/PackageList.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.modpack; + +import lombok.Data; + +import java.util.List; + +@Data +public class PackageList { + + private int minimumVersion; + private List packages; + +} diff --git a/src/main/java/com/skcraft/launcher/model/modpack/Task.java b/src/main/java/com/skcraft/launcher/model/modpack/Task.java new file mode 100644 index 0000000..8297f1b --- /dev/null +++ b/src/main/java/com/skcraft/launcher/model/modpack/Task.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.model.modpack; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.skcraft.launcher.update.Installer; +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") +}) +@Data +@ToString(exclude = "manifest") +public abstract class Task implements Runnable { + + @JsonBackReference("manifest") + private Manifest manifest; + + @JsonIgnore + public Installer getInstaller() { + return getManifest().getInstaller(); + } + +} diff --git a/src/main/java/com/skcraft/launcher/persistence/MkdirByteSink.java b/src/main/java/com/skcraft/launcher/persistence/MkdirByteSink.java new file mode 100644 index 0000000..e76be6c --- /dev/null +++ b/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/src/main/java/com/skcraft/launcher/persistence/Persistence.java b/src/main/java/com/skcraft/launcher/persistence/Persistence.java new file mode 100644 index 0000000..0eae3cc --- /dev/null +++ b/src/main/java/com/skcraft/launcher/persistence/Persistence.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.persistence; + +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.*; +import java.util.WeakHashMap; +import java.util.logging.Level; + +/** + * Simple persistence framework that can bind an object to a file and later allow for + * code utilizing the object to save it globally. + */ +@Log +public final class Persistence { + + private static final ObjectMapper mapper = new ObjectMapper(); + private static final WeakHashMap bound = + new WeakHashMap(); + + 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 { + file.getParentFile().mkdirs(); + mapper.writeValue(file, object); + } + +} diff --git a/src/main/java/com/skcraft/launcher/persistence/Scrambled.java b/src/main/java/com/skcraft/launcher/persistence/Scrambled.java new file mode 100644 index 0000000..c96753c --- /dev/null +++ b/src/main/java/com/skcraft/launcher/persistence/Scrambled.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.persistence; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Scrambled { + + String value(); + +} diff --git a/src/main/java/com/skcraft/launcher/persistence/ScramblingSinkFilter.java b/src/main/java/com/skcraft/launcher/persistence/ScramblingSinkFilter.java new file mode 100644 index 0000000..862a6e9 --- /dev/null +++ b/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/src/main/java/com/skcraft/launcher/persistence/ScramblingSourceFilter.java b/src/main/java/com/skcraft/launcher/persistence/ScramblingSourceFilter.java new file mode 100644 index 0000000..fdfa03b --- /dev/null +++ b/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/src/main/java/com/skcraft/launcher/selfupdate/ComparableVersion.java b/src/main/java/com/skcraft/launcher/selfupdate/ComparableVersion.java new file mode 100644 index 0000000..54da83a --- /dev/null +++ b/src/main/java/com/skcraft/launcher/selfupdate/ComparableVersion.java @@ -0,0 +1,365 @@ +/* + * 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$ + */ +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/src/main/java/com/skcraft/launcher/selfupdate/LatestVersionInfo.java b/src/main/java/com/skcraft/launcher/selfupdate/LatestVersionInfo.java new file mode 100644 index 0000000..b299a3c --- /dev/null +++ b/src/main/java/com/skcraft/launcher/selfupdate/LatestVersionInfo.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.selfupdate; + +import lombok.Data; + +import java.net.URL; + +@Data +public class LatestVersionInfo { + + private String version; + private URL url; + +} diff --git a/src/main/java/com/skcraft/launcher/selfupdate/LauncherUpdateChecker.java b/src/main/java/com/skcraft/launcher/selfupdate/LauncherUpdateChecker.java new file mode 100644 index 0000000..4d617e8 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/selfupdate/LauncherUpdateChecker.java @@ -0,0 +1,59 @@ +/* + * 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 lombok.NonNull; +import lombok.extern.java.Log; + +import java.net.URL; +import java.util.concurrent.Callable; + +import static com.skcraft.launcher.util.SharedLocale._; + +@Log +public class LauncherUpdateChecker implements Callable { + + private final Launcher launcher; + + public LauncherUpdateChecker(@NonNull Launcher launcher) { + this.launcher = launcher; + } + + @Override + public URL call() throws Exception { + try { + LauncherUpdateChecker.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()); + + LauncherUpdateChecker.log.info("Latest version is " + latest + ", while current is " + current); + + if (latest.compareTo(current) >= 1) { + LauncherUpdateChecker.log.info("Update available at " + versionInfo.getUrl()); + return versionInfo.getUrl(); + } else { + LauncherUpdateChecker.log.info("No update required."); + return null; + } + } catch (Exception e) { + throw new LauncherException(e, _("errors.selfUpdateCheckError")); + } + } + +} diff --git a/src/main/java/com/skcraft/launcher/selfupdate/LauncherUpdater.java b/src/main/java/com/skcraft/launcher/selfupdate/LauncherUpdater.java new file mode 100644 index 0000000..a32cf2e --- /dev/null +++ b/src/main/java/com/skcraft/launcher/selfupdate/LauncherUpdater.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.selfupdate; + +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import com.skcraft.concurrency.ProgressObservable; +import com.skcraft.launcher.Launcher; +import com.skcraft.launcher.update.FileDownloader; +import com.skcraft.launcher.update.Installer; +import lombok.NonNull; + +import java.io.File; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.Executors; + +public class LauncherUpdater implements Callable, ProgressObservable { + + private final ListeningExecutorService executor = + MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()); + private final Launcher launcher; + private final URL url; + private Installer currentInstaller; + + public LauncherUpdater(@NonNull Launcher launcher, @NonNull URL url) { + this.launcher = launcher; + this.url = url; + } + + @Override + public File call() throws Exception { + try { + File dir = launcher.getLauncherBinariesDir(); + File finalPath = new File(dir, String.valueOf(System.currentTimeMillis()) + ".jar.pack"); + List paths = new ArrayList(); + paths.add(finalPath); + + Installer installer = new Installer(executor, launcher.getInstallerDir(), dir, dir); + currentInstaller = installer; + installer.submit(new FileDownloader(installer, url, paths)); + installer.awaitCompletion(); + + return finalPath; + } finally { + executor.shutdownNow(); + } + } + + @Override + public double getProgress() { + return -1; + } + + @Override + public String toString() { + Installer installer = currentInstaller; + if (installer != null) { + return installer.toString(); + } else { + return "..."; + } + } + +} diff --git a/src/main/java/com/skcraft/launcher/swing/ActionListeners.java b/src/main/java/com/skcraft/launcher/swing/ActionListeners.java new file mode 100644 index 0000000..807d3ae --- /dev/null +++ b/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/src/main/java/com/skcraft/launcher/swing/CheckboxTable.java b/src/main/java/com/skcraft/launcher/swing/CheckboxTable.java new file mode 100644 index 0000000..2ab18e1 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/swing/CheckboxTable.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.swing; + +import javax.swing.*; +import javax.swing.table.TableModel; +import java.awt.*; + +public class CheckboxTable extends JTable { + + public CheckboxTable() { + setShowGrid(false); + setRowHeight(getRowHeight() + 4); + setIntercellSpacing(new Dimension(0, 0)); + setFillsViewportHeight(true); + setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + } + + @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/src/main/java/com/skcraft/launcher/swing/DoubleClickToButtonAdapter.java b/src/main/java/com/skcraft/launcher/swing/DoubleClickToButtonAdapter.java new file mode 100644 index 0000000..5c6a841 --- /dev/null +++ b/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/src/main/java/com/skcraft/launcher/swing/FormPanel.java b/src/main/java/com/skcraft/launcher/swing/FormPanel.java new file mode 100644 index 0000000..5808227 --- /dev/null +++ b/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/src/main/java/com/skcraft/launcher/swing/HeaderPanel.java b/src/main/java/com/skcraft/launcher/swing/HeaderPanel.java new file mode 100644 index 0000000..da04fa3 --- /dev/null +++ b/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/src/main/java/com/skcraft/launcher/swing/InstanceTable.java b/src/main/java/com/skcraft/launcher/swing/InstanceTable.java new file mode 100644 index 0000000..7b85b0f --- /dev/null +++ b/src/main/java/com/skcraft/launcher/swing/InstanceTable.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.swing; + +import javax.swing.*; +import javax.swing.table.TableModel; +import java.awt.*; + +public class InstanceTable extends JTable { + + public InstanceTable() { + setShowGrid(false); + setRowHeight(Math.max(getRowHeight() + 4, 20 )); + setIntercellSpacing(new Dimension(0, 0)); + setFillsViewportHeight(true); + setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + } + + @Override + public void setModel(TableModel dataModel) { + super.setModel(dataModel); + try { + getColumnModel().getColumn(0).setMaxWidth(24); + } catch (ArrayIndexOutOfBoundsException e) { + } + } +} diff --git a/src/main/java/com/skcraft/launcher/swing/InstanceTableModel.java b/src/main/java/com/skcraft/launcher/swing/InstanceTableModel.java new file mode 100644 index 0000000..eb9bd41 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/swing/InstanceTableModel.java @@ -0,0 +1,121 @@ +/* + * 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 javax.swing.*; +import javax.swing.table.AbstractTableModel; +import java.awt.*; + +import static com.skcraft.launcher.util.SharedLocale._; + +public class InstanceTableModel extends AbstractTableModel { + + private final InstanceList instances; + private final ImageIcon instanceIcon; + private final ImageIcon downloadIcon; + + public InstanceTableModel(InstanceList instances) { + this.instances = instances; + instanceIcon = new ImageIcon(SwingHelper.readIconImage(Launcher.class, "instance_icon.png") + .getScaledInstance(16, 16, Image.SCALE_SMOOTH)); + downloadIcon = new ImageIcon(SwingHelper.readIconImage(Launcher.class, "download_icon.png") + .getScaledInstance(14, 14, Image.SCALE_SMOOTH)); + } + + public void update() { + instances.sort(); + fireTableDataChanged(); + } + + @Override + public String getColumnName(int columnIndex) { + switch (columnIndex) { + case 0: + return ""; + case 1: + return _("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 true; + 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) { + switch (columnIndex) { + case 0: + return instances.get(rowIndex).isLocal() ? instanceIcon : downloadIcon; + case 1: + Instance instance = instances.get(rowIndex); + return "" + SwingHelper.htmlEscape(instance.getTitle()) + getAddendum(instance) + ""; + default: + return null; + } + } + + private String getAddendum(Instance instance) { + if (!instance.isLocal()) { + return " " + _("launcher.notInstalledHint") + ""; + } else if (!instance.isInstalled()) { + return " " + _("launcher.requiresUpdateHint") + ""; + } else if (instance.isUpdatePending()) { + return " " + _("launcher.updatePendingHint") + ""; + } else { + return ""; + } + } + +} diff --git a/src/main/java/com/skcraft/launcher/swing/LinedBoxPanel.java b/src/main/java/com/skcraft/launcher/swing/LinedBoxPanel.java new file mode 100644 index 0000000..cb61e11 --- /dev/null +++ b/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/src/main/java/com/skcraft/launcher/swing/LinkButton.java b/src/main/java/com/skcraft/launcher/swing/LinkButton.java new file mode 100644 index 0000000..30c45fb --- /dev/null +++ b/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/src/main/java/com/skcraft/launcher/swing/MessageLog.java b/src/main/java/com/skcraft/launcher/swing/MessageLog.java new file mode 100644 index 0000000..d6d9817 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/swing/MessageLog.java @@ -0,0 +1,313 @@ +/* + * 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 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(String line, AttributeSet attributes) { + if (colorEnabled) { + if (line.startsWith("(!!)")) { + attributes = highlightedAttributes; + } + } + + try { + int offset = document.getLength(); + document.insertString(offset, line, + (attributes != null && colorEnabled) ? attributes : defaultAttributes); + textComponent.setCaretPosition(document.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 { + @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(record.getMessage() + "\n", attributes); + if (t != null) { + log(LauncherUtils.getStackTrace(t) + "\n", 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/src/main/java/com/skcraft/launcher/swing/ObjectSwingMapper.java b/src/main/java/com/skcraft/launcher/swing/ObjectSwingMapper.java new file mode 100644 index 0000000..6c4957d --- /dev/null +++ b/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/src/main/java/com/skcraft/launcher/swing/PopupMouseAdapter.java b/src/main/java/com/skcraft/launcher/swing/PopupMouseAdapter.java new file mode 100644 index 0000000..ea3f6cc --- /dev/null +++ b/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/src/main/java/com/skcraft/launcher/swing/SelectionKeeper.java b/src/main/java/com/skcraft/launcher/swing/SelectionKeeper.java new file mode 100644 index 0000000..dd777a6 --- /dev/null +++ b/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/src/main/java/com/skcraft/launcher/swing/SwingHelper.java b/src/main/java/com/skcraft/launcher/swing/SwingHelper.java new file mode 100644 index 0000000..dc072a1 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/swing/SwingHelper.java @@ -0,0 +1,372 @@ +/* + * 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.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +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.image.BufferedImage; +import java.io.*; +import java.lang.reflect.InvocationTargetException; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.concurrent.CancellationException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; + +import static com.skcraft.launcher.util.SharedLocale._; +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 SwingHelper() { + } + + public static String htmlEscape(String str) { + return str.replace(">", ">") + .replace("<", "<") + .replace("&", "&"); + } + + 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().browse(new URL("file://" + file.getAbsolutePath()).toURI()); + } catch (IOException e) { + JOptionPane.showMessageDialog(component, _("errors.openDirError", file.getAbsolutePath()), + _("errorTitle"), JOptionPane.ERROR_MESSAGE); + } catch (URISyntaxException 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(@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 { + Desktop.getDesktop().browse(url.toURI()); + } catch (IOException e) { + showErrorDialog(parentComponent, _("errors.openUrlError", url.toString()), _("errorTitle")); + } catch (URISyntaxException e) { + } + } + + /** + * 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(_("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 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(JFrame 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(); + } + }); + } + + 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 = t.getLocalizedMessage(); + if (message == null) { + message = _("errors.genericError"); + } + log.log(Level.WARNING, "Task failed", t); + SwingHelper.showErrorDialog(owner, message, _("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; + } +} diff --git a/src/main/java/com/skcraft/launcher/swing/TextFieldPopupMenu.java b/src/main/java/com/skcraft/launcher/swing/TextFieldPopupMenu.java new file mode 100644 index 0000000..07f6257 --- /dev/null +++ b/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 javax.swing.*; +import javax.swing.text.JTextComponent; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import static com.skcraft.launcher.util.SharedLocale._; + +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(_("context.cut"), 'T')); + copyItem = addMenuItem(new JMenuItem(_("context.copy"), 'C')); + pasteItem = addMenuItem(new JMenuItem(_("context.paste"), 'P')); + deleteItem = addMenuItem(new JMenuItem(_("context.delete"), 'D')); + addSeparator(); + selectAllItem = addMenuItem(new JMenuItem(_("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/src/main/java/com/skcraft/launcher/swing/WebpageLayoutManager.java b/src/main/java/com/skcraft/launcher/swing/WebpageLayoutManager.java new file mode 100644 index 0000000..74ee2de --- /dev/null +++ b/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/src/main/java/com/skcraft/launcher/swing/WebpagePanel.java b/src/main/java/com/skcraft/launcher/swing/WebpagePanel.java new file mode 100644 index 0000000..86d5231 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/swing/WebpagePanel.java @@ -0,0 +1,271 @@ +/* + * 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.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 JProgressBar progressBar; + private Thread thread; + + 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(); + } + } + + private void setDocument() { + activated = true; + + JLayeredPane panel = new JLayeredPane(); + panel.setLayout(new WebpageLayoutManager()); + + documentView = new JEditorPane(); + 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); + } + } + } + }); + + JScrollPane scrollPane = new JScrollPane(documentView); + panel.add(scrollPane, new Integer(1)); + scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS); + + progressBar = new JProgressBar(); + progressBar.setIndeterminate(true); + panel.add(progressBar, new Integer(2)); + + 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.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/src/main/java/com/skcraft/launcher/update/FileCopy.java b/src/main/java/com/skcraft/launcher/update/FileCopy.java new file mode 100644 index 0000000..5a81e36 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/update/FileCopy.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.update; + +import com.google.common.io.Files; +import lombok.Data; +import lombok.extern.java.Log; + +import java.io.File; +import java.io.IOException; +import java.util.logging.Level; + +@Data +@Log +public class FileCopy implements Runnable { + + public static final Object driveAccessLock = new Object(); + + private final File from; + private final File to; + + public FileCopy(File from, File to) { + this.from = from; + this.to = to; + } + + @Override + public void run() { + synchronized (driveAccessLock) { + log.log(Level.INFO, "Copying to {0} (from {1})...", new Object[]{to.getAbsoluteFile(), from.getName()}); + try { + to.getParentFile().mkdirs(); + Files.copy(from, to); + } catch (IOException e) { + throw new RuntimeException("Failed to copy to " + to, e); + } + } + } +} diff --git a/src/main/java/com/skcraft/launcher/update/FileDistribute.java b/src/main/java/com/skcraft/launcher/update/FileDistribute.java new file mode 100644 index 0000000..f14c6bc --- /dev/null +++ b/src/main/java/com/skcraft/launcher/update/FileDistribute.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.update; + +import com.google.common.io.Files; +import lombok.Data; +import lombok.extern.java.Log; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.logging.Level; + +@Data +@Log +public class FileDistribute implements Runnable { + + private final File from; + private final List to; + + public FileDistribute(File from, List to) { + this.from = from; + this.to = to; + } + + @Override + public void run() { + synchronized (FileCopy.driveAccessLock) { + try { + for (int i = 0; i < to.size(); i++) { + File dest = to.get(i); + dest.getParentFile().mkdirs(); + log.log(Level.INFO, "Copying to {0} (from {1})...", + new Object[]{dest.getAbsoluteFile(), from.getName()}); + if (i == to.size() - 1) { + dest.delete(); + from.renameTo(dest); + } else { + Files.copy(from, dest); + } + } + } catch (IOException e) { + throw new RuntimeException("Failed to copy to " + to, e); + } + } + } +} diff --git a/src/main/java/com/skcraft/launcher/update/FileDownloader.java b/src/main/java/com/skcraft/launcher/update/FileDownloader.java new file mode 100644 index 0000000..a297cb4 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/update/FileDownloader.java @@ -0,0 +1,42 @@ +/* + * 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 lombok.Getter; +import lombok.ToString; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.List; + +@ToString(exclude = "installer") +public class FileDownloader implements Runnable { + + private final Installer installer; + @Getter private final URL url; + @Getter private final List targets; + + public FileDownloader(Installer installer, URL url, List targets) { + this.installer = installer; + this.url = url; + this.targets = targets; + } + + @Override + public void run() { + try { + File sourceFile = installer.download(url, ""); + installer.submit(new FileDistribute(sourceFile, targets)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (IOException e) { + throw new RuntimeException("Failed to download " + url, e); + } + } + +} diff --git a/src/main/java/com/skcraft/launcher/update/GameUpdater.java b/src/main/java/com/skcraft/launcher/update/GameUpdater.java new file mode 100644 index 0000000..4d0654d --- /dev/null +++ b/src/main/java/com/skcraft/launcher/update/GameUpdater.java @@ -0,0 +1,120 @@ +/* + * 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.launcher.AssetsRoot; +import com.skcraft.launcher.Launcher; +import com.skcraft.launcher.model.minecraft.Asset; +import com.skcraft.launcher.model.minecraft.AssetsIndex; +import com.skcraft.launcher.model.minecraft.Library; +import com.skcraft.launcher.model.minecraft.VersionManifest; +import com.skcraft.launcher.util.Environment; +import com.skcraft.launcher.util.HttpRequest; +import lombok.NonNull; +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.Map; + +import static com.skcraft.launcher.LauncherUtils.checkInterrupted; +import static com.skcraft.launcher.util.HttpRequest.url; + +@Log +public class GameUpdater implements Runnable { + + private final Installer installer; + private final Launcher launcher; + private final VersionManifest versionManifest; + private final URL librariesBaseURL; + private Environment environment = Environment.getInstance(); + + public GameUpdater(@NonNull Installer installer, + @NonNull Launcher launcher, + @NonNull VersionManifest versionManifest, URL librariesBaseURL) { + this.installer = installer; + this.launcher = launcher; + this.versionManifest = versionManifest; + this.librariesBaseURL = librariesBaseURL; + } + + @Override + public void run() { + try { + File librariesDir = launcher.getLibrariesDir(); + AssetsRoot assetsRoot = launcher.getAssets(); + File jarPath = launcher.getJarPath(versionManifest); + + URL jarURL = url(String.format( + launcher.getProperties().getProperty("jarUrl"), versionManifest.getId())); + URL assetsIndexURL = url(String.format( + launcher.getProperties().getProperty("assetsIndexUrl"), versionManifest.getAssetsIndex())); + + // If the JAR does not exist, install it + if (!jarPath.exists()) { + List targets = new ArrayList(); + targets.add(jarPath); + installer.submit(new FileDownloader(installer, jarURL, targets)); + } + + // Install libraries + for (Library library : versionManifest.getLibraries()) { + if (library.matches(environment)) { + URL url = library.getURL(launcher, environment, librariesBaseURL); + File file = new File(librariesDir, library.getPath(environment)); + + if (!file.exists()) { + List targets = new ArrayList(); + targets.add(file); + installer.submit(new FileDownloader(installer, url, targets)); + } + + checkInterrupted(); + } + } + + // Install assets + AssetsIndex index = HttpRequest + .get(assetsIndexURL) + .execute() + .expectResponseCode(200) + .returnContent() + .saveContent(assetsRoot.getIndexPath(versionManifest.getId())) + .asJson(AssetsIndex.class); + + for (Map.Entry entry : index.getObjects().entrySet()) { + String hash = entry.getValue().getHash(); + URL url = url(String.format( + launcher.getProperties().getProperty("assetUrl"), hash.subSequence(0, 2), hash)); + File path = assetsRoot.getObjectPath(entry.getValue()); + + checkInterrupted(); + + if (!path.exists()) { + List targets = new ArrayList(); + targets.add(path); + installer.submit(new FileDownloader(installer, url, targets)); + } + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (IOException e) { + throw new RuntimeException("Failed to get resources", e); + } + } + + @Override + public String toString() { + return "GameUpdater{" + + "versionManifest.id=" + versionManifest.getId() + + ", environment=" + environment + + '}'; + } +} diff --git a/src/main/java/com/skcraft/launcher/update/InstallLog.java b/src/main/java/com/skcraft/launcher/update/InstallLog.java new file mode 100644 index 0000000..b598e71 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/update/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.update; + +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 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 void add(@NonNull File group, @NonNull File entry) { + add(relativize(group), relativize(entry)); + } + + public boolean has(@NonNull String entry) { + return cache.contains(entry); + } + + public boolean has(@NonNull File entry) { + return has(relativize(entry)); + } + + public 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 boolean copyGroupFrom(@NonNull InstallLog other, @NonNull File entry) { + return copyGroupFrom(other, relativize(entry)); + } + + @JsonIgnore + public Set>> getEntrySet() { + return entries.entrySet(); + } + + public 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/src/main/java/com/skcraft/launcher/update/Installer.java b/src/main/java/com/skcraft/launcher/update/Installer.java new file mode 100644 index 0000000..0b1521b --- /dev/null +++ b/src/main/java/com/skcraft/launcher/update/Installer.java @@ -0,0 +1,247 @@ +/* + * 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.google.common.base.Charsets; +import com.google.common.hash.HashFunction; +import com.google.common.hash.Hashing; +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.launcher.util.HttpRequest; +import com.skcraft.launcher.persistence.Persistence; +import lombok.Getter; +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.logging.Level; + +import static com.google.common.util.concurrent.MoreExecutors.sameThreadExecutor; +import static com.skcraft.launcher.LauncherUtils.checkInterrupted; + +@Log +public class Installer { + + private final ObjectMapper mapper = new ObjectMapper(); + private final HashFunction hf = Hashing.sha1(); + private final ListeningExecutorService executor; + @Getter private final File temporaryDir; + @Getter private final File dataFilesDir; + @Getter private final File destinationDir; + @Getter private final InstallLog currentLog = new InstallLog(); + @Getter private final InstallLog previousLog; + @Getter private final UpdateCache updateCache; + @Getter @Setter private int downloadTries = 5; + @Getter @Setter private int tryDelay = 3000; + private final List httpRequests = new ArrayList(); + private final Set usedHashes = new HashSet(); + private final File installLogPath; + private final File updateCachePath; + private final Set running = new HashSet(); + private final ErrorHandler errorHandler = new ErrorHandler(); + private Throwable throwable; + + public Installer(ListeningExecutorService executor, File temporaryDir, File dataFilesDir, File destinationDir) { + this.executor = executor; + this.temporaryDir = temporaryDir; + this.dataFilesDir = dataFilesDir; + this.destinationDir = destinationDir; + + installLogPath = new File(dataFilesDir, "install_log.json"); + updateCachePath = new File(dataFilesDir, "update_cache.json"); + + this.previousLog = Persistence.read(installLogPath, InstallLog.class); + this.updateCache = Persistence.read(updateCachePath, UpdateCache.class); + } + + public File download(URL url, String version) throws IOException, InterruptedException { + String baseId = hf.newHasher() + .putString(url.toString(), Charsets.UTF_8) + .putString(version, Charsets.UTF_8) + .hash() + .toString(); + String id = baseId; + int index = 0; + + while (usedHashes.contains(id)) { + id = baseId + "_" + (index++); + } + + File dir = new File(temporaryDir, id.charAt(0) + File.separator + id.charAt(1)); + dir.mkdirs(); + File downloadPath = new File(dir, id + ".filepart"); + File tempPath = new File(dir, id + ".filedownload"); + + if (tempPath.exists()) { + log.log(Level.INFO, "Using existing {0} for {1}...", new Object[]{tempPath, url}); + return tempPath; + } else { + log.log(Level.INFO, "Downloading {0} to {1}...", new Object[]{url, downloadPath}); + + int trial = 0; + while (true) { + HttpRequest request = HttpRequest.get(url); + try { + synchronized (this) { + httpRequests.add(request); + } + request.execute() + .expectResponseCode(200) + .saveContent(downloadPath); + break; + } catch (IOException e) { + if (++trial >= downloadTries) { + throw e; + } + + log.log(Level.WARNING, String.format("Download of %s failed; retrying in %d ms", url, tryDelay), e); + Thread.sleep(tryDelay); + } finally { + synchronized (this) { + httpRequests.remove(request); + } + } + } + + downloadPath.renameTo(tempPath); + return tempPath; + } + } + + public synchronized ListenableFuture submit(Runnable runnable) { + running.add(runnable); + ListenableFuture future = executor.submit(runnable); + Futures.addCallback(future, errorHandler); + future.addListener(new RemoveRunnable(runnable), sameThreadExecutor()); + return future; + } + + public synchronized void submitAll(List tasks) { + for (Runnable runnable : tasks) { + submit(runnable); + } + } + + private synchronized void failAll(Throwable throwable) { + this.throwable = throwable; + running.clear(); + executor.shutdownNow(); + notifyAll(); + } + + public void awaitCompletion() throws ExecutionException, InterruptedException { + try { + synchronized (this) { + while (running.size() > 0) { + wait(); + } + } + + if (throwable != null) { + throw new ExecutionException(throwable); + } + } catch (InterruptedException e) { + executor.shutdownNow(); + throw new InterruptedException(); + } + } + + public void commit() throws IOException, InterruptedException { + deleteOldFiles(); + writeCache(); + } + + protected void deleteOldFiles() throws InterruptedException { + for (Map.Entry> entry : previousLog.getEntrySet()) { + for (String path : entry.getValue()) { + checkInterrupted(); + if (!currentLog.has(path)) { + new File(getDestinationDir(), path).delete(); + } + } + } + } + + protected void writeCache() throws IOException { + Persistence.write(installLogPath, currentLog); + Persistence.write(updateCachePath, updateCache); + } + + private class RemoveRunnable implements Runnable { + private final Runnable runnable; + + public RemoveRunnable(Runnable runnable) { + this.runnable = runnable; + } + + @Override + public void run() { + synchronized (Installer.this) { + running.remove(runnable); + if (running.isEmpty()) { + Installer.this.notifyAll(); + } + } + } + } + + private class ErrorHandler implements FutureCallback { + @Override + public void onSuccess(Object result) { + } + + @Override + public void onFailure(Throwable t) { + if (t instanceof InterruptedException) { + return; + } + log.log(Level.WARNING, "Failed install stage", t); + failAll(t); + } + } + + + @Override + public synchronized String toString() { + StringBuilder builder = new StringBuilder(); + + if (httpRequests.size() > 0) { + builder.append("Downloads:\n"); + for (HttpRequest request : httpRequests) { + builder.append("- "); + builder.append(request.getUrl()); + builder.append(" ("); + double progress = request.getProgress(); + if (progress >= 0) { + builder.append(Math.round(request.getProgress() * 100.0 * 100.0) / 100.0); + builder.append("%)"); + } else { + builder.append("pending)"); + } + builder.append("\n"); + } + builder.append("\n"); + + } + + builder.append("Tasks:\n"); + for (Runnable runnable : running) { + builder.append("- "); + builder.append(runnable.toString()); + builder.append("\n"); + } + return builder.toString(); + } + +} diff --git a/src/main/java/com/skcraft/launcher/update/InstanceDeleter.java b/src/main/java/com/skcraft/launcher/update/InstanceDeleter.java new file mode 100644 index 0000000..e3e091c --- /dev/null +++ b/src/main/java/com/skcraft/launcher/update/InstanceDeleter.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.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.IOException; +import java.util.concurrent.Callable; + +import static com.skcraft.launcher.LauncherUtils.checkInterrupted; + +public class InstanceDeleter implements Callable, ProgressObservable { + + private final Instance instance; + + public InstanceDeleter(@NonNull Instance instance) { + this.instance = instance; + } + + @Override + public double getProgress() { + return -1; + } + + @Override + public Instance call() throws Exception { + instance.setInstalled(false); + instance.setUpdatePending(true); + Persistence.commitAndForget(instance); + + checkInterrupted(); + + Thread.sleep(2000); + + try { + LauncherUtils.interruptibleDelete(instance.getDir()); + } catch (IOException e) { + Thread.sleep(1000); + LauncherUtils.interruptibleDelete(instance.getDir()); + } + + return instance; + } + +} diff --git a/src/main/java/com/skcraft/launcher/update/InstanceResetter.java b/src/main/java/com/skcraft/launcher/update/InstanceResetter.java new file mode 100644 index 0000000..e9301a0 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/update/InstanceResetter.java @@ -0,0 +1,67 @@ +/* + * 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.concurrent.Callable; + +public class InstanceResetter implements Callable, ProgressObservable { + + private final Instance instance; + private File currentDir; + + public InstanceResetter(@NonNull Instance instance) { + this.instance = instance; + } + + @Override + public double getProgress() { + return -1; + } + + @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); + } + } finally { + currentDir = null; + } + } + + public String toString() { + File dir = currentDir; + if (dir != null) { + return "Removing " + dir.getAbsolutePath(); + } else { + return "Working..."; + } + } + +} diff --git a/src/main/java/com/skcraft/launcher/update/InstanceUpdater.java b/src/main/java/com/skcraft/launcher/update/InstanceUpdater.java new file mode 100644 index 0000000..d749858 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/update/InstanceUpdater.java @@ -0,0 +1,151 @@ +/* + * 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.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import com.skcraft.concurrency.ProgressObservable; +import com.skcraft.launcher.Instance; +import com.skcraft.launcher.Launcher; +import com.skcraft.launcher.model.minecraft.VersionManifest; +import com.skcraft.launcher.model.modpack.Manifest; +import com.skcraft.launcher.util.HttpRequest; +import com.skcraft.launcher.persistence.Persistence; +import lombok.NonNull; +import lombok.extern.java.Log; + +import java.io.IOException; +import java.net.URL; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.logging.Level; + +import static com.skcraft.launcher.util.HttpRequest.url; + +@Log +public class InstanceUpdater implements Callable, ProgressObservable { + + private final Launcher launcher; + private final Instance instance; + private final ListeningExecutorService executor = + MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(6)); + private Installer currentInstaller; + + public InstanceUpdater(@NonNull Launcher launcher, @NonNull Instance instance) { + this.launcher = launcher; + this.instance = instance; + } + + private URL getVersionManifestURL(String version) { + return url(String.format(launcher.getProperties().getProperty("versionManifestUrl"), version)); + } + + @Override + public Instance call() throws Exception { + try { + log.info("Checking for an update for '" + instance.getName() + "'..."); + if (instance.getManifestURL() == null) { + log.log(Level.INFO, + "No URL set for {0}, so it can't be updated (the modpack may be removed from the server)", + new Object[] { instance }); + } else if (instance.isUpdatePending() || !instance.isInstalled()) { + log.log(Level.INFO, "Updating {0}...", new Object[]{instance}); + update(instance); + } else { + log.log(Level.INFO, "No update found for {0}.", new Object[] { instance }); + } + + return instance; + } finally { + executor.shutdownNow(); + } + } + + private void update(Instance instance) throws IOException, InterruptedException, ExecutionException { + try { + instance.setLocal(true); + Persistence.commitAndForget(instance); + + Installer installer = new Installer(executor, + launcher.getInstallerDir(), + instance.getDir(), + instance.getContentDir()); + currentInstaller = installer; + + Manifest manifest = HttpRequest + .get(instance.getManifestURL()) + .execute() + .expectResponseCode(200) + .returnContent() + .saveContent(instance.getManifestPath()) + .asJson(Manifest.class); + if (manifest.getBaseUrl() == null) { + manifest.setBaseUrl(instance.getManifestURL()); + } + manifest.setInstaller(installer); + + installer.submitAll(manifest.getTasks()); + installer.awaitCompletion(); + installer.commit(); + + URL url = getVersionManifestURL(manifest.getGameVersion()); + log.log(Level.INFO, instance.getName() + ": Fetching version manifest from " + url + "..."); + + VersionManifest versionManifest = manifest.getVersionManifest(); + + if (versionManifest != null) { + Persistence.write(instance.getVersionManifestPath(), versionManifest); + } else { + // The manifest doesn't come with its own version manifest, so let's download the one for the given + // version of Minecraft + versionManifest = HttpRequest + .get(url) + .execute() + .expectResponseCode(200) + .returnContent() + .saveContent(instance.getVersionManifestPath()) + .asJson(VersionManifest.class); + } + + installer = new Installer(executor, + launcher.getInstallerDir(), + launcher.getCommonDataDir(), + launcher.getCommonDataDir()); + currentInstaller = installer; + + log.log(Level.INFO, instance.getName() + ": Enumerating common data files..."); + installer.submit(new GameUpdater(installer, launcher, versionManifest, manifest.getLibrariesURL())); + installer.awaitCompletion(); + + 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() + "."); + } finally { + currentInstaller = null; + } + } + + @Override + public double getProgress() { + return -1; + } + + @Override + public String toString() { + Installer installer = currentInstaller; + if (installer != null) { + return installer.toString(); + } else { + return "..."; + } + } +} diff --git a/src/main/java/com/skcraft/launcher/update/UpdateCache.java b/src/main/java/com/skcraft/launcher/update/UpdateCache.java new file mode 100644 index 0000000..a2dc629 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/update/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.update; + +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/src/main/java/com/skcraft/launcher/update/ZipExtract.java b/src/main/java/com/skcraft/launcher/update/ZipExtract.java new file mode 100644 index 0000000..7a54c77 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/update/ZipExtract.java @@ -0,0 +1,102 @@ +/* + * 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.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()); + 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/src/main/java/com/skcraft/launcher/util/Environment.java b/src/main/java/com/skcraft/launcher/util/Environment.java new file mode 100644 index 0000000..828f827 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/util/Environment.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.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; + + return Platform.UNKNOWN; + } + +} diff --git a/src/main/java/com/skcraft/launcher/util/HttpRequest.java b/src/main/java/com/skcraft/launcher/util/HttpRequest.java new file mode 100644 index 0000000..84a2193 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/util/HttpRequest.java @@ -0,0 +1,516 @@ +/* + * 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.databind.ObjectMapper; +import com.skcraft.concurrency.ProgressObservable; +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.*; +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 final String method; + @Getter + private final URL url; + private String contentType; + private byte[] body; + private HttpURLConnection conn; + private InputStream inputStream; + + 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 = (HttpURLConnection) reformat(url).openConnection(); + + 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(); + } + + inputStream = conn.getResponseCode() == HttpURLConnection.HTTP_OK ? + conn.getInputStream() : conn.getErrorStream(); + + successful = true; + } finally { + if (!successful) { + close(); + } + } + + return this; + } + + /** + * 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); + } + } + + /** + * URL may contain spaces and other nasties that will cause a failure. + * + * @param existing the existing URL to transform + * @return the new URL, or old one if there was a failure + */ + private static URL reformat(URL existing) { + try { + URL url = new URL(existing.toString()); + URI uri = new URI( + url.getProtocol(), url.getUserInfo(), url.getHost(), url.getPort(), + url.getPath(), url.getQuery(), url.getRef()); + url = uri.toURL(); + return url; + } catch (MalformedURLException e) { + return existing; + } catch (URISyntaxException e) { + return existing; + } + } + + /** + * 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. + * + * @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 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/src/main/java/com/skcraft/launcher/util/LimitLinesDocumentListener.java b/src/main/java/com/skcraft/launcher/util/LimitLinesDocumentListener.java new file mode 100644 index 0000000..92da7b1 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/util/LimitLinesDocumentListener.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 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; + + /** + * 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; + } + + /** + * 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 + + 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. + + Document document = e.getDocument(); + Element root = document.getDefaultRootElement(); + + while (root.getElementCount() > maximumLines) { + if (isRemoveFromStart) { + removeFromStart(document, root); + } else { + removeFromEnd(document, root); + } + } + } + + private void removeFromStart(Document document, Element root) { + Element line = root.getElement(0); + 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(root.getElementCount() - 1); + int start = line.getStartOffset(); + int end = line.getEndOffset(); + + try { + document.remove(start - 1, end - start); + } catch (BadLocationException ble) { + System.out.println(ble); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/skcraft/launcher/util/PastebinPoster.java b/src/main/java/com/skcraft/launcher/util/PastebinPoster.java new file mode 100644 index 0000000..53a9666 --- /dev/null +++ b/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("http://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/src/main/java/com/skcraft/launcher/util/Platform.java b/src/main/java/com/skcraft/launcher/util/Platform.java new file mode 100644 index 0000000..a338351 --- /dev/null +++ b/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/src/main/java/com/skcraft/launcher/util/SharedLocale.java b/src/main/java/com/skcraft/launcher/util/SharedLocale.java new file mode 100644 index 0000000..93df77a --- /dev/null +++ b/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 _(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 _(String key, Object... args) { + if (bundle != null) { + try { + MessageFormat formatter = new MessageFormat(_(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/src/main/java/com/skcraft/launcher/util/SimpleLogFormatter.java b/src/main/java/com/skcraft/launcher/util/SimpleLogFormatter.java new file mode 100644 index 0000000..4cd9cc4 --- /dev/null +++ b/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/src/main/java/com/skcraft/launcher/util/SwingExecutor.java b/src/main/java/com/skcraft/launcher/util/SwingExecutor.java new file mode 100644 index 0000000..d3e24ec --- /dev/null +++ b/src/main/java/com/skcraft/launcher/util/SwingExecutor.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 javax.swing.*; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.*; + +public final class SwingExecutor extends AbstractExecutorService { + + public static final SwingExecutor INSTANCE = new SwingExecutor(); + + private SwingExecutor() { + } + + @Override + public void execute(Runnable runnable) { + SwingUtilities.invokeLater(runnable); + } + + @Override + protected RunnableFuture newTaskFor(final Callable callable) { + return new FutureTask(callable) { + @Override + public void run() { + try { + super.run(); + } catch (Throwable e) { + setException(e); + } + } + }; + } + + @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; + } +} \ No newline at end of file diff --git a/src/main/java/com/skcraft/launcher/util/WinRegistry.java b/src/main/java/com/skcraft/launcher/util/WinRegistry.java new file mode 100644 index 0000000..9e51983 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/util/WinRegistry.java @@ -0,0 +1,371 @@ +/* + * 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.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.prefs.Preferences; + +public class WinRegistry { + public static final int HKEY_CURRENT_USER = 0x80000001; + public static final int HKEY_LOCAL_MACHINE = 0x80000002; + public static final int REG_SUCCESS = 0; + public static final int REG_NOTFOUND = 2; + public static final int REG_ACCESSDENIED = 5; + + private static final int KEY_ALL_ACCESS = 0xf003f; + private static final int KEY_READ = 0x20019; + private static Preferences userRoot = Preferences.userRoot(); + private static Preferences systemRoot = Preferences.systemRoot(); + private static Class userClass = userRoot.getClass(); + private static Method regOpenKey = null; + private static Method regCloseKey = null; + private static Method regQueryValueEx = null; + private static Method regEnumValue = null; + private static Method regQueryInfoKey = null; + private static Method regEnumKeyEx = null; + private static Method regCreateKeyEx = null; + private static Method regSetValueEx = null; + private static Method regDeleteKey = null; + private static Method regDeleteValue = null; + + static { + try { + regOpenKey = userClass.getDeclaredMethod("WindowsRegOpenKey", + new Class[] { int.class, byte[].class, int.class }); + regOpenKey.setAccessible(true); + regCloseKey = userClass.getDeclaredMethod("WindowsRegCloseKey", + new Class[] { int.class }); + regCloseKey.setAccessible(true); + regQueryValueEx = userClass.getDeclaredMethod( + "WindowsRegQueryValueEx", new Class[] { int.class, + byte[].class }); + regQueryValueEx.setAccessible(true); + regEnumValue = userClass.getDeclaredMethod("WindowsRegEnumValue", + new Class[] { int.class, int.class, int.class }); + regEnumValue.setAccessible(true); + regQueryInfoKey = userClass.getDeclaredMethod( + "WindowsRegQueryInfoKey1", new Class[] { int.class }); + regQueryInfoKey.setAccessible(true); + regEnumKeyEx = userClass.getDeclaredMethod("WindowsRegEnumKeyEx", + new Class[] { int.class, int.class, int.class }); + regEnumKeyEx.setAccessible(true); + regCreateKeyEx = userClass.getDeclaredMethod( + "WindowsRegCreateKeyEx", new Class[] { int.class, + byte[].class }); + regCreateKeyEx.setAccessible(true); + regSetValueEx = userClass.getDeclaredMethod("WindowsRegSetValueEx", + new Class[] { int.class, byte[].class, byte[].class }); + regSetValueEx.setAccessible(true); + regDeleteValue = userClass.getDeclaredMethod( + "WindowsRegDeleteValue", new Class[] { int.class, + byte[].class }); + regDeleteValue.setAccessible(true); + regDeleteKey = userClass.getDeclaredMethod("WindowsRegDeleteKey", + new Class[] { int.class, byte[].class }); + regDeleteKey.setAccessible(true); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private WinRegistry() { + } + + /** + * Read a value from key and value name + * + * @param hkey + * HKEY_CURRENT_USER/HKEY_LOCAL_MACHINE + * @param key + * @param valueName + * @return the value + * @throws IllegalArgumentException + * @throws IllegalAccessException + * @throws java.lang.reflect.InvocationTargetException + */ + public static String readString(int hkey, String key, String valueName) + throws IllegalArgumentException, IllegalAccessException, + InvocationTargetException { + if (hkey == HKEY_LOCAL_MACHINE) { + return readString(systemRoot, hkey, key, valueName); + } else if (hkey == HKEY_CURRENT_USER) { + return readString(userRoot, hkey, key, valueName); + } else { + throw new IllegalArgumentException("hkey=" + hkey); + } + } + + /** + * Read value(s) and value name(s) form given key + * + * @param hkey + * HKEY_CURRENT_USER/HKEY_LOCAL_MACHINE + * @param key + * @return the value name(s) plus the value(s) + * @throws IllegalArgumentException + * @throws IllegalAccessException + * @throws java.lang.reflect.InvocationTargetException + */ + public static Map readStringValues(int hkey, String key) + throws IllegalArgumentException, IllegalAccessException, + InvocationTargetException { + if (hkey == HKEY_LOCAL_MACHINE) { + return readStringValues(systemRoot, hkey, key); + } else if (hkey == HKEY_CURRENT_USER) { + return readStringValues(userRoot, hkey, key); + } else { + throw new IllegalArgumentException("hkey=" + hkey); + } + } + + /** + * Read the value name(s) from a given key + * + * @param hkey + * HKEY_CURRENT_USER/HKEY_LOCAL_MACHINE + * @param key + * @return the value name(s) + * @throws IllegalArgumentException + * @throws IllegalAccessException + * @throws java.lang.reflect.InvocationTargetException + */ + public static List readStringSubKeys(int hkey, String key) + throws IllegalArgumentException, IllegalAccessException, + InvocationTargetException { + if (hkey == HKEY_LOCAL_MACHINE) { + return readStringSubKeys(systemRoot, hkey, key); + } else if (hkey == HKEY_CURRENT_USER) { + return readStringSubKeys(userRoot, hkey, key); + } else { + throw new IllegalArgumentException("hkey=" + hkey); + } + } + + /** + * Create a key + * + * @param hkey + * HKEY_CURRENT_USER/HKEY_LOCAL_MACHINE + * @param key + * @throws IllegalArgumentException + * @throws IllegalAccessException + * @throws java.lang.reflect.InvocationTargetException + */ + public static void createKey(int hkey, String key) + throws IllegalArgumentException, IllegalAccessException, + InvocationTargetException { + int[] ret; + if (hkey == HKEY_LOCAL_MACHINE) { + ret = createKey(systemRoot, hkey, key); + regCloseKey + .invoke(systemRoot, new Object[] { new Integer(ret[0]) }); + } else if (hkey == HKEY_CURRENT_USER) { + ret = createKey(userRoot, hkey, key); + regCloseKey.invoke(userRoot, new Object[] { new Integer(ret[0]) }); + } else { + throw new IllegalArgumentException("hkey=" + hkey); + } + if (ret[1] != REG_SUCCESS) { + throw new IllegalArgumentException("rc=" + ret[1] + " key=" + key); + } + } + + /** + * Write a value in a given key/value name + * + * @param hkey + * @param key + * @param valueName + * @param value + * @throws IllegalArgumentException + * @throws IllegalAccessException + * @throws java.lang.reflect.InvocationTargetException + */ + public static void writeStringValue(int hkey, String key, String valueName, + String value) throws IllegalArgumentException, + IllegalAccessException, InvocationTargetException { + if (hkey == HKEY_LOCAL_MACHINE) { + writeStringValue(systemRoot, hkey, key, valueName, value); + } else if (hkey == HKEY_CURRENT_USER) { + writeStringValue(userRoot, hkey, key, valueName, value); + } else { + throw new IllegalArgumentException("hkey=" + hkey); + } + } + + /** + * Delete a given key + * + * @param hkey + * @param key + * @throws IllegalArgumentException + * @throws IllegalAccessException + * @throws java.lang.reflect.InvocationTargetException + */ + public static void deleteKey(int hkey, String key) + throws IllegalArgumentException, IllegalAccessException, + InvocationTargetException { + int rc = -1; + if (hkey == HKEY_LOCAL_MACHINE) { + rc = deleteKey(systemRoot, hkey, key); + } else if (hkey == HKEY_CURRENT_USER) { + rc = deleteKey(userRoot, hkey, key); + } + if (rc != REG_SUCCESS) { + throw new IllegalArgumentException("rc=" + rc + " key=" + key); + } + } + + /** + * delete a value from a given key/value name + * + * @param hkey + * @param key + * @param value + * @throws IllegalArgumentException + * @throws IllegalAccessException + * @throws java.lang.reflect.InvocationTargetException + */ + public static void deleteValue(int hkey, String key, String value) + throws IllegalArgumentException, IllegalAccessException, + InvocationTargetException { + int rc = -1; + if (hkey == HKEY_LOCAL_MACHINE) { + rc = deleteValue(systemRoot, hkey, key, value); + } else if (hkey == HKEY_CURRENT_USER) { + rc = deleteValue(userRoot, hkey, key, value); + } + if (rc != REG_SUCCESS) { + throw new IllegalArgumentException("rc=" + rc + " key=" + key + + " value=" + value); + } + } + + // ===================== + + private static int deleteValue(Preferences root, int hkey, String key, + String value) throws IllegalArgumentException, + IllegalAccessException, InvocationTargetException { + int[] handles = (int[]) regOpenKey.invoke(root, new Object[] { + new Integer(hkey), toCstr(key), new Integer(KEY_ALL_ACCESS) }); + if (handles[1] != REG_SUCCESS) { + return handles[1]; // can be REG_NOTFOUND, REG_ACCESSDENIED + } + int rc = ((Integer) regDeleteValue.invoke(root, new Object[] { + new Integer(handles[0]), toCstr(value) })).intValue(); + regCloseKey.invoke(root, new Object[] { new Integer(handles[0]) }); + return rc; + } + + private static int deleteKey(Preferences root, int hkey, String key) + throws IllegalArgumentException, IllegalAccessException, + InvocationTargetException { + int rc = ((Integer) regDeleteKey.invoke(root, new Object[] { + new Integer(hkey), toCstr(key) })).intValue(); + return rc; // can REG_NOTFOUND, REG_ACCESSDENIED, REG_SUCCESS + } + + private static String readString(Preferences root, int hkey, String key, + String value) throws IllegalArgumentException, + IllegalAccessException, InvocationTargetException { + int[] handles = (int[]) regOpenKey.invoke(root, new Object[] { + new Integer(hkey), toCstr(key), new Integer(KEY_READ) }); + if (handles[1] != REG_SUCCESS) { + return null; + } + byte[] valb = (byte[]) regQueryValueEx.invoke(root, new Object[] { + new Integer(handles[0]), toCstr(value) }); + regCloseKey.invoke(root, new Object[] { new Integer(handles[0]) }); + return (valb != null ? new String(valb).trim() : null); + } + + private static Map readStringValues(Preferences root, + int hkey, String key) throws IllegalArgumentException, + IllegalAccessException, InvocationTargetException { + HashMap results = new HashMap(); + int[] handles = (int[]) regOpenKey.invoke(root, new Object[] { + new Integer(hkey), toCstr(key), new Integer(KEY_READ) }); + if (handles[1] != REG_SUCCESS) { + return null; + } + int[] info = (int[]) regQueryInfoKey.invoke(root, + new Object[] { new Integer(handles[0]) }); + + int count = info[0]; // count + int maxlen = info[3]; // value length max + for (int index = 0; index < count; index++) { + byte[] name = (byte[]) regEnumValue.invoke(root, new Object[] { + new Integer(handles[0]), new Integer(index), + new Integer(maxlen + 1) }); + String value = readString(hkey, key, new String(name)); + results.put(new String(name).trim(), value); + } + regCloseKey.invoke(root, new Object[] { new Integer(handles[0]) }); + return results; + } + + private static List readStringSubKeys(Preferences root, int hkey, + String key) throws IllegalArgumentException, + IllegalAccessException, InvocationTargetException { + List results = new ArrayList(); + int[] handles = (int[]) regOpenKey.invoke(root, new Object[] { + new Integer(hkey), toCstr(key), new Integer(KEY_READ) }); + if (handles[1] != REG_SUCCESS) { + return null; + } + int[] info = (int[]) regQueryInfoKey.invoke(root, + new Object[] { new Integer(handles[0]) }); + + int count = info[0]; // Fix: info[2] was being used here with wrong + // results. Suggested by davenpcj, confirmed by + // Petrucio + int maxlen = info[3]; // value length max + for (int index = 0; index < count; index++) { + byte[] name = (byte[]) regEnumKeyEx.invoke(root, new Object[] { + new Integer(handles[0]), new Integer(index), + new Integer(maxlen + 1) }); + results.add(new String(name).trim()); + } + regCloseKey.invoke(root, new Object[] { new Integer(handles[0]) }); + return results; + } + + private static int[] createKey(Preferences root, int hkey, String key) + throws IllegalArgumentException, IllegalAccessException, + InvocationTargetException { + return (int[]) regCreateKeyEx.invoke(root, new Object[] { + new Integer(hkey), toCstr(key) }); + } + + private static void writeStringValue(Preferences root, int hkey, + String key, String valueName, String value) + throws IllegalArgumentException, IllegalAccessException, + InvocationTargetException { + int[] handles = (int[]) regOpenKey.invoke(root, new Object[] { + new Integer(hkey), toCstr(key), new Integer(KEY_ALL_ACCESS) }); + + regSetValueEx.invoke(root, new Object[] { new Integer(handles[0]), + toCstr(valueName), toCstr(value) }); + regCloseKey.invoke(root, new Object[] { new Integer(handles[0]) }); + } + + // utility + private static byte[] toCstr(String str) { + byte[] result = new byte[str.length() + 1]; + + for (int i = 0; i < str.length(); i++) { + result[i] = (byte) str.charAt(i); + } + result[str.length()] = 0; + return result; + } +} \ No newline at end of file diff --git a/src/main/resources/com/skcraft/launcher/download_icon.png b/src/main/resources/com/skcraft/launcher/download_icon.png new file mode 100644 index 0000000..7a76bcb --- /dev/null +++ b/src/main/resources/com/skcraft/launcher/download_icon.png Binary files differ diff --git a/src/main/resources/com/skcraft/launcher/icon.png b/src/main/resources/com/skcraft/launcher/icon.png new file mode 100644 index 0000000..8c74095 --- /dev/null +++ b/src/main/resources/com/skcraft/launcher/icon.png Binary files differ diff --git a/src/main/resources/com/skcraft/launcher/instance_icon.png b/src/main/resources/com/skcraft/launcher/instance_icon.png new file mode 100644 index 0000000..8c74095 --- /dev/null +++ b/src/main/resources/com/skcraft/launcher/instance_icon.png Binary files differ diff --git a/src/main/resources/com/skcraft/launcher/lang/Launcher.properties b/src/main/resources/com/skcraft/launcher/lang/Launcher.properties new file mode 100644 index 0000000..3643a90 --- /dev/null +++ b/src/main/resources/com/skcraft/launcher/lang/Launcher.properties @@ -0,0 +1,121 @@ +# +# 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 checking for updates. See skcraft.com for more information. +errors.selfUpdateCheckError=Checking for an update to the launcher has failed. + +button.cancel=Cancel +button.ok=OK + +options.title = Options +options.useProxyCheck = Use following proxy in Minecraft +options.jvmPath=JVM path\: +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 + +instance.openFolder=Open folder +instance.openSaves=Open saves folder +instance.openResourcePacks=Open resource packs folder +instance.openScreenshots=Open screenshots folder +instance.copyAsPath=Copy as path +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 (screnshots, 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}''... + +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.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.modpackColumn=Modpack +launcher.notInstalledHint=(not installed) +launcher.requiresUpdateHint=(requires update) +launcher.updatePendingHint=(pending update) + +login.rememberId=Remember my account in the list +login.rememberPassword=Remember my password +login.login=Login... +login.recoverAccount=Forgot your login? +login.playOffline=Play offline +login.title=Minecraft Login +login.idPassword=ID/Password\: +login.password=Password\: +login.forgetUser=Forget selected user +login.forgetPassword=Forget password +login.forgetAllPasswords=Forget all passwords... +login.confirmForgetAllPasswords=Are you sure that you want to forget all saved passwords? +login.forgetAllPasswordsTitle=Forget passwords +login.noPasswordError=Please enter a password. +login.noPasswordTitle=Missing Password +login.loggingInTitle=Logging in... +login.loggingInStatus=Logging in to Mojang... +login.noLoginError=Please enter your account details. +login.noLoginTitle=Missing Account +login.minecraftNotOwnedError=Sorry, Minecraft is not owned on that account. + +console.title=Messages and Errors +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? \ No newline at end of file diff --git a/src/main/resources/com/skcraft/launcher/launcher.properties b/src/main/resources/com/skcraft/launcher/launcher.properties new file mode 100644 index 0000000..7e81582 --- /dev/null +++ b/src/main/resources/com/skcraft/launcher/launcher.properties @@ -0,0 +1,21 @@ +# +# SK's Minecraft Launcher +# Copyright (C) 2010-2014 Albert Pham and contributors +# Please see LICENSE.txt for license information. +# + +version=${project.version} +agentName=Minecraft +offlinePlayerName=Player + +versionManifestUrl=https://s3.amazonaws.com/Minecraft.Download/versions/%1$s/%1$s.json +librariesUrl=https://libraries.minecraft.net/ +jarUrl=http://s3.amazonaws.com/Minecraft.Download/versions/%1$s/%1$s.jar +assetsIndexUrl=https://s3.amazonaws.com/Minecraft.Download/indexes/%s.json +assetUrl=http://resources.download.minecraft.net/%s/%s +yggdrasilAuthUrl=https://authserver.mojang.com/authenticate +resetPasswordUrl=https://minecraft.net/resetpassword + +newsUrl=http://update.skcraft.com/quark/feed?version=%s +packageListUrl=http://update.skcraft.com/quark/packages.json?key=%s +selfUpdateUrl=http://update.skcraft.com/quark/launcher/latest.json diff --git a/src/main/resources/com/skcraft/launcher/tray_closed.png b/src/main/resources/com/skcraft/launcher/tray_closed.png new file mode 100644 index 0000000..5e70e5b --- /dev/null +++ b/src/main/resources/com/skcraft/launcher/tray_closed.png Binary files differ diff --git a/src/main/resources/com/skcraft/launcher/tray_ok.png b/src/main/resources/com/skcraft/launcher/tray_ok.png new file mode 100644 index 0000000..867849c --- /dev/null +++ b/src/main/resources/com/skcraft/launcher/tray_ok.png Binary files differ