diff --git a/build.gradle b/build.gradle index ef0c52bca..49eea452c 100644 --- a/build.gradle +++ b/build.gradle @@ -1,16 +1,5 @@ -buildscript { - repositories { - mavenCentral() - jcenter() - } - - dependencies { - classpath "com.github.jengelman.gradle.plugins:shadow:2.0.1" - } -} - plugins { - id "com.github.johnrengelman.shadow" version "4.0.4" + id "com.github.johnrengelman.shadow" version "7.1.2" id 'io.freefair.lombok' version '5.3.0' } @@ -24,13 +13,15 @@ println """ subprojects { apply plugin: 'java' - apply plugin: 'maven' group = 'com.skcraft' version = '4.6-SNAPSHOT' - sourceCompatibility = 1.8 - targetCompatibility = 1.8 + java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(8)) + } + } repositories { mavenCentral() @@ -47,6 +38,34 @@ subprojects { workingDir = new File(rootDir, "run/") workingDir.mkdirs() } + + // Work around gradle shadow bug + // see https://github.com/johnrengelman/shadow/issues/713 + afterEvaluate { + startScripts { + dependsOn(shadowJar) + } + + distTar { + dependsOn(shadowJar) + } + + distZip { + dependsOn(shadowJar) + } + + startShadowScripts { + dependsOn(jar) + } + + shadowDistTar { + dependsOn(jar) + } + + shadowDistZip { + dependsOn(jar) + } + } } task clean { diff --git a/creator-tools/build.gradle b/creator-tools/build.gradle index 09dada89f..0905e6d0b 100644 --- a/creator-tools/build.gradle +++ b/creator-tools/build.gradle @@ -5,15 +5,15 @@ plugins { } version = "2.1.0-SNAPSHOT" -sourceCompatibility = 1.8 -targetCompatibility = 1.8 -mainClassName = "com.skcraft.launcher.creator.Creator" +application { + mainClassName = "com.skcraft.launcher.creator.Creator" +} dependencies { - compile project(':launcher-builder') - compile 'org.eclipse.jetty:jetty-server:9.3.1.v20150714' - compile 'com.jidesoft:jide-oss:3.6.10' + implementation project(path: ':launcher-builder') + implementation 'org.eclipse.jetty:jetty-server:9.3.1.v20150714' + implementation 'com.jidesoft:jide-oss:3.6.18' } processResources { @@ -25,6 +25,9 @@ processResources { } shadowJar { + archiveClassifier.set("") } -build.dependsOn(shadowJar) +build { + dependsOn(shadowJar) +} diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index da9702f9e..aa991fcea 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/launcher-bootstrap/build.gradle b/launcher-bootstrap/build.gradle index 6400242fa..12969d60d 100644 --- a/launcher-bootstrap/build.gradle +++ b/launcher-bootstrap/build.gradle @@ -4,11 +4,13 @@ plugins { id 'io.freefair.lombok' } -mainClassName = "com.skcraft.launcher.Bootstrap" +application { + mainClassName = "com.skcraft.launcher.Bootstrap" +} dependencies { - compile 'com.googlecode.json-simple:json-simple:1.1.1' - compile 'javax.xml.bind:jaxb-api:2.3.0' + implementation 'com.googlecode.json-simple:json-simple:1.1.1' + implementation 'javax.xml.bind:jaxb-api:2.3.1' } processResources { @@ -20,6 +22,9 @@ processResources { } shadowJar { + archiveClassifier.set("") } -build.dependsOn(shadowJar) +build { + dependsOn(shadowJar) +} diff --git a/launcher-bootstrap/src/main/java/com/skcraft/launcher/Bootstrap.java b/launcher-bootstrap/src/main/java/com/skcraft/launcher/Bootstrap.java index 463245efc..42a913fa5 100644 --- a/launcher-bootstrap/src/main/java/com/skcraft/launcher/Bootstrap.java +++ b/launcher-bootstrap/src/main/java/com/skcraft/launcher/Bootstrap.java @@ -194,9 +194,21 @@ private File getUserLauncherDir() { String osName = System.getProperty("os.name").toLowerCase(); if (osName.contains("win")) { return new File(getFileChooseDefaultDir(), getProperties().getProperty("homeFolderWindows")); - } else { - return new File(System.getProperty("user.home"), getProperties().getProperty("homeFolder")); } + + File dotFolder = new File(System.getProperty("user.home"), getProperties().getProperty("homeFolder")); + String xdgFolderName = getProperties().getProperty("homeFolderLinux"); + + if (osName.contains("linux") && !dotFolder.exists() && xdgFolderName != null && !xdgFolderName.isEmpty()) { + String xdgDataHome = System.getenv("XDG_DATA_HOME"); + if (xdgDataHome.isEmpty()) { + xdgDataHome = System.getProperty("user.home") + "/.local/share"; + } + + return new File(xdgDataHome, xdgFolderName); + } + + return dotFolder; } private static boolean isPortableMode() { diff --git a/launcher-bootstrap/src/main/resources/com/skcraft/launcher/bootstrap.properties b/launcher-bootstrap/src/main/resources/com/skcraft/launcher/bootstrap.properties index 8609369f7..678f61648 100644 --- a/launcher-bootstrap/src/main/resources/com/skcraft/launcher/bootstrap.properties +++ b/launcher-bootstrap/src/main/resources/com/skcraft/launcher/bootstrap.properties @@ -5,6 +5,7 @@ # homeFolderWindows=Example Launcher +homeFolderLinux=example_launcher homeFolder=.examplelauncher launcherClass=com.skcraft.launcher.Launcher -latestUrl=http://update.skcraft.com/quark/launcher/latest.json \ No newline at end of file +latestUrl=http://update.skcraft.com/quark/launcher/latest.json diff --git a/launcher-builder/build.gradle b/launcher-builder/build.gradle index 0d8387c45..224895771 100644 --- a/launcher-builder/build.gradle +++ b/launcher-builder/build.gradle @@ -1,18 +1,28 @@ plugins { id 'application' + id 'java-library' id "com.github.johnrengelman.shadow" id 'io.freefair.lombok' } -mainClassName = "com.skcraft.launcher.builder.PackageBuilder" +application { + mainClassName = "com.skcraft.launcher.builder.PackageBuilder" +} dependencies { - compile project(':launcher') - compile 'org.tukaani:xz:1.0' - compile 'org.apache.commons:commons-compress:1.9' + api project(':launcher') + implementation 'org.apache.commons:commons-compress:1.21' } shadowJar { + dependsOn ':launcher:shadowJar' + archiveClassifier.set("") } +// Work around gradle shadow bug +// see https://github.com/johnrengelman/shadow/issues/713 +startScripts.dependsOn(':launcher:shadowJar') +distTar.dependsOn(':launcher:shadowJar') +distZip.dependsOn(':launcher:shadowJar') + build.dependsOn(shadowJar) diff --git a/launcher-builder/src/main/java/com/skcraft/launcher/builder/PackageBuilder.java b/launcher-builder/src/main/java/com/skcraft/launcher/builder/PackageBuilder.java index 92f54fdf9..9b693ce69 100644 --- a/launcher-builder/src/main/java/com/skcraft/launcher/builder/PackageBuilder.java +++ b/launcher-builder/src/main/java/com/skcraft/launcher/builder/PackageBuilder.java @@ -125,7 +125,8 @@ public void addFiles(File dir, File destDir) throws IOException { public void addLoaders(File dir, File librariesDir) { logSection("Checking for mod loaders to install..."); - LinkedHashSet collected = new LinkedHashSet(); + VersionManifest version = manifest.getVersionManifest(); + LinkedHashSet collected = new LinkedHashSet<>(); File[] files = dir.listFiles(new JarFileFilter()); if (files != null) { @@ -140,7 +141,6 @@ public void addLoaders(File dir, File librariesDir) { this.loaderLibraries.addAll(collected); - VersionManifest version = manifest.getVersionManifest(); collected.addAll(version.getLibraries()); version.setLibraries(collected); } @@ -164,11 +164,13 @@ private void processLoader(LinkedHashSet loaderLibraries, File file, Fi if (basicProfile.isLegacy()) { processor = new OldForgeLoaderProcessor(); - } else if (basicProfile.getProfile().equalsIgnoreCase("forge")) { + } else { processor = new ModernForgeLoaderProcessor(); } } else if (BuilderUtils.getZipEntry(jarFile, "fabric-installer.json") != null) { - processor = new FabricLoaderProcessor(); + processor = new FabricLoaderProcessor(FabricLoaderProcessor.Variant.FABRIC); + } else if (BuilderUtils.getZipEntry(jarFile, "quilt_installer.json") != null) { + processor = new FabricLoaderProcessor(FabricLoaderProcessor.Variant.QUILT); } } finally { closer.close(); @@ -204,9 +206,10 @@ public void downloadLibraries(File librariesDir) throws IOException, Interrupted if (!outputPath.exists()) { Files.createParentDirs(outputPath); boolean found = false; + boolean urlEmpty = artifact.getUrl().isEmpty(); - // Try just the URL, it might be a full URL to the file - if (!artifact.getUrl().isEmpty()) { + // If URL doesn't end with a /, it might be the direct file + if (!urlEmpty && !artifact.getUrl().endsWith("/")) { found = tryDownloadLibrary(library, artifact, artifact.getUrl(), outputPath); } @@ -219,7 +222,7 @@ public void downloadLibraries(File librariesDir) throws IOException, Interrupted } // Assume artifact URL is a maven repository URL and try that - if (!found) { + if (!found && !urlEmpty) { URL url = LauncherUtils.concat(url(artifact.getUrl()), artifact.getPath()); found = tryDownloadLibrary(library, artifact, url.toString(), outputPath); } @@ -261,7 +264,9 @@ private boolean tryDownloadLibrary(Library library, Library.Artifact artifact, S try { log.info("Downloading library " + library.getName() + " from " + url + "..."); - HttpRequest.get(url).execute().expectResponseCode(200).saveContent(tempFile); + HttpRequest.get(url).execute().expectResponseCode(200) + .expectContentType("application/java-archive", "application/octet-stream", "application/zip") + .saveContent(tempFile); } catch (IOException e) { log.info("Could not get file from " + url + ": " + e.getMessage()); return false; @@ -367,7 +372,7 @@ public void writeManifest(@NonNull File path) throws IOException { private static BuilderOptions parseArgs(String[] args) { BuilderOptions options = new BuilderOptions(); - new JCommander(options, args); + new JCommander(options).parse(args); options.choosePaths(); return options; } @@ -407,7 +412,7 @@ public static void main(String[] args) throws IOException, InterruptedException // Initialize SimpleLogFormatter.configureGlobalLogger(); ObjectMapper mapper = new ObjectMapper(); - mapper.setSerializationInclusion(JsonInclude.Include.NON_DEFAULT); + mapper.setSerializationInclusion(JsonInclude.Include.NON_ABSENT); Manifest manifest = new Manifest(); manifest.setMinimumVersion(Manifest.MIN_PROTOCOL_VERSION); diff --git a/launcher-builder/src/main/java/com/skcraft/launcher/builder/loaders/FabricLoaderProcessor.java b/launcher-builder/src/main/java/com/skcraft/launcher/builder/loaders/FabricLoaderProcessor.java index a5d3cfb28..59b1baa37 100644 --- a/launcher-builder/src/main/java/com/skcraft/launcher/builder/loaders/FabricLoaderProcessor.java +++ b/launcher-builder/src/main/java/com/skcraft/launcher/builder/loaders/FabricLoaderProcessor.java @@ -4,10 +4,14 @@ import com.google.common.io.Closer; import com.skcraft.launcher.builder.BuilderUtils; import com.skcraft.launcher.model.loader.FabricMod; +import com.skcraft.launcher.model.loader.QuiltMod; +import com.skcraft.launcher.model.loader.Versionable; import com.skcraft.launcher.model.minecraft.Library; +import com.skcraft.launcher.model.minecraft.MavenName; import com.skcraft.launcher.model.minecraft.VersionManifest; import com.skcraft.launcher.model.modpack.Manifest; import com.skcraft.launcher.util.HttpRequest; +import lombok.RequiredArgsConstructor; import lombok.extern.java.Log; import java.io.File; @@ -18,7 +22,10 @@ import java.util.zip.ZipEntry; @Log +@RequiredArgsConstructor public class FabricLoaderProcessor implements ILoaderProcessor { + private final Variant variant; + @Override public LoaderResult process(File loaderJar, Manifest manifest, ObjectMapper mapper, File baseDir) throws IOException { JarFile jarFile = new JarFile(loaderJar); @@ -26,24 +33,23 @@ public LoaderResult process(File loaderJar, Manifest manifest, ObjectMapper mapp Closer closer = Closer.create(); try { - String loaderVersion; + Versionable loaderMod; - ZipEntry modEntry = BuilderUtils.getZipEntry(jarFile, "fabric.mod.json"); + ZipEntry modEntry = BuilderUtils.getZipEntry(jarFile, variant.modJsonName); if (modEntry != null) { InputStreamReader reader = new InputStreamReader(jarFile.getInputStream(modEntry)); - FabricMod loaderMod = mapper.readValue( - BuilderUtils.readStringFromStream(closer.register(reader)), FabricMod.class); - loaderVersion = loaderMod.getVersion(); + loaderMod = mapper.readValue( + BuilderUtils.readStringFromStream(closer.register(reader)), variant.mappedClass); } else { - log.warning("Fabric loader has no 'fabric.mod.json' file, is it really a Fabric Loader jar?"); + log.warning(String.format("%s loader has no '%s' file, is it really a %s Loader jar?", + variant.friendlyName, variant.modJsonName, variant.friendlyName)); return null; } - log.info("Downloading fabric metadata..."); + log.info(String.format("Downloading %s metadata...", variant.friendlyName)); URL metaUrl = HttpRequest.url( - String.format("https://meta.fabricmc.net/v2/versions/loader/%s/%s/profile/json", - manifest.getGameVersion(), loaderVersion)); + String.format(variant.metaUrl, manifest.getGameVersion(), loaderMod.getVersion())); VersionManifest fabricManifest = HttpRequest.get(metaUrl) .execute() .expectResponseCode(200) @@ -51,6 +57,21 @@ public LoaderResult process(File loaderJar, Manifest manifest, ObjectMapper mapp .asJson(VersionManifest.class); for (Library library : fabricManifest.getLibraries()) { + // To quote a famous comment: "And here we come upon a sad state of affairs." + // Quilt's meta API returns broken data about how to launch the game. It specifies its own incomplete + // and ultimately broken set of intermediary mappings called "hashed". If the loader finds "hashed" in + // the classpath it tries to use it and blows up because it doesn't work. + // We work around this here by just throwing the hashed library out - they do now at least specify + // fabric's intermediary mappings in the library list, which DO work. + // Historical note: previously they didn't do this! Every launcher that added Quilt support had to add + // a hack that replaced hashed with intermediary! This is a lot of technical debt that is gonna come + // back to bite them in the ass later, because it's all still there! + // TODO pester Quilt again about fixing this.... + MavenName libraryName = library.getName(); + if (libraryName.getGroup().equals("org.quiltmc") && libraryName.getPath().equals("hashed") && loaderMod instanceof QuiltMod) { + continue; + } + result.getLoaderLibraries().add(library); log.info("Adding loader library " + library.getName()); } @@ -61,7 +82,8 @@ public LoaderResult process(File loaderJar, Manifest manifest, ObjectMapper mapp log.info("Using main class " + mainClass); } } catch (InterruptedException e) { - log.warning("HTTP request to fabric metadata API was interrupted, this will probably not work!"); + log.warning(String.format("HTTP request to %s metadata API was interrupted!", variant.friendlyName)); + throw new IOException(e); } finally { closer.close(); jarFile.close(); @@ -69,4 +91,15 @@ public LoaderResult process(File loaderJar, Manifest manifest, ObjectMapper mapp return result; } + + @RequiredArgsConstructor + public enum Variant { + FABRIC("Fabric", "fabric.mod.json", "https://meta.fabricmc.net/v2/versions/loader/%s/%s/profile/json", FabricMod.class), + QUILT("Quilt", "quilt.mod.json", "https://meta.quiltmc.org/v3/versions/loader/%s/%s/profile/json", QuiltMod.class); + + private final String friendlyName; + private final String modJsonName; + private final String metaUrl; + private final Class mappedClass; + } } diff --git a/launcher-builder/src/main/java/com/skcraft/launcher/builder/loaders/ModernForgeLoaderProcessor.java b/launcher-builder/src/main/java/com/skcraft/launcher/builder/loaders/ModernForgeLoaderProcessor.java index b21b63f61..17c8c3606 100644 --- a/launcher-builder/src/main/java/com/skcraft/launcher/builder/loaders/ModernForgeLoaderProcessor.java +++ b/launcher-builder/src/main/java/com/skcraft/launcher/builder/loaders/ModernForgeLoaderProcessor.java @@ -73,6 +73,12 @@ public LoaderResult process(File loaderJar, Manifest manifest, ObjectMapper mapp } } + // Copy logging config + SidedData loggingConfig = info.getLogging(); + if (loggingConfig != null) { + version.setLogging(loggingConfig); + } + // Copy main class String mainClass = info.getMainClass(); if (mainClass != null) { @@ -150,6 +156,22 @@ public LoaderResult process(File loaderJar, Manifest manifest, ObjectMapper mapp // Add loader manifest to the map manifest.getLoaders().put(loaderName, new LoaderManifest(profile.getLibraries(), profile.getData(), extraFiles)); + // Find name of final patched library and mark it as excluded from download + // TODO: we should generalize this to all process outputs, really + SidedData finalJars = profile.getData().get("PATCHED"); + if (finalJars != null) { + String libraryName = finalJars.getClient(); + libraryName = libraryName.substring(1, libraryName.length() - 1); + + for (Library lib : result.getLoaderLibraries()) { + if (lib.matches(libraryName)) { + lib.setGenerated(true); + log.info(String.format("Setting generated flag on library '%s'", lib.getName())); + break; + } + } + } + // Add processors manifest.getTasks().addAll(profile.toProcessorEntries(loaderName)); } diff --git a/launcher-builder/src/main/java/com/skcraft/launcher/builder/loaders/OldForgeLoaderProcessor.java b/launcher-builder/src/main/java/com/skcraft/launcher/builder/loaders/OldForgeLoaderProcessor.java index c5e970d5e..c5c9759d9 100644 --- a/launcher-builder/src/main/java/com/skcraft/launcher/builder/loaders/OldForgeLoaderProcessor.java +++ b/launcher-builder/src/main/java/com/skcraft/launcher/builder/loaders/OldForgeLoaderProcessor.java @@ -6,10 +6,7 @@ import com.google.common.io.Files; import com.skcraft.launcher.builder.BuilderUtils; import com.skcraft.launcher.model.loader.profiles.LegacyInstallProfile; -import com.skcraft.launcher.model.minecraft.GameArgument; -import com.skcraft.launcher.model.minecraft.Library; -import com.skcraft.launcher.model.minecraft.MinecraftArguments; -import com.skcraft.launcher.model.minecraft.VersionManifest; +import com.skcraft.launcher.model.minecraft.*; import com.skcraft.launcher.model.modpack.Manifest; import lombok.extern.java.Log; @@ -64,11 +61,7 @@ public LoaderResult process(File loaderJar, Manifest manifest, ObjectMapper mapp // Add libraries List libraries = profile.getVersionInfo().getLibraries(); if (libraries != null) { - for (Library library : libraries) { - if (!version.getLibraries().contains(library)) { - result.getLoaderLibraries().add(library); - } - } + result.getLoaderLibraries().addAll(libraries); } // Copy main class @@ -87,7 +80,7 @@ public LoaderResult process(File loaderJar, Manifest manifest, ObjectMapper mapp if (libraryEntry != null) { File librariesDir = new File(baseDir, "libraries"); - File extractPath = new File(librariesDir, Library.mavenNameToPath(libraryPath)); + File extractPath = new File(librariesDir, MavenName.from(libraryPath).getFilePath()); Files.createParentDirs(extractPath); ByteStreams.copy(closer.register(jarFile.getInputStream(libraryEntry)), diff --git a/launcher-fancy/build.gradle b/launcher-fancy/build.gradle index 6cc9891f1..22af7053d 100644 --- a/launcher-fancy/build.gradle +++ b/launcher-fancy/build.gradle @@ -4,21 +4,33 @@ plugins { id 'io.freefair.lombok' } -mainClassName = "com.skcraft.launcher.FancyLauncher" +application { + mainClassName = "com.skcraft.launcher.FancyLauncher" +} repositories { maven { - name = 'obw maven' - url = 'https://maven.offbeatwit.ch/repository/snapshots' + name = 'bbkr.space maven' + url = 'https://server.bbkr.space/artifactory/libs-snapshot' } } dependencies { - compile project(':launcher') - compile 'io.github.cottonmc.insubstantial:substance:7.3.1-SNAPSHOT' + implementation project(path: ':launcher') + implementation 'io.github.cottonmc.insubstantial:substance:7.3.1-SNAPSHOT' } shadowJar { + dependsOn ':launcher:shadowJar' + archiveClassifier.set("") } -build.dependsOn(shadowJar) +// Work around gradle shadow bug +// see https://github.com/johnrengelman/shadow/issues/713 +startScripts.dependsOn(':launcher:shadowJar') +distTar.dependsOn(':launcher:shadowJar') +distZip.dependsOn(':launcher:shadowJar') + +build { + dependsOn(shadowJar) +} diff --git a/launcher/build.gradle b/launcher/build.gradle index 1525face0..b41115ad2 100644 --- a/launcher/build.gradle +++ b/launcher/build.gradle @@ -1,23 +1,26 @@ plugins { id 'application' + id 'java-library' id "com.github.johnrengelman.shadow" id 'io.freefair.lombok' } -mainClassName = "com.skcraft.launcher.Launcher" +application { + mainClassName = "com.skcraft.launcher.Launcher" +} dependencies { - compile 'javax.xml.bind:jaxb-api:2.2.4' - compile 'com.fasterxml.jackson.core:jackson-databind:2.3.0' - compile 'commons-lang:commons-lang:2.6' - compile 'commons-io:commons-io:1.2' - compile 'com.google.guava:guava:15.0' - compile 'com.beust:jcommander:1.32' - compile 'com.miglayout:miglayout:3.7.4' - compile 'com.google.code.findbugs:jsr305:3.0.0' - compile 'com.googlecode.plist:dd-plist:1.23' + api 'javax.xml.bind:jaxb-api:2.3.1' + api 'com.fasterxml.jackson.core:jackson-databind:2.13.2.2' + api 'commons-lang:commons-lang:2.6' + api 'commons-io:commons-io:1.2' + api 'com.google.guava:guava:15.0' + api 'com.beust:jcommander:1.82' + api 'com.miglayout:miglayout:3.7.4' + api 'com.google.code.findbugs:jsr305:3.0.2' - implementation 'net.java.dev.jna:jna-platform:5.10.0' + implementation 'com.googlecode.plist:dd-plist:1.23' + implementation 'net.java.dev.jna:jna-platform:5.11.0' } processResources { @@ -29,6 +32,9 @@ processResources { } shadowJar { + archiveClassifier.set("") } -build.dependsOn(shadowJar) +build { + dependsOn(shadowJar) +} diff --git a/launcher/src/main/java/com/skcraft/launcher/AssetsRoot.java b/launcher/src/main/java/com/skcraft/launcher/AssetsRoot.java index 2bf64c798..247188f8e 100644 --- a/launcher/src/main/java/com/skcraft/launcher/AssetsRoot.java +++ b/launcher/src/main/java/com/skcraft/launcher/AssetsRoot.java @@ -6,7 +6,6 @@ package com.skcraft.launcher; -import com.google.common.io.Files; import com.skcraft.concurrency.ProgressObservable; import com.skcraft.launcher.model.minecraft.Asset; import com.skcraft.launcher.model.minecraft.AssetsIndex; @@ -18,6 +17,7 @@ import java.io.File; import java.io.IOException; +import java.nio.file.Files; import java.util.Map; import java.util.logging.Level; @@ -101,6 +101,7 @@ public AssetsTreeBuilder(AssetsIndex index, File destDir) { public File build() throws IOException, LauncherException { AssetsRoot.log.info("Building asset virtual tree at '" + destDir.getAbsolutePath() + "'..."); + boolean supportsLinks = true; for (Map.Entry entry : index.getObjects().entrySet()) { File objectPath = getObjectPath(entry.getValue()); File virtualPath = new File(destDir, entry.getKey()); @@ -114,7 +115,17 @@ public File build() throws IOException, LauncherException { throw new LauncherException("Missing object " + objectPath.getAbsolutePath(), message); } - Files.copy(objectPath, virtualPath); + if (supportsLinks) { + try { + Files.createLink(virtualPath.toPath(), objectPath.toPath()); + } catch (UnsupportedOperationException e) { + supportsLinks = false; + } + } + + if (!supportsLinks) { + Files.copy(objectPath.toPath(), virtualPath.toPath()); + } } processed++; } diff --git a/launcher/src/main/java/com/skcraft/launcher/Launcher.java b/launcher/src/main/java/com/skcraft/launcher/Launcher.java index b2c950896..baef3da7e 100644 --- a/launcher/src/main/java/com/skcraft/launcher/Launcher.java +++ b/launcher/src/main/java/com/skcraft/launcher/Launcher.java @@ -105,7 +105,7 @@ public void run() { } }); - updateManager.checkForUpdate(); + updateManager.checkForUpdate(null); } /** @@ -401,8 +401,11 @@ public URL propUrl(String key, String... args) { /** * Show the launcher. */ - public void showLauncherWindow() { - mainWindowSupplier.get().setVisible(true); + public Window showLauncherWindow() { + Window window = mainWindowSupplier.get(); + window.setVisible(true); + + return window; } /** @@ -415,7 +418,7 @@ public void showLauncherWindow() { */ public static Launcher createFromArguments(String[] args) throws ParameterException, IOException { LauncherArguments options = new LauncherArguments(); - new JCommander(options, args); + new JCommander(options).parse(args); Integer bsVersion = options.getBootstrapVersion(); log.info(bsVersion != null ? "Bootstrap version " + bsVersion + " detected" : "Not bootstrapped"); diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/MicrosoftLoginService.java b/launcher/src/main/java/com/skcraft/launcher/auth/MicrosoftLoginService.java index b79aa32e6..0c461772c 100644 --- a/launcher/src/main/java/com/skcraft/launcher/auth/MicrosoftLoginService.java +++ b/launcher/src/main/java/com/skcraft/launcher/auth/MicrosoftLoginService.java @@ -1,7 +1,7 @@ package com.skcraft.launcher.auth; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; import com.skcraft.launcher.auth.microsoft.MicrosoftWebAuthorizer; import com.skcraft.launcher.auth.microsoft.MinecraftServicesAuthorizer; @@ -87,7 +87,7 @@ private TokenResponse exchangeToken(Consumer formConsumer) .expectResponseCodeOr(200, (req) -> { TokenError error = req.returnContent().asJson(TokenError.class); - return new AuthenticationException(error.errorDescription); + return new AuthenticationException(error.errorDescription, true); }) .returnContent() .asJson(TokenResponse.class); @@ -163,7 +163,7 @@ public SavedSession toSavedSession() { } @Data - @JsonNaming(PropertyNamingStrategy.LowerCaseWithUnderscoresStrategy.class) + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) @JsonIgnoreProperties(ignoreUnknown = true) private static class TokenError { private String error; diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/UserType.java b/launcher/src/main/java/com/skcraft/launcher/auth/UserType.java index 1d60b29f0..a697886be 100644 --- a/launcher/src/main/java/com/skcraft/launcher/auth/UserType.java +++ b/launcher/src/main/java/com/skcraft/launcher/auth/UserType.java @@ -10,28 +10,31 @@ * Represents the type of user for the account. */ public enum UserType { - /** * Legacy accounts login with an account username. */ - LEGACY, + LEGACY("legacy"), /** * Mojang accounts login with an email address. */ - MOJANG, + MOJANG("mojang"), /** * Microsoft accounts login via OAuth. */ - MICROSOFT; + MICROSOFT("msa"); + + private final String id; + + UserType(String id) { + this.id = id; + } /** - * Return a lowercase version of the enum type. + * Return the account type string as the game understands it * - * @return the lowercase name + * @return the account type ID for passing to the game */ - public String getName() { - return name().toLowerCase(); + public String getId() { + return this.id; } - - } diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/YggdrasilLoginService.java b/launcher/src/main/java/com/skcraft/launcher/auth/YggdrasilLoginService.java index 1e7993425..0ea50c12f 100644 --- a/launcher/src/main/java/com/skcraft/launcher/auth/YggdrasilLoginService.java +++ b/launcher/src/main/java/com/skcraft/launcher/auth/YggdrasilLoginService.java @@ -7,9 +7,6 @@ package com.skcraft.launcher.auth; import com.fasterxml.jackson.annotation.*; -import com.skcraft.launcher.auth.microsoft.MinecraftServicesAuthorizer; -import com.skcraft.launcher.auth.microsoft.model.McProfileResponse; -import com.skcraft.launcher.auth.skin.MinecraftSkinService; import com.skcraft.launcher.util.HttpRequest; import lombok.Data; import lombok.RequiredArgsConstructor; @@ -63,13 +60,11 @@ private Session call(URL url, Object payload, SavedSession previous) if (previous != null && previous.getAvatarImage() != null) { profile.setAvatarImage(previous.getAvatarImage()); - } else { - McProfileResponse skinProfile = MinecraftServicesAuthorizer - .getUserProfile("Bearer " + response.getAccessToken()); - - profile.setAvatarImage(MinecraftSkinService.fetchSkinHead(skinProfile)); } + // DEPRECEATION: minecraft services API no longer accepts yggdrasil tokens + // login still works though. until it doesn't, this class will remain + return profile; } } diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/MicrosoftWebAuthorizer.java b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/MicrosoftWebAuthorizer.java index eb07245b9..a4d79535c 100644 --- a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/MicrosoftWebAuthorizer.java +++ b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/MicrosoftWebAuthorizer.java @@ -1,6 +1,7 @@ package com.skcraft.launcher.auth.microsoft; import com.skcraft.launcher.auth.AuthenticationException; +import com.skcraft.launcher.swing.SwingHelper; import com.skcraft.launcher.util.HttpRequest; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -30,7 +31,7 @@ public OauthResult authorize() throws IOException, AuthenticationException, Inte private OauthResult authorizeInteractive() throws IOException, AuthenticationException, InterruptedException { OauthHttpHandler httpHandler = new OauthHttpHandler(); - Desktop.getDesktop().browse(generateInteractiveUrl(httpHandler.getPort())); + SwingHelper.openURL(generateInteractiveUrl(httpHandler.getPort())); return httpHandler.await(); } diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/MinecraftServicesAuthorizer.java b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/MinecraftServicesAuthorizer.java index 528fbbf10..a64a67d4c 100644 --- a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/MinecraftServicesAuthorizer.java +++ b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/MinecraftServicesAuthorizer.java @@ -37,7 +37,13 @@ public static McProfileResponse getUserProfile(String authorization) .header("Authorization", authorization) .execute() .expectResponseCodeOr(200, req -> { - McServicesError error = req.returnContent().asJson(McServicesError.class); + HttpRequest.BufferedResponse content = req.returnContent(); + if (content.asBytes().length == 0) { + return new AuthenticationException("Got empty response from Minecraft services", + SharedLocale.tr("login.minecraft.error", req.getResponseCode())); + } + + McServicesError error = content.asJson(McServicesError.class); if (error.getError().equals("NOT_FOUND")) { return new AuthenticationException("No Minecraft profile", diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/OauthHttpHandler.java b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/OauthHttpHandler.java index b7b1e0fc7..34fd9bf56 100644 --- a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/OauthHttpHandler.java +++ b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/OauthHttpHandler.java @@ -2,13 +2,17 @@ import com.google.common.base.Charsets; import com.google.common.base.Splitter; +import com.skcraft.launcher.Launcher; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; import lombok.extern.java.Log; +import org.apache.commons.io.IOUtils; import java.io.IOException; +import java.io.InputStream; import java.net.InetSocketAddress; +import java.util.Base64; import java.util.Map; import java.util.concurrent.Executor; import java.util.concurrent.Executors; @@ -56,7 +60,23 @@ public void handle(HttpExchange httpExchange) throws IOException { OauthHttpHandler.this.notifyAll(); } - byte[] response = "OK: you can close the browser now".getBytes(Charsets.UTF_8); + byte[] response; + InputStream is = Launcher.class.getResourceAsStream("login.html"); + if (is != null) { + response = IOUtils.toByteArray(is); + } else { + response = "Unable to fetch resource login.html".getBytes(Charsets.UTF_8); + } + + InputStream iconStream = Launcher.class.getResourceAsStream("icon.png"); + if (iconStream != null) { + byte[] iconBytes = IOUtils.toByteArray(iconStream); + String encodedIcon = Base64.getEncoder().encodeToString(iconBytes); + response = String.format(new String(response), encodedIcon).getBytes(Charsets.UTF_8); + } else { + log.warning("Unable to fetch resource icon.png"); + } + httpExchange.sendResponseHeaders(200, response.length); httpExchange.getResponseBody().write(response); httpExchange.getResponseBody().flush(); diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/McAuthResponse.java b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/McAuthResponse.java index 768c05fcf..c047d12fe 100644 --- a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/McAuthResponse.java +++ b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/McAuthResponse.java @@ -2,12 +2,12 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; import lombok.Data; @Data -@JsonNaming(PropertyNamingStrategy.LowerCaseWithUnderscoresStrategy.class) +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) @JsonIgnoreProperties(ignoreUnknown = true) public class McAuthResponse { private String accessToken; diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/TokenResponse.java b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/TokenResponse.java index d6aeb60be..2348afc02 100644 --- a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/TokenResponse.java +++ b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/TokenResponse.java @@ -1,12 +1,12 @@ package com.skcraft.launcher.auth.microsoft.model; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; import lombok.Data; @Data -@JsonNaming(PropertyNamingStrategy.LowerCaseWithUnderscoresStrategy.class) +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) @JsonIgnoreProperties(ignoreUnknown = true) public class TokenResponse { private String tokenType; diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XblAuthProperties.java b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XblAuthProperties.java index 141468784..989187564 100644 --- a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XblAuthProperties.java +++ b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XblAuthProperties.java @@ -1,12 +1,12 @@ package com.skcraft.launcher.auth.microsoft.model; -import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; import lombok.Data; import lombok.NonNull; @Data -@JsonNaming(PropertyNamingStrategy.PascalCaseStrategy.class) +@JsonNaming(PropertyNamingStrategies.UpperCamelCaseStrategy.class) public class XblAuthProperties { private String authMethod = "RPS"; private String siteName = "user.auth.xboxlive.com"; diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XboxAuthRequest.java b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XboxAuthRequest.java index 53c449815..388835748 100644 --- a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XboxAuthRequest.java +++ b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XboxAuthRequest.java @@ -1,12 +1,12 @@ package com.skcraft.launcher.auth.microsoft.model; -import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; import lombok.Data; import lombok.NonNull; @Data -@JsonNaming(PropertyNamingStrategy.PascalCaseStrategy.class) +@JsonNaming(PropertyNamingStrategies.UpperCamelCaseStrategy.class) public class XboxAuthRequest { @NonNull private T properties; private String relyingParty = "http://auth.xboxlive.com"; diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XboxAuthResponse.java b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XboxAuthResponse.java index c7893a81a..e989e3889 100644 --- a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XboxAuthResponse.java +++ b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XboxAuthResponse.java @@ -2,14 +2,14 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; import lombok.Data; import java.util.List; @Data -@JsonNaming(PropertyNamingStrategy.PascalCaseStrategy.class) +@JsonNaming(PropertyNamingStrategies.UpperCamelCaseStrategy.class) @JsonIgnoreProperties(ignoreUnknown = true) public class XboxAuthResponse { private String token; diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XstsAuthProperties.java b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XstsAuthProperties.java index 63fcef881..5d59693b2 100644 --- a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XstsAuthProperties.java +++ b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XstsAuthProperties.java @@ -1,6 +1,6 @@ package com.skcraft.launcher.auth.microsoft.model; -import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; import lombok.Data; @@ -8,7 +8,7 @@ import java.util.List; @Data -@JsonNaming(PropertyNamingStrategy.PascalCaseStrategy.class) +@JsonNaming(PropertyNamingStrategies.UpperCamelCaseStrategy.class) public class XstsAuthProperties { private String sandboxId = "RETAIL"; private List userTokens; diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XstsError.java b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XstsError.java index d279d42b6..aec313ff8 100644 --- a/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XstsError.java +++ b/launcher/src/main/java/com/skcraft/launcher/auth/microsoft/model/XstsError.java @@ -2,12 +2,12 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; import lombok.Data; @Data -@JsonNaming(PropertyNamingStrategy.PascalCaseStrategy.class) +@JsonNaming(PropertyNamingStrategies.UpperCamelCaseStrategy.class) @JsonIgnoreProperties(ignoreUnknown = true) public class XstsError { @JsonProperty("XErr") diff --git a/launcher/src/main/java/com/skcraft/launcher/dialog/AboutDialog.java b/launcher/src/main/java/com/skcraft/launcher/dialog/AboutDialog.java index 534be27eb..30b466253 100644 --- a/launcher/src/main/java/com/skcraft/launcher/dialog/AboutDialog.java +++ b/launcher/src/main/java/com/skcraft/launcher/dialog/AboutDialog.java @@ -29,7 +29,7 @@ private void initComponents() { JPanel container = new JPanel(); container.setLayout(new MigLayout("insets dialog")); - container.add(new JLabel("Licensed under GNU General Public License, version 3."), "wrap, gapbottom unrel"); + container.add(new JLabel("Licensed under the GNU Lesser General Public License, version 3."), "wrap, gapbottom unrel"); container.add(new JLabel("You are using SKCraft Launcher, an open-source customizable
" + "launcher platform that anyone can use."), "wrap, gapbottom unrel"); container.add(new JLabel("SKCraft does not necessarily endorse the version of
" + diff --git a/launcher/src/main/java/com/skcraft/launcher/dialog/AccountSelectDialog.java b/launcher/src/main/java/com/skcraft/launcher/dialog/AccountSelectDialog.java index 9221d7942..89d0785f2 100644 --- a/launcher/src/main/java/com/skcraft/launcher/dialog/AccountSelectDialog.java +++ b/launcher/src/main/java/com/skcraft/launcher/dialog/AccountSelectDialog.java @@ -102,7 +102,7 @@ private void initComponents() { } }); - addMicrosoftButton.addActionListener(ev -> attemptMicrosoftLogin()); + addMicrosoftButton.addActionListener(ev -> attemptMicrosoftLogin(SharedLocale.tr("login.microsoft.seeBrowser"))); offlineButton.addActionListener(ev -> setResult(new OfflineSession(launcher.getProperties().getProperty("offlinePlayerName")))); @@ -145,8 +145,7 @@ private void setResult(Session result) { dispose(); } - private void attemptMicrosoftLogin() { - String status = SharedLocale.tr("login.microsoft.seeBrowser"); + private void attemptMicrosoftLogin(String status) { SettableProgress progress = new SettableProgress(status, -1); ListenableFuture future = launcher.getExecutor().submit(() -> { @@ -181,14 +180,9 @@ public void onSuccess(Session result) { @Override public void onFailure(Throwable t) { - if (t instanceof AuthenticationException) { - if (((AuthenticationException) t).isInvalidatedSession()) { - // Just need to log in again - LoginDialog.ReloginDetails details = new LoginDialog.ReloginDetails(session.getUsername(), t.getLocalizedMessage()); - Session newSession = LoginDialog.showLoginRequest(AccountSelectDialog.this, launcher, details); - - setResult(newSession); - } + if (t instanceof AuthenticationException && ((AuthenticationException) t).isInvalidatedSession()) { + // Just need to log in again + relogin(session, t.getLocalizedMessage()); } else { SwingHelper.showErrorDialog(AccountSelectDialog.this, t.getLocalizedMessage(), SharedLocale.tr("errorTitle"), t); } @@ -199,6 +193,22 @@ public void onFailure(Throwable t) { SharedLocale.tr("login.loggingInStatus")); } + /** + * Re-login to an expired session + */ + private void relogin(SavedSession session, String message) { + if (session.getType() == UserType.MICROSOFT) { + this.attemptMicrosoftLogin(message); + } else { + LoginDialog.ReloginDetails details = new LoginDialog.ReloginDetails(session.getUsername(), + SharedLocale.tr("login.relogin", message)); + Session newSession = LoginDialog.showLoginRequest(AccountSelectDialog.this, launcher, details); + + launcher.getAccounts().update(newSession.toSavedSession()); + setResult(newSession); + } + } + @RequiredArgsConstructor private static class RestoreSessionCallable implements Callable, ProgressObservable { private final LoginService service; diff --git a/launcher/src/main/java/com/skcraft/launcher/dialog/InstanceSettingsDialog.java b/launcher/src/main/java/com/skcraft/launcher/dialog/InstanceSettingsDialog.java index f261e95cf..9518024ca 100644 --- a/launcher/src/main/java/com/skcraft/launcher/dialog/InstanceSettingsDialog.java +++ b/launcher/src/main/java/com/skcraft/launcher/dialog/InstanceSettingsDialog.java @@ -57,7 +57,7 @@ private void initComponents() { javaRuntimeBox.setModel(new DefaultComboBoxModel<>(javaRuntimes)); runtimePanel.addRow(enableCustomRuntime); - runtimePanel.addRow(new JLabel(SharedLocale.tr("options.jvmPath")), javaRuntimeBox); + runtimePanel.addRow(new JLabel(SharedLocale.tr("options.jvmRuntime")), javaRuntimeBox); runtimePanel.addRow(new JLabel(SharedLocale.tr("options.jvmArguments")), javaArgsBox); okButton.setMargin(new Insets(0, 10, 0, 10)); diff --git a/launcher/src/main/java/com/skcraft/launcher/dialog/LauncherFrame.java b/launcher/src/main/java/com/skcraft/launcher/dialog/LauncherFrame.java index 2083da75b..497d1aadf 100644 --- a/launcher/src/main/java/com/skcraft/launcher/dialog/LauncherFrame.java +++ b/launcher/src/main/java/com/skcraft/launcher/dialog/LauncherFrame.java @@ -132,7 +132,7 @@ public void tableChanged(TableModelEvent e) { @Override public void actionPerformed(ActionEvent e) { loadInstances(); - launcher.getUpdateManager().checkForUpdate(); + launcher.getUpdateManager().checkForUpdate(LauncherFrame.this); webView.browse(launcher.getNewsURL(), false); } }); @@ -394,7 +394,8 @@ public void gameStarted() { @Override public void gameClosed() { - launcher.showLauncherWindow(); + Window newLauncherWindow = launcher.showLauncherWindow(); + launcher.getUpdateManager().checkForUpdate(newLauncherWindow); } } diff --git a/launcher/src/main/java/com/skcraft/launcher/launch/LaunchSupervisor.java b/launcher/src/main/java/com/skcraft/launcher/launch/LaunchSupervisor.java index 9dd8a2726..09f2f23c4 100644 --- a/launcher/src/main/java/com/skcraft/launcher/launch/LaunchSupervisor.java +++ b/launcher/src/main/java/com/skcraft/launcher/launch/LaunchSupervisor.java @@ -14,21 +14,29 @@ import com.skcraft.launcher.Launcher; import com.skcraft.launcher.auth.Session; import com.skcraft.launcher.dialog.AccountSelectDialog; +import com.skcraft.launcher.dialog.ProcessConsoleFrame; import com.skcraft.launcher.dialog.ProgressDialog; import com.skcraft.launcher.launch.LaunchOptions.UpdatePolicy; +import com.skcraft.launcher.launch.runtime.JavaRuntime; +import com.skcraft.launcher.model.minecraft.JavaVersion; import com.skcraft.launcher.persistence.Persistence; import com.skcraft.launcher.swing.SwingHelper; import com.skcraft.launcher.update.Updater; import com.skcraft.launcher.util.SharedLocale; import com.skcraft.launcher.util.SwingExecutor; +import lombok.RequiredArgsConstructor; import lombok.extern.java.Log; import org.apache.commons.io.FileUtils; +import javax.annotation.Nullable; import javax.swing.*; import java.awt.*; import java.io.File; import java.io.IOException; import java.util.Date; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.function.BiPredicate; import java.util.logging.Level; import static com.google.common.util.concurrent.MoreExecutors.sameThreadExecutor; @@ -119,7 +127,7 @@ private void launch(Window window, Instance instance, Session session, final Lau final File extractDir = launcher.createExtractDir(); // Get the process - Runner task = new Runner(launcher, instance, session, extractDir); + Runner task = new Runner(launcher, instance, session, extractDir, new RuntimeVerifier(instance)); ObservableFuture processFuture = new ObservableFuture( launcher.getExecutor().submit(task), task); @@ -131,12 +139,7 @@ private void launch(Window window, Instance instance, Session session, final Lau Futures.addCallback(processFuture, new FutureCallback() { @Override public void onSuccess(Process result) { - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - listener.gameStarted(); - } - }); + SwingUtilities.invokeLater(listener::gameStarted); } @Override @@ -145,28 +148,69 @@ public void onFailure(Throwable t) { }); // Watch the created process - ListenableFuture future = Futures.transform( + ListenableFuture future = Futures.transform( processFuture, new LaunchProcessHandler(launcher), launcher.getExecutor()); SwingHelper.addErrorDialogCallback(null, future); // Clean up at the very end - future.addListener(new Runnable() { + future.addListener(() -> { + try { + log.info("Process ended; cleaning up " + extractDir.getAbsolutePath()); + FileUtils.deleteDirectory(extractDir); + } catch (IOException e) { + log.log(Level.WARNING, "Failed to clean up " + extractDir.getAbsolutePath(), e); + } + }, sameThreadExecutor()); + + // Hook up launch listener + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(@Nullable ProcessConsoleFrame result) { + // gameStarted was only invoked on success above, so only call gameClosed on success + listener.gameClosed(); + } + @Override - public void 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); + public void onFailure(Throwable t) { + // likely user cancellation + if (!(t instanceof CancellationException)) { + log.info("Process failure: " + t.getLocalizedMessage()); } + } + }, SwingExecutor.INSTANCE); + } - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - listener.gameClosed(); - } - }); + @RequiredArgsConstructor + static class RuntimeVerifier implements BiPredicate { + private final Instance instance; + + @Override + public boolean test(JavaRuntime javaRuntime, JavaVersion javaVersion) { + ListenableFuture fut = SwingExecutor.INSTANCE.submit(() -> { + Object[] options = new Object[]{ + tr("button.cancel"), + tr("button.launchAnyway"), + }; + + String message = tr("runner.wrongJavaVersion", + instance.getTitle(), javaVersion.getMajorVersion(), javaRuntime.getVersion()); + int picked = JOptionPane.showOptionDialog(null, + SwingHelper.htmlWrap(message), + tr("launcher.javaMismatchTitle"), + JOptionPane.DEFAULT_OPTION, + JOptionPane.WARNING_MESSAGE, + null, + options, + null); + + return picked == 1; + }); + + try { + return fut.get(); + } catch (ExecutionException | InterruptedException e) { + throw new RuntimeException(e); } - }, sameThreadExecutor()); + } } } diff --git a/launcher/src/main/java/com/skcraft/launcher/launch/Runner.java b/launcher/src/main/java/com/skcraft/launcher/launch/Runner.java index 27d98e7f6..7846861f8 100644 --- a/launcher/src/main/java/com/skcraft/launcher/launch/Runner.java +++ b/launcher/src/main/java/com/skcraft/launcher/launch/Runner.java @@ -37,6 +37,8 @@ import java.util.Map; import java.util.Optional; import java.util.concurrent.Callable; +import java.util.concurrent.CancellationException; +import java.util.function.BiPredicate; import static com.skcraft.launcher.LauncherUtils.checkInterrupted; import static com.skcraft.launcher.util.SharedLocale.tr; @@ -54,6 +56,7 @@ public class Runner implements Callable, ProgressObservable { private final Instance instance; private final Session session; private final File extractDir; + private final BiPredicate javaRuntimeMismatch; @Getter @Setter private Environment environment = Environment.getInstance(); private VersionManifest versionManifest; @@ -66,18 +69,20 @@ public class Runner implements Callable, ProgressObservable { /** * Create a new instance launcher. - * - * @param launcher the launcher + * @param launcher the launcher * @param instance the instance * @param session the session * @param extractDir the directory to extract to + * @param javaRuntimeMismatch */ public Runner(@NonNull Launcher launcher, @NonNull Instance instance, - @NonNull Session session, @NonNull File extractDir) { + @NonNull Session session, @NonNull File extractDir, + BiPredicate javaRuntimeMismatch) { this.launcher = launcher; this.instance = instance; this.session = session; this.extractDir = extractDir; + this.javaRuntimeMismatch = javaRuntimeMismatch; this.featureList = new FeatureList.Mutable(); } @@ -135,7 +140,6 @@ public Process call() throws Exception { } progress = new DefaultProgress(0.9, SharedLocale.tr("runner.collectingArgs")); - builder.classPath(getJarPath()); builder.setMainClass(versionManifest.getMainClass()); addWindowArgs(); @@ -149,6 +153,8 @@ public Process call() throws Exception { callLaunchModifier(); + verifyJavaRuntime(); + ProcessBuilder processBuilder = new ProcessBuilder(builder.buildCommand()); processBuilder.directory(instance.getContentDir()); Runner.log.info("Launching: " + builder); @@ -166,6 +172,23 @@ private void callLaunchModifier() { instance.modify(builder); } + private void verifyJavaRuntime() { + JavaRuntime pickedRuntime = builder.getRuntime(); + JavaVersion targetVersion = versionManifest.getJavaVersion(); + + if (pickedRuntime == null || targetVersion == null) { + return; + } + + if (pickedRuntime.getMajorVersion() != targetVersion.getMajorVersion()) { + boolean launchAnyway = javaRuntimeMismatch.test(pickedRuntime, targetVersion); + + if (!launchAnyway) { + throw new CancellationException("Launch cancelled by user."); + } + } + } + /** * Add platform-specific arguments. */ @@ -208,6 +231,9 @@ private void addLibraries() throws LauncherException { tr("runner.missingLibrary", instance.getTitle(), library.getName())); } } + + // The official launcher puts the vanilla jar at the end of the classpath, we'll do the same + builder.classPath(getJarPath()); } /** @@ -283,7 +309,7 @@ private void addJvmArgs() throws IOException, LauncherException { } } - if (versionManifest.getLogging() != null) { + if (versionManifest.getLogging() != null && versionManifest.getLogging().getClient() != null) { log.info("Logging config present, log4j2 bug likely mitigated"); VersionManifest.LoggingConfig config = versionManifest.getLogging().getClient(); @@ -421,7 +447,7 @@ private Map getCommandSubstitutions() throws JsonProcessingExcep map.put("auth_uuid", session.getUuid()); map.put("profile_name", session.getName()); - map.put("user_type", session.getUserType().getName()); + map.put("user_type", session.getUserType().getId()); map.put("user_properties", mapper.writeValueAsString(session.getUserProperties())); map.put("game_directory", instance.getContentDir().getAbsolutePath()); diff --git a/launcher/src/main/java/com/skcraft/launcher/launch/runtime/JavaRuntime.java b/launcher/src/main/java/com/skcraft/launcher/launch/runtime/JavaRuntime.java index 7d92730bd..ed0e39f93 100644 --- a/launcher/src/main/java/com/skcraft/launcher/launch/runtime/JavaRuntime.java +++ b/launcher/src/main/java/com/skcraft/launcher/launch/runtime/JavaRuntime.java @@ -108,6 +108,8 @@ public int compareTo(JavaRuntime o) { @Override public String toString() { + String version = this.version != null ? this.version : "unknown"; + return String.format("Java %s (%s) (%s)", version, is64Bit ? "64-bit" : "32-bit", dir); } } diff --git a/launcher/src/main/java/com/skcraft/launcher/launch/runtime/JavaRuntimeFinder.java b/launcher/src/main/java/com/skcraft/launcher/launch/runtime/JavaRuntimeFinder.java index 7af48c84a..01ecf2efe 100644 --- a/launcher/src/main/java/com/skcraft/launcher/launch/runtime/JavaRuntimeFinder.java +++ b/launcher/src/main/java/com/skcraft/launcher/launch/runtime/JavaRuntimeFinder.java @@ -43,6 +43,7 @@ public static List getAvailableRuntimes() { // Add system Javas runtimeFinder.getCandidateJavaLocations().stream() .map(JavaRuntimeFinder::getRuntimeFromPath) + .filter(Objects::nonNull) .forEach(entries::add); // Add extra runtimes @@ -73,6 +74,7 @@ public static JavaRuntime getRuntimeFromPath(String path) { } public static JavaRuntime getRuntimeFromPath(File target) { + // Normalize target to root first if (target.isFile()) { // Probably referring directly to bin/java, back up two levels target = target.getParentFile().getParentFile(); @@ -81,20 +83,31 @@ public static JavaRuntime getRuntimeFromPath(File target) { target = target.getParentFile(); } - { - File jre = new File(target, "jre/release"); - if (jre.isFile()) { - target = jre.getParentFile(); - } + // Find the release file + File releaseFile = new File(target, "release"); + if (!releaseFile.isFile()) { + releaseFile = new File(target, "jre/release"); + // may still not exist - parseFromRelease below will return null if so } - JavaReleaseFile release = JavaReleaseFile.parseFromRelease(target); + // Find the bin folder + File binFolder = new File(target, "bin"); + if (!binFolder.isDirectory()) { + binFolder = new File(target, "jre/bin"); + } + + if (!binFolder.isDirectory()) { + // No bin folder, this isn't a usable install + return null; + } + + JavaReleaseFile release = JavaReleaseFile.parseFromRelease(releaseFile.getParentFile()); if (release == null) { // Make some assumptions... - return new JavaRuntime(target, null, true); + return new JavaRuntime(binFolder.getParentFile(), null, true); } - return new JavaRuntime(target, release.getVersion(), release.isArch64Bit()); + return new JavaRuntime(binFolder.getParentFile(), release.getVersion(), release.isArch64Bit()); } private static PlatformRuntimeFinder getRuntimeFinder(Environment env) { diff --git a/launcher/src/main/java/com/skcraft/launcher/model/loader/FabricMod.java b/launcher/src/main/java/com/skcraft/launcher/model/loader/FabricMod.java index c354964f0..0afbb91f0 100644 --- a/launcher/src/main/java/com/skcraft/launcher/model/loader/FabricMod.java +++ b/launcher/src/main/java/com/skcraft/launcher/model/loader/FabricMod.java @@ -5,7 +5,7 @@ @Data @JsonIgnoreProperties(ignoreUnknown = true) -public class FabricMod { +public class FabricMod implements Versionable { private String id; private String name; private String version; diff --git a/launcher/src/main/java/com/skcraft/launcher/model/loader/LoaderManifest.java b/launcher/src/main/java/com/skcraft/launcher/model/loader/LoaderManifest.java index c1b567c70..6d6cb7aa8 100644 --- a/launcher/src/main/java/com/skcraft/launcher/model/loader/LoaderManifest.java +++ b/launcher/src/main/java/com/skcraft/launcher/model/loader/LoaderManifest.java @@ -19,7 +19,7 @@ public class LoaderManifest { public Library findLibrary(String name) { for (Library library : getLibraries()) { - if (library.getName().equals(name)) { + if (library.matches(name)) { return library; } } diff --git a/launcher/src/main/java/com/skcraft/launcher/model/loader/LoaderSubResolver.java b/launcher/src/main/java/com/skcraft/launcher/model/loader/LoaderSubResolver.java index 9042daab0..ab25cbda5 100644 --- a/launcher/src/main/java/com/skcraft/launcher/model/loader/LoaderSubResolver.java +++ b/launcher/src/main/java/com/skcraft/launcher/model/loader/LoaderSubResolver.java @@ -2,6 +2,7 @@ import com.google.common.base.Function; import com.skcraft.launcher.model.minecraft.Library; +import com.skcraft.launcher.model.minecraft.MavenName; import com.skcraft.launcher.model.minecraft.Side; import com.skcraft.launcher.model.modpack.DownloadableFile; import com.skcraft.launcher.model.modpack.Manifest; @@ -51,7 +52,7 @@ public String apply(String arg) { if (library != null) { arg = getPathOf(library.getPath(env)); } else { - arg = getPathOf(Library.mavenNameToPath(libraryName)); + arg = getPathOf(MavenName.from(libraryName).getFilePath()); } } else if (start == '&' && end == '&') { String localFileName = arg.substring(1, bound); diff --git a/launcher/src/main/java/com/skcraft/launcher/model/loader/QuiltMod.java b/launcher/src/main/java/com/skcraft/launcher/model/loader/QuiltMod.java new file mode 100644 index 000000000..6d0169b39 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/loader/QuiltMod.java @@ -0,0 +1,29 @@ +package com.skcraft.launcher.model.loader; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class QuiltMod implements Versionable { + @JsonProperty("schema_version") + private int schemaVersion; + + @JsonProperty("quilt_loader") + private Mod meta; + + @Override + public String getVersion() { + return meta.getVersion(); + } + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Mod { + private String version; + + @JsonProperty("intermediate_mappings") + private String intermediateMappings; + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/loader/VersionInfo.java b/launcher/src/main/java/com/skcraft/launcher/model/loader/VersionInfo.java index 7d383bcd0..ecc1ba8cd 100644 --- a/launcher/src/main/java/com/skcraft/launcher/model/loader/VersionInfo.java +++ b/launcher/src/main/java/com/skcraft/launcher/model/loader/VersionInfo.java @@ -12,9 +12,9 @@ import com.skcraft.launcher.model.minecraft.GameArgument; import com.skcraft.launcher.model.minecraft.Library; import com.skcraft.launcher.model.minecraft.MinecraftArguments; +import com.skcraft.launcher.model.minecraft.VersionManifest; import lombok.Data; -import java.util.ArrayList; import java.util.List; @Data @@ -24,12 +24,12 @@ public class VersionInfo { private MinecraftArguments arguments; private String mainClass; private List libraries; + private SidedData logging; @JsonIgnore private transient boolean overridingArguments; public void setMinecraftArguments(String argumentString) { MinecraftArguments minecraftArguments = new MinecraftArguments(); - minecraftArguments.setGameArguments(new ArrayList()); for (String arg : Splitter.on(' ').split(argumentString)) { minecraftArguments.getGameArguments().add(new GameArgument(arg)); diff --git a/launcher/src/main/java/com/skcraft/launcher/model/loader/Versionable.java b/launcher/src/main/java/com/skcraft/launcher/model/loader/Versionable.java new file mode 100644 index 000000000..b2faa2b73 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/loader/Versionable.java @@ -0,0 +1,5 @@ +package com.skcraft.launcher.model.loader; + +public interface Versionable { + String getVersion(); +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/Library.java b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/Library.java index f6cd2d8c8..9ba25ac94 100644 --- a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/Library.java +++ b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/Library.java @@ -8,8 +8,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.google.common.base.Joiner; -import com.google.common.base.Splitter; +import com.fasterxml.jackson.annotation.JsonInclude; import com.google.common.collect.Lists; import com.skcraft.launcher.util.Environment; import lombok.Data; @@ -23,7 +22,7 @@ @JsonIgnoreProperties(ignoreUnknown = true) public class Library { - private String name; + private MavenName name; private Downloads downloads; private Map natives; private Extract extract; @@ -33,7 +32,8 @@ public class Library { private String comment; // Custom - private boolean locallyAvailable; + @JsonInclude(value = JsonInclude.Include.NON_DEFAULT) + private boolean generated; public boolean matches(Environment environment) { boolean allow = false; @@ -102,7 +102,7 @@ public Artifact getArtifact(Environment environment) { // BACKWARDS COMPATIBILITY: make up a virtual artifact Artifact virtualArtifact = new Artifact(); virtualArtifact.setUrl(getDownloads().getArtifact().getUrl()); - virtualArtifact.setPath(mavenNameToPath(name + ":" + nativeString)); + virtualArtifact.setPath(MavenName.from(name + ":" + nativeString).getFilePath()); return virtualArtifact; } @@ -126,8 +126,9 @@ public boolean equals(Object o) { EqualsBuilder builder = new EqualsBuilder(); builder.append(name, library.getName()); - // If libraries have different natives lists, they should be separate. + // If libraries have different natives or environment rules, they should be separate. builder.append(natives, library.getNatives()); + builder.append(rules, library.getRules()); return builder.isEquals(); } @@ -191,7 +192,7 @@ public void setUrl(String url) { virtualArtifact.setUrl(url); if (getName() != null) { - virtualArtifact.setPath(mavenNameToPath(getName())); + virtualArtifact.setPath(name.getFilePath()); } Downloads downloads = new Downloads(); @@ -201,7 +202,7 @@ public void setUrl(String url) { } public void setName(String name) { - this.name = name; + this.name = MavenName.from(name); // [DEEP SIGH] // Sometimes 'name' comes after 'url', and I can't figure out how to get Jackson to enforce order @@ -210,7 +211,7 @@ public void setName(String name) { if (getDownloads().getArtifact() == null) return; if (getDownloads().getArtifact().getPath() == null) { - getDownloads().getArtifact().setPath(mavenNameToPath(name)); + getDownloads().getArtifact().setPath(this.name.getFilePath()); } } } @@ -226,32 +227,11 @@ public void setServerreq(boolean value) { } } - public static String mavenNameToPath(String mavenName) { - List split = Splitter.on(':').splitToList(mavenName); - int size = split.size(); - - String group = split.get(0); - String name = split.get(1); - String version = split.get(2); - String extension = "jar"; - - String fileName = name + "-" + version; - - if (size > 3) { - String classifier = split.get(3); - - if (classifier.indexOf("@") != -1) { - List parts = Splitter.on('@').splitToList(classifier); - - classifier = parts.get(0); - extension = parts.get(1); - } - - fileName += "-" + classifier; - } - - fileName += "." + extension; - - return Joiner.on('/').join(group.replace('.', '/'), name, version, fileName); + /** + * @param mavenName Maven name of a library + * @return True if this library is named 'mavenName'. + */ + public boolean matches(String mavenName) { + return this.name.equals(MavenName.from(mavenName)); } } diff --git a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/MavenName.java b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/MavenName.java new file mode 100644 index 000000000..1f1ebaad5 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/MavenName.java @@ -0,0 +1,63 @@ +package com.skcraft.launcher.model.minecraft; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import com.google.common.base.Joiner; +import com.google.common.base.Splitter; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +@Data +@EqualsAndHashCode(of = {"group", "path", "classifier"}) +public class MavenName { + @JsonValue + private final String fullName; + private final String group; + private final String path; + private final String version; + private final String classifier; + private final String extension; + + private final String filePath; + + @Override + public String toString() { + return fullName; + } + + @JsonCreator + public static MavenName from(String mavenName) { + if (mavenName == null) return null; + + List split = Splitter.on(':').splitToList(mavenName); + int size = split.size(); + + String group = split.get(0); + String name = split.get(1); + String version = split.get(2); + String classifier = null; + String extension = "jar"; + + String fileName = name + "-" + version; + + if (size > 3) { + classifier = split.get(3); + + if (classifier.indexOf("@") != -1) { + List parts = Splitter.on('@').splitToList(classifier); + + classifier = parts.get(0); + extension = parts.get(1); + } + + fileName += "-" + classifier; + } + + fileName += "." + extension; + String filePath = Joiner.on('/').join(group.replace('.', '/'), name, version, fileName); + + return new MavenName(mavenName, group, name, version, classifier, extension, filePath); + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/MinecraftArguments.java b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/MinecraftArguments.java index 801e2d244..077f7a4f1 100644 --- a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/MinecraftArguments.java +++ b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/MinecraftArguments.java @@ -9,6 +9,7 @@ import com.skcraft.launcher.model.minecraft.mapper.MinecraftArgumentsSerializer; import lombok.Data; +import java.util.ArrayList; import java.util.List; @Data @@ -18,12 +19,12 @@ public class MinecraftArguments { @JsonTypeInfo(use = JsonTypeInfo.Id.NONE) @JsonSerialize(contentUsing = MinecraftArgumentsSerializer.class) @JsonDeserialize(contentUsing = MinecraftArgumentsDeserializer.class) - private List gameArguments; + private List gameArguments = new ArrayList<>(); @JsonProperty("jvm") @JsonTypeInfo(use = JsonTypeInfo.Id.NONE) @JsonSerialize(contentUsing = MinecraftArgumentsSerializer.class) @JsonDeserialize(contentUsing = MinecraftArgumentsDeserializer.class) - private List jvmArguments; + private List jvmArguments = new ArrayList<>(); } diff --git a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/VersionManifest.java b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/VersionManifest.java index dd3cf63c6..d82ebd440 100644 --- a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/VersionManifest.java +++ b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/VersionManifest.java @@ -12,7 +12,10 @@ import com.skcraft.launcher.model.loader.SidedData; import lombok.Data; -import java.util.*; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; @Data @JsonIgnoreProperties(ignoreUnknown = true) @@ -38,20 +41,8 @@ public String getAssetId() { : "legacy"; } - public Library findLibrary(String name) { - for (Library library : getLibraries()) { - if (library.getName().equals(name)) { - return library; - } - } - - return null; - } - public void setMinecraftArguments(String minecraftArguments) { MinecraftArguments result = new MinecraftArguments(); - result.setGameArguments(new ArrayList()); - result.setJvmArguments(new ArrayList()); for (String arg : Splitter.on(' ').split(minecraftArguments)) { result.getGameArguments().add(new GameArgument(arg)); diff --git a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/mapper/ArgumentValueDeserializer.java b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/mapper/ArgumentValueDeserializer.java index 7bf74d471..de9931300 100644 --- a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/mapper/ArgumentValueDeserializer.java +++ b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/mapper/ArgumentValueDeserializer.java @@ -30,6 +30,6 @@ public List deserialize(JsonParser jp, DeserializationContext ctxt) thro return Lists.newArrayList(value); } - throw new InvalidFormatException("Invalid JSON type for deserializer (not string or array)", null, List.class); + throw new InvalidFormatException(jp, "Invalid JSON type for deserializer (not string or array)", null, List.class); } } diff --git a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/mapper/MinecraftArgumentsDeserializer.java b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/mapper/MinecraftArgumentsDeserializer.java index a6436a260..f297d372e 100644 --- a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/mapper/MinecraftArgumentsDeserializer.java +++ b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/mapper/MinecraftArgumentsDeserializer.java @@ -26,6 +26,6 @@ public GameArgument deserialize(JsonParser jp, DeserializationContext ctxt) thro return new GameArgument(argument); } - throw new InvalidFormatException("Invalid JSON type for deserializer (not string or object)", null, GameArgument.class); + throw new InvalidFormatException(jp, "Invalid JSON type for deserializer (not string or object)", null, GameArgument.class); } } diff --git a/launcher/src/main/java/com/skcraft/launcher/persistence/Persistence.java b/launcher/src/main/java/com/skcraft/launcher/persistence/Persistence.java index 8f1e264bf..529a2b5a0 100644 --- a/launcher/src/main/java/com/skcraft/launcher/persistence/Persistence.java +++ b/launcher/src/main/java/com/skcraft/launcher/persistence/Persistence.java @@ -7,8 +7,8 @@ package com.skcraft.launcher.persistence; import com.fasterxml.jackson.core.PrettyPrinter; +import com.fasterxml.jackson.core.util.DefaultIndenter; import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; -import com.fasterxml.jackson.core.util.DefaultPrettyPrinter.Lf2SpacesIndenter; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.io.ByteSink; import com.google.common.io.ByteSource; @@ -43,7 +43,7 @@ public final class Persistence { static { L2F_LIST_PRETTY_PRINTER = new DefaultPrettyPrinter(); - L2F_LIST_PRETTY_PRINTER.indentArraysWith(Lf2SpacesIndenter.instance); + L2F_LIST_PRETTY_PRINTER.indentArraysWith(DefaultIndenter.SYSTEM_LINEFEED_INSTANCE); } private Persistence() { diff --git a/launcher/src/main/java/com/skcraft/launcher/swing/SwingHelper.java b/launcher/src/main/java/com/skcraft/launcher/swing/SwingHelper.java index 8024e0b2f..1ee7a1c50 100644 --- a/launcher/src/main/java/com/skcraft/launcher/swing/SwingHelper.java +++ b/launcher/src/main/java/com/skcraft/launcher/swing/SwingHelper.java @@ -35,6 +35,7 @@ import java.io.*; import java.lang.reflect.InvocationTargetException; import java.net.MalformedURLException; +import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.util.List; @@ -69,6 +70,11 @@ public static String htmlEscape(String str) { .replace("&", "&"); } + public static String htmlWrap(String message) { + // To force the label to wrap, convert the message to broken HTML + return "
" + htmlEscape(message); + } + public static void setClipboard(String text) { Toolkit.getDefaultToolkit().getSystemClipboard().setContents( new StringSelection(text), clipboardOwner); @@ -104,16 +110,7 @@ public static void openURL(@NonNull String url, @NonNull Component parentCompone */ public static void openURL(URL url, Component parentComponent) { try { - Desktop.getDesktop().browse(url.toURI()); - } catch (UnsupportedOperationException e) { - if (Environment.detectPlatform() == Platform.LINUX) { - // Try xdg-open instead - try { - Runtime.getRuntime().exec(new String[]{"xdg-open", url.toString()}); - } catch (IOException ex) { - showErrorDialog(parentComponent, tr("errors.openUrlError", url.toString()), tr("errorTitle"), ex); - } - } + openURL(url.toURI()); } catch (IOException e) { showErrorDialog(parentComponent, tr("errors.openUrlError", url.toString()), SharedLocale.tr("errorTitle")); } catch (URISyntaxException e) { @@ -121,6 +118,17 @@ public static void openURL(URL url, Component parentComponent) { } } + public static void openURL(URI url) throws IOException { + try { + Desktop.getDesktop().browse(url); + } catch (UnsupportedOperationException e) { + if (Environment.detectPlatform() == Platform.LINUX) { + // Try xdg-open instead + Runtime.getRuntime().exec(new String[]{"xdg-open", url.toString()}); + } + } + } + /** * Shows an popup error dialog, with potential extra details shown either immediately * or available on the dialog. @@ -186,8 +194,7 @@ public static void showMessageDialog(final Component parentComponent, final int messageType) { if (SwingUtilities.isEventDispatchThread()) { - // To force the label to wrap, convert the message to broken HTML - String htmlMessage = "
" + htmlEscape(message); + String htmlMessage = htmlWrap(message); JPanel panel = new JPanel(new BorderLayout(0, detailsText != null ? 20 : 0)); diff --git a/launcher/src/main/java/com/skcraft/launcher/update/BaseUpdater.java b/launcher/src/main/java/com/skcraft/launcher/update/BaseUpdater.java index fac401dad..4bb4a76f2 100644 --- a/launcher/src/main/java/com/skcraft/launcher/update/BaseUpdater.java +++ b/launcher/src/main/java/com/skcraft/launcher/update/BaseUpdater.java @@ -34,8 +34,12 @@ import javax.swing.*; import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.util.*; import java.util.logging.Level; @@ -79,6 +83,9 @@ protected Manifest installPackage(@NonNull Installer installer, @NonNull Instanc final File cachePath = new File(instance.getDir(), "update_cache.json"); final File featuresPath = new File(instance.getDir(), "features.json"); + // Make sure the temp dir exists + installer.getTempDir().mkdirs(); + final InstallLog previousLog = Persistence.read(logPath, InstallLog.class); final InstallLog currentLog = new InstallLog(); currentLog.setBaseDir(contentDir); @@ -242,6 +249,8 @@ protected void installLibraries(@NonNull Installer installer, } for (Library library : allLibraries) { + if (library.isGenerated()) continue; // Skip generated libraries. + if (library.matches(environment)) { checkInterrupted(); @@ -264,28 +273,38 @@ protected void installLibraries(@NonNull Installer installer, } File tempFile = installer.getDownloader().download(urls, "", size, - library.getName() + ".jar"); + library.getName().toString()); log.info("Fetching " + path + " from " + urls); installer.queue(new FileMover(tempFile, targetFile)); if (artifact.getSha1() != null) { - installer.queue(new FileVerify(targetFile, library.getName(), artifact.getSha1())); + installer.queue(new FileVerify(targetFile, library.getName().toString(), artifact.getSha1())); } } } } - // Fetch logging config - if (versionManifest.getLogging() != null) { + // Use our custom logging config depending on what the manifest specifies + if (versionManifest.getLogging() != null && versionManifest.getLogging().getClient() != null) { VersionManifest.LoggingConfig config = versionManifest.getLogging().getClient(); VersionManifest.Artifact file = config.getFile(); File targetFile = new File(librariesDir, file.getId()); + InputStream embeddedConfig = Launcher.class.getResourceAsStream("logging/" + file.getId()); - if (!targetFile.exists() || !Objects.equals(config.getFile().getHash(), FileUtils.getShaHash(targetFile))) { + if (embeddedConfig == null) { + // No embedded config, just use whatever the server gives us File tempFile = installer.getDownloader().download(url(file.getUrl()), file.getHash(), file.getSize(), file.getId()); log.info("Downloading logging config " + file.getId() + " from " + file.getUrl()); installer.queue(new FileMover(tempFile, targetFile)); + } else if (!targetFile.exists() || FileUtils.getShaHash(targetFile).equals(file.getHash())) { + // Use our embedded replacement + + Path tempFile = installer.getTempDir().toPath().resolve(file.getId()); + Files.copy(embeddedConfig, tempFile, StandardCopyOption.REPLACE_EXISTING); + + log.info("Substituting embedded logging config " + file.getId()); + installer.queue(new FileMover(tempFile.toFile(), targetFile)); } } } diff --git a/launcher/src/main/java/com/skcraft/launcher/update/UpdateManager.java b/launcher/src/main/java/com/skcraft/launcher/update/UpdateManager.java index 1d6ee2071..e24e8f370 100644 --- a/launcher/src/main/java/com/skcraft/launcher/update/UpdateManager.java +++ b/launcher/src/main/java/com/skcraft/launcher/update/UpdateManager.java @@ -50,7 +50,7 @@ public boolean getPendingUpdate() { return pendingUpdate != null; } - public void checkForUpdate() { + public void checkForUpdate(final Window window) { ListenableFuture future = launcher.getExecutor().submit(new UpdateChecker(launcher)); Futures.addCallback(future, new FutureCallback() { @@ -63,9 +63,11 @@ public void onSuccess(LatestVersionInfo result) { @Override public void onFailure(Throwable t) { - + // Error handler attached below. } }, SwingExecutor.INSTANCE); + + SwingHelper.addErrorDialogCallback(window, future); } public void performUpdate(final Window window) { diff --git a/launcher/src/main/java/com/skcraft/launcher/util/HttpRequest.java b/launcher/src/main/java/com/skcraft/launcher/util/HttpRequest.java index 6bf14572a..e1b94d42d 100644 --- a/launcher/src/main/java/com/skcraft/launcher/util/HttpRequest.java +++ b/launcher/src/main/java/com/skcraft/launcher/util/HttpRequest.java @@ -232,6 +232,28 @@ public HttpRequest expectResponseCodeOr(int code, HttpFunc throw exc; } + /** + * Continue if the content type matches, otherwise throw an exception + * + * @param expectedTypes Expected content-type(s) + * @return this object + * @throws IOException Unexpected content-type or other error + */ + public HttpRequest expectContentType(String... expectedTypes) throws IOException { + if (conn == null) throw new IllegalArgumentException("No connection has been made!"); + + String contentType = conn.getHeaderField("Content-Type"); + for (String expectedType : expectedTypes) { + if (expectedType.equals(contentType)) { + return this; + } + } + + close(); + throw new IOException(String.format("Did not get expected content type '%s', instead got '%s'.", + String.join(" | ", expectedTypes), contentType)); + } + /** * Get the response code. * @@ -558,7 +580,7 @@ public T asJson(Class cls) throws IOException { * @return the object * @throws java.io.IOException on I/O error */ - public T asJson(TypeReference type) throws IOException { + public T asJson(TypeReference type) throws IOException { return mapper.readValue(asString("UTF-8"), type); } diff --git a/launcher/src/main/java/com/skcraft/launcher/util/LocaleEncodingControl.java b/launcher/src/main/java/com/skcraft/launcher/util/LocaleEncodingControl.java new file mode 100644 index 000000000..72e5932e1 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/util/LocaleEncodingControl.java @@ -0,0 +1,92 @@ +package com.skcraft.launcher.util; + +import lombok.extern.java.Log; + +import java.io.*; +import java.net.URL; +import java.net.URLConnection; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Locale; +import java.util.PropertyResourceBundle; +import java.util.ResourceBundle; + +@Log +public class LocaleEncodingControl extends ResourceBundle.Control { + @Override + public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader, boolean reload) throws IllegalAccessException, InstantiationException, IOException { + if (!format.equals("java.properties")) { + return super.newBundle(baseName, locale, format, loader, reload); + } + + String bundleName = this.toBundleName(baseName, locale); + String resourceName = this.toResourceName(bundleName, "properties"); + + InputStream is = this.getResourceAsStream(resourceName, loader, reload); + if (is == null) { + return null; + } + + BufferedInputStream bis = new BufferedInputStream(is); + + // Let's do the timewalk + boolean isUtf8; + bis.mark(3); + { + byte[] buf = new byte[3]; + readFully(bis, buf); + + // the BOM is 0xEF,0xBB,0xBF + isUtf8 = buf[0] == (byte) 0xEF && buf[1] == (byte) 0xBB && buf[2] == (byte) 0xBF; + } + bis.reset(); + + if (isUtf8) { + log.info("Found UTF-8 locale file " + resourceName); + } + + Charset charset = isUtf8 ? StandardCharsets.UTF_8 : StandardCharsets.ISO_8859_1; + Reader reader = new InputStreamReader(bis, charset); + + try { + PropertyResourceBundle bundle = new PropertyResourceBundle(reader); + reader.close(); + + return bundle; + } finally { + // Just in case of exception... + reader.close(); + } + } + + private InputStream getResourceAsStream(String resourceName, ClassLoader loader, boolean reload) throws IOException { + if (reload) { + URL url = loader.getResource(resourceName); + if (url != null) { + URLConnection conn = url.openConnection(); + if (conn != null) { + conn.setUseCaches(false); + + return conn.getInputStream(); + } + } + } + + return loader.getResourceAsStream(resourceName); + } + + private void readFully(InputStream is, byte[] buf) throws IOException { + int offset = 0; + int length = buf.length; + + while (length > 0) { + int n = is.read(buf, offset, length); + if (n == -1) { + throw new EOFException(); + } + + offset += n; + length -= n; + } + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/util/PastebinPoster.java b/launcher/src/main/java/com/skcraft/launcher/util/PastebinPoster.java index 53a966682..285c595cf 100644 --- a/launcher/src/main/java/com/skcraft/launcher/util/PastebinPoster.java +++ b/launcher/src/main/java/com/skcraft/launcher/util/PastebinPoster.java @@ -42,7 +42,7 @@ public void run() { InputStream in = null; try { - URL url = new URL("http://pastebin.com/api/api_post.php"); + URL url = new URL("https://pastebin.com/api/api_post.php"); conn = (HttpURLConnection) url.openConnection(); conn.setConnectTimeout(CONNECT_TIMEOUT); conn.setReadTimeout(READ_TIMEOUT); diff --git a/launcher/src/main/java/com/skcraft/launcher/util/SharedLocale.java b/launcher/src/main/java/com/skcraft/launcher/util/SharedLocale.java index 425a24e8c..a58b415a2 100644 --- a/launcher/src/main/java/com/skcraft/launcher/util/SharedLocale.java +++ b/launcher/src/main/java/com/skcraft/launcher/util/SharedLocale.java @@ -96,7 +96,7 @@ public static boolean loadBundle(@NonNull String baseName, @NonNull Locale local try { SharedLocale.locale = locale; bundle = ResourceBundle.getBundle(baseName, locale, - SharedLocale.class.getClassLoader()); + SharedLocale.class.getClassLoader(), new LocaleEncodingControl()); return true; } catch (MissingResourceException e) { log.log(Level.SEVERE, "Failed to load resource bundle", e); diff --git a/launcher/src/main/resources/com/skcraft/launcher/lang/Launcher.properties b/launcher/src/main/resources/com/skcraft/launcher/lang/Launcher.properties index d8699d433..ef48f3c4b 100644 --- a/launcher/src/main/resources/com/skcraft/launcher/lang/Launcher.properties +++ b/launcher/src/main/resources/com/skcraft/launcher/lang/Launcher.properties @@ -23,10 +23,12 @@ errors.selfUpdateCheckError=Checking for an update to the launcher has failed. button.cancel=Cancel button.ok=OK button.save=Save +button.launchAnyway=Launch Anyway options.title = Options options.useProxyCheck = Use following proxy in Minecraft -options.jvmPath=Java Runtime\: +options.jvmPath=Fallback Java Runtime\: +options.jvmRuntime=Java Runtime\: options.jvmArguments=JVM arguments\: options.64BitJavaWarning=Make sure to have 64-bit Java installed if you are planning to set the memory limits higher. options.minMemory=Minimum memory (MB)\: @@ -88,6 +90,7 @@ launcher.noInstanceError=Please select a modpack to launch. launcher.noInstanceTitle=No Modpack Selected launcher.launchingTItle=Launching the game... launcher.launchingStatus=Launching ''{0}''. Please wait. +launcher.javaMismatchTitle=Java version warning launcher.modpackColumn=Modpack launcher.notInstalledHint=(not installed) launcher.requiresUpdateHint=(requires update) @@ -116,6 +119,7 @@ login.loggingInStatus=Logging in to Minecraft... login.noLoginError=Please enter your account details. login.noLoginTitle=Missing Account login.minecraftNotOwnedError=Sorry, Minecraft is not owned on that account. +login.relogin=Please log in again. ({0}) login.microsoft.seeBrowser=Check your browser to login with Microsoft. login.xbox.generic=Failed to authenticate with Xbox Live. @@ -190,6 +194,7 @@ runner.updateRequired=This instance must be updated before it can be run. runner.missingLibrary={0} needs to be relaunched and updated because the library ''{1}'' is missing. runner.missingAssetsIndex={0} needs to be relaunched and updated because its asset index is missing. runner.corruptAssetsIndex={0} needs to be relaunched and updated because its asset index is corrupt. +runner.wrongJavaVersion=Instance ''{0}'' requires Java version {1}, but could only find {2}. assets.expanding1=Expanding {0} asset... ({1} remaining) assets.expandingN=Expanding {0} assets... ({1} remaining) diff --git a/launcher/src/main/resources/com/skcraft/launcher/launcher.properties b/launcher/src/main/resources/com/skcraft/launcher/launcher.properties index bfe5a72d4..1ff0c13cd 100644 --- a/launcher/src/main/resources/com/skcraft/launcher/launcher.properties +++ b/launcher/src/main/resources/com/skcraft/launcher/launcher.properties @@ -15,12 +15,12 @@ offlinePlayerName=Player # Only change these if you know what you're doing. versionManifestUrl=https://launchermeta.mojang.com/mc/game/version_manifest.json librariesSource=https://libraries.minecraft.net/ -assetsSource=http://resources.download.minecraft.net/ +assetsSource=https://resources.download.minecraft.net/ yggdrasilAuthUrl=https://authserver.mojang.com/authenticate microsoftClientId=d18bb4d8-a27f-4451-a87f-fe6de4436813 resetPasswordUrl=https://minecraft.net/resetpassword # You MUST change these from the defaults. These URLs are provides as examples only. -newsUrl=http://update.skcraft.com/template/news.html?version=%s -packageListUrl=http://update.skcraft.com/template/packages.json?key=%s -selfUpdateUrl=http://update.skcraft.com/template/launcher/latest.json +newsUrl=https://update.skcraft.com/template/news.html?version=%s +packageListUrl=https://update.skcraft.com/template/packages.json?key=%s +selfUpdateUrl=https://update.skcraft.com/template/launcher/latest.json diff --git a/launcher/src/main/resources/com/skcraft/launcher/logging/client-1.12.xml b/launcher/src/main/resources/com/skcraft/launcher/logging/client-1.12.xml new file mode 100644 index 000000000..2e9ac98e5 --- /dev/null +++ b/launcher/src/main/resources/com/skcraft/launcher/logging/client-1.12.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/launcher/src/main/resources/com/skcraft/launcher/logging/client-1.21.2.xml b/launcher/src/main/resources/com/skcraft/launcher/logging/client-1.21.2.xml new file mode 100644 index 000000000..bdf546320 --- /dev/null +++ b/launcher/src/main/resources/com/skcraft/launcher/logging/client-1.21.2.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/launcher/src/main/resources/com/skcraft/launcher/logging/client-1.7.xml b/launcher/src/main/resources/com/skcraft/launcher/logging/client-1.7.xml new file mode 100644 index 000000000..87f3dea8e --- /dev/null +++ b/launcher/src/main/resources/com/skcraft/launcher/logging/client-1.7.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/launcher/src/main/resources/com/skcraft/launcher/login.html b/launcher/src/main/resources/com/skcraft/launcher/login.html new file mode 100644 index 000000000..df0fcace1 --- /dev/null +++ b/launcher/src/main/resources/com/skcraft/launcher/login.html @@ -0,0 +1,25 @@ + + + + Login success + + + + +
+ icon.png +

Authentication successful, you may return to the launcher

+
+ +