Compare commits

..
Author SHA1 Message Date
huanghongxun f67c3d5284 fix(launch): cannot launch game in path that contains CJK characters. Closes #1141. 2021-10-30 23:03:49 +08:00
Glavo 46385a8445 Do not delete soft links when exiting HMCL 2021-10-30 23:01:27 +08:00
Glavo 9f617c33f5 Support path containing unicode characters on Linux 2021-10-30 23:01:27 +08:00
Glavo 6aef034880 Avoid generating command lines repeatedly 2021-10-30 23:01:27 +08:00
huanghongxun c4941a01d6 fix(multiplayer): show "failed to test" instead of "testing". 2021-10-30 22:40:44 +08:00
huanghongxun 7380423eee fix(multiplayer): does not show correct kick message. 2021-10-30 22:13:32 +08:00
huanghongxun f35073ffb5 fix(ui): alignment. 2021-10-30 21:05:40 +08:00
huanghongxun f89ea56d86 fix(multiplayer): session name null. 2021-10-30 21:04:53 +08:00
huanghongxun a240b8ca9f fix(skin): Do not load authlib-injector when skin manually set to steve/alex. 2021-10-29 22:52:16 +08:00
huanghongxun 230ec2bc8e feat(update): show update dialog when found update. 2021-10-29 02:51:08 +08:00
huanghongxun a3c2642b93 feat(download): customize name of the mod to be downloaded. 2021-10-29 01:21:48 +08:00
huanghongxun de755d5495 feat(game): more version icon choices. 2021-10-29 01:21:48 +08:00
huanghongxun 08054f3026 feat(countly): initial 2021-10-27 00:21:42 +08:00
Glavo 026239903d Fix IllegalAccessError: tried to access protected method 2021-10-25 17:23:07 +08:00
Glavo a320fe4cc4 Use shadowJar minimize 2021-10-24 17:26:26 +08:00
vanja-san b122d4c5e4 Update I18N_ru.properties
Full text translation
2021-10-24 17:25:40 +08:00
49 changed files with 1603 additions and 1757 deletions

No files matched your search

+13 -91
View File
@@ -6,8 +6,6 @@ buildscript {
dependencies {
classpath 'org.tukaani:xz:1.8'
classpath 'org.glavo:pack200:0.3.0'
classpath 'com.guardsquare:proguard-gradle:7.1.0' // The ProGuard Gradle plugin.
}
}
@@ -118,8 +116,20 @@ compileJava11Java {
targetCompatibility = 11
}
jar {
enabled = false
dependsOn shadowJar
}
shadowJar {
classifier = "shadow"
classifier = null
minimize {
exclude(dependency('com.google.code.gson:.*:.*'))
exclude(dependency('com.github.steveice10:.*:.*'))
exclude(project(":HMCLTransformerDiscoveryService"))
}
manifest {
attributes 'Created-By': 'Copyright(c) 2013-2021 huangyuhui.',
@@ -152,89 +162,6 @@ shadowJar {
'javafx.base/com.sun.javafx.event'
].join(" ")
}
}
task proguard(type: proguard.gradle.ProGuardTask) {
dependsOn shadowJar
injars shadowJar
outjars "${buildDir}/libs/${project.name}-${project.version}-proguard.jar"
dontobfuscate
dontoptimize
keep 'class org.jackhuang.** { *; }'
keep 'class com.jfoenix.** { *; }'
keep 'class com.google.gson.** { *; }'
keep 'class com.github.steveice10.opennbt.** { *; }' // TagRegistry use reflection to create object instance
keep 'class * extends com.google.gson.TypeAdapter'
keep 'class * implements com.google.gson.TypeAdapterFactory'
keep 'class * implements com.google.gson.JsonSerializer'
keep 'class * implements com.google.gson.JsonDeserializer'
printusage "${buildDir}/libs/proguard-shrinking-details.txt"
System.out.println(new File(".").absolutePath)
dontwarn 'com.nqzero.**'
dontwarn 'org.slf4j.**'
dontwarn 'org.jackhuang.hmcl.util.Pack200Utils'
dontwarn 'com.sun.javafx.**'
dontwarn 'com.jfoenix.**'
dontwarn 'org.apache.**'
adaptclassstrings
// next block taken verbatim from Proguard's documentation examples:
libraryjars files(configurations.compileClasspath.collect())
keepattributes 'SourceFile,LineNumberTable'
keepattributes '*Annotation*'
keepattributes 'Signature'
var javaHome = System.getProperty('java.home')
// Automatically handle the Java version of this build.
if (System.getProperty('java.version').startsWith('1.')) {
// Before Java 9, the runtime classes were packaged in a single jar file.
libraryjars "${javaHome}/lib/rt.jar"
libraryjars "${javaHome}/lib/ext/jfxrt.jar"
} else {
// As of Java 9, the runtime classes are packaged in modular jmod files.
libraryjars "${javaHome}/jmods/java.base.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/java.desktop.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/java.logging.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/java.management.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/java.sql.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/java.xml.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/jdk.management.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/jdk.unsupported.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
if (new File("${javaHome}/jmods/javafx.base.jmod").exists()) {
libraryjars "${javaHome}/jmods/javafx.base.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/javafx.controls.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/javafx.graphics.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/javafx.media.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/javafx.fxml.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/javafx.web.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
}
}
}
task finalJar(type: Jar) {
dependsOn proguard
manifest = shadowJar.manifest
from { proguard.outJarFiles.collect { zipTree(it)} }
into('/') {
from { shadowJar.outputs.files.collect { zipTree(it) } }
include("META-INF/versions/**")
}
doLast {
repack(jar.archivePath) // see repack()
@@ -243,11 +170,6 @@ task finalJar(type: Jar) {
}
}
jar {
enabled = false
dependsOn finalJar
}
def createExecutable(String suffix, String header) {
def output = new File(jar.archivePath.parentFile, jar.archivePath.name[0..-4] + suffix)
output.bytes = new File(project.projectDir, header).bytes
@@ -0,0 +1,87 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.countly;
import org.jackhuang.hmcl.util.io.HttpRequest;
import java.io.IOException;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import static org.jackhuang.hmcl.util.Pair.pair;
public class Countly {
private String deviceId;
private String endpoint;
private String serverURL;
public void sendMetric(String metrics) throws IOException {
HttpRequest.GET(serverURL + endpoint,
pair("begin_session", "1"),
pair("session_id", "1"),
pair("metrics", metrics),
pair("device_id", deviceId),
pair("timestamp", Long.toString(System.currentTimeMillis())),
pair("tz", Integer.toString(TimeZone.getDefault().getOffset(new Date().getTime()) / 60000)),
pair("hour", Integer.toString(currentHour())),
pair("dow", Integer.toString(currentDayOfWeek())),
pair("app_key", APP_KEY),
pair("sdk_name", "java-native"),
pair("sdk_version", "20.11.1"))
.getString();
}
private static int getTimezoneOffset() {
return TimeZone.getDefault().getOffset(new Date().getTime()) / 60000;
}
private static String getLocale() {
final Locale locale = Locale.getDefault();
return locale.getLanguage() + "_" + locale.getCountry();
}
private static int currentHour() {
return Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
}
private int currentDayOfWeek() {
int day = Calendar.getInstance().get(Calendar.DAY_OF_WEEK);
switch (day) {
case Calendar.SUNDAY:
return 0;
case Calendar.MONDAY:
return 1;
case Calendar.TUESDAY:
return 2;
case Calendar.WEDNESDAY:
return 3;
case Calendar.THURSDAY:
return 4;
case Calendar.FRIDAY:
return 5;
case Calendar.SATURDAY:
return 6;
}
return 0;
}
private static final String APP_KEY = "";
}
@@ -0,0 +1,104 @@
package org.jackhuang.hmcl.countly;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.util.CrashReporter;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
public class CrashReport {
private final Thread thread;
private final Throwable throwable;
private final String stackTrace;
private boolean nonFatal;
public CrashReport(Thread thread, Throwable throwable) {
this.thread = thread;
this.throwable = throwable;
stackTrace = StringUtils.getStackTrace(throwable);
nonFatal = false;
}
public CrashReport setNonFatal() {
nonFatal = true;
return this;
}
public boolean shouldBeReport() {
if (!stackTrace.contains("org.jackhuang"))
return false;
return true;
}
public Map<String, Object> getMetrics(long runningTime) {
return mapOf(
pair("_run", runningTime),
pair("_app_version", Metadata.VERSION),
pair("_os", OperatingSystem.SYSTEM_NAME),
pair("_os_version", OperatingSystem.SYSTEM_VERSION),
pair("_disk_current", getDiskAvailable()),
pair("_disk_total", getDiskTotal()),
pair("_ram_current", getMemoryAvailable()),
pair("_ram_total", Runtime.getRuntime().maxMemory() / BYTES_IN_MB),
pair("_error", stackTrace),
pair("_logs", Logging.getLogs()),
pair("_name", throwable.getLocalizedMessage()),
pair("_nonfatal", nonFatal)
);
}
public String getDisplayText() {
return "---- Hello Minecraft! Crash Report ----\n" +
" Version: " + Metadata.VERSION + "\n" +
" Time: " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + "\n" +
" Thread: " + thread + "\n" +
"\n Content: \n " +
stackTrace + "\n\n" +
"-- System Details --\n" +
" Operating System: " + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION + "\n" +
" System Architecture: " + Architecture.SYSTEM_ARCH_NAME + "\n" +
" Java Architecture: " + Architecture.CURRENT_ARCH_NAME + "\n" +
" Java Version: " + System.getProperty("java.version") + ", " + System.getProperty("java.vendor") + "\n" +
" Java VM Version: " + System.getProperty("java.vm.name") + " (" + System.getProperty("java.vm.info") + "), " + System.getProperty("java.vm.vendor") + "\n" +
" JVM Max Memory: " + Runtime.getRuntime().maxMemory() + "\n" +
" JVM Total Memory: " + Runtime.getRuntime().totalMemory() + "\n" +
" JVM Free Memory: " + Runtime.getRuntime().freeMemory() + "\n";
}
private static final Long BYTES_IN_MB = 1024L * 1024;
private static long getMemoryAvailable() {
Long total = Runtime.getRuntime().totalMemory();
Long availMem = Runtime.getRuntime().freeMemory();
return (total - availMem) / BYTES_IN_MB;
}
private static long getDiskAvailable() {
long total = 0, free = 0;
for (File f : File.listRoots()) {
total += f.getTotalSpace();
free += f.getUsableSpace();
}
return (total - free) / BYTES_IN_MB;
}
private static long getDiskTotal() {
long total = 0;
for (File f : File.listRoots()) {
total += f.getTotalSpace();
}
return total / BYTES_IN_MB;
}
}
@@ -24,12 +24,15 @@ import com.google.gson.reflect.TypeToken;
import javafx.scene.image.Image;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackLocalInstallTask;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackManifest;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.ProxyManager;
import org.jackhuang.hmcl.setting.VersionIconType;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
@@ -38,6 +41,7 @@ import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
@@ -58,6 +62,8 @@ public class HMCLGameRepository extends DefaultGameRepository {
private final Map<String, VersionSetting> localVersionSettings = new HashMap<>();
private final Set<String> beingModpackVersions = new HashSet<>();
public final EventManager<Event> onVersionIconChanged = new EventManager<>();
public HMCLGameRepository(Profile profile, File baseDirectory) {
super(baseDirectory);
this.profile = profile;
@@ -231,6 +237,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
*
* @return corresponding version setting, null if the version has no its own version setting.
*/
@Nullable
public VersionSetting getLocalVersionSetting(String id) {
if (!localVersionSettings.containsKey(id))
loadLocalVersionSetting(id);
@@ -240,6 +247,15 @@ public class HMCLGameRepository extends DefaultGameRepository {
return setting;
}
@Nullable
public VersionSetting getLocalVersionSettingOrCreate(String id) {
VersionSetting vs = getLocalVersionSetting(id);
if (vs == null) {
vs = createLocalVersionSetting(id);
}
return vs;
}
public VersionSetting getVersionSetting(String id) {
VersionSetting vs = getLocalVersionSetting(id);
if (vs == null || vs.isUsesGlobal()) {
@@ -258,14 +274,21 @@ public class HMCLGameRepository extends DefaultGameRepository {
if (id == null || !isLoaded())
return newImage("/assets/img/grass.png");
Version version = getVersion(id).resolve(this);
File iconFile = getVersionIconFile(id);
if (iconFile.exists())
return new Image("file:" + iconFile.getAbsolutePath());
else if (LibraryAnalyzer.isModded(this, version))
return newImage("/assets/img/furnace.png");
else
return newImage("/assets/img/grass.png");
VersionSetting vs = getLocalVersionSettingOrCreate(id);
VersionIconType iconType = Optional.ofNullable(vs).map(VersionSetting::getVersionIcon).orElse(VersionIconType.DEFAULT);
if (iconType == VersionIconType.DEFAULT) {
Version version = getVersion(id).resolve(this);
File iconFile = getVersionIconFile(id);
if (iconFile.exists())
return new Image("file:" + iconFile.getAbsolutePath());
else if (LibraryAnalyzer.isModded(this, version))
return newImage("/assets/img/furnace.png");
else
return newImage("/assets/img/grass.png");
} else {
return newImage(iconType.getResourceUrl());
}
}
public boolean saveVersionSetting(String id) {
@@ -23,12 +23,12 @@ import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.FileSystem;
import java.nio.file.Path;
/**
* @author huangyuhui
@@ -40,20 +40,20 @@ public final class HMCLModpackManager {
/**
* Read the manifest in a HMCL modpack.
*
* @param fs a HMCL modpack file.
* @param file a HMCL modpack file.
* @param encoding encoding of modpack zip file.
* @return the manifest of HMCL modpack.
* @throws IOException if the file is not a valid zip file.
* @throws JsonParseException if the manifest.json is missing or malformed.
*/
public static Modpack readHMCLModpackManifest(FileSystem fs, Charset encoding) throws IOException, JsonParseException {
String manifestJson = FileUtils.readText(fs.getPath("modpack.json"));
public static Modpack readHMCLModpackManifest(Path file, Charset encoding) throws IOException, JsonParseException {
String manifestJson = CompressingUtils.readTextZipEntry(file, "modpack.json", encoding);
Modpack manifest = JsonUtils.fromNonNullJson(manifestJson, HMCLModpack.class).setEncoding(encoding);
String gameJson = FileUtils.readText(fs.getPath("minecraft/pack.json"));
String gameJson = CompressingUtils.readTextZipEntry(file, "minecraft/pack.json", encoding);
Version game = JsonUtils.fromNonNullJson(gameJson, Version.class);
if (game.getJar() == null)
if (StringUtils.isBlank(manifest.getVersion()))
throw new JsonParseException("Cannot recognize the game version of modpack");
throw new JsonParseException("Cannot recognize the game version of modpack " + file + ".");
else
manifest.setManifest(HMCLModpackManifest.INSTANCE);
else
@@ -41,7 +41,6 @@ import org.jackhuang.hmcl.util.function.ExceptionalRunnable;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.ZipFileSystem;
import java.io.File;
import java.io.FileNotFoundException;
@@ -62,38 +61,34 @@ public final class ModpackHelper {
private ModpackHelper() {}
public static Modpack readModpackManifest(Path file, Charset charset) throws UnsupportedModpackException, ManuallyCreatedModpackException {
try (ZipFileSystem zfs = CompressingUtils.createReadOnlyZipFileSystem(file, charset)) {
try {
return McbbsModpackManifest.readManifest(zfs, charset);
} catch (Exception ignored) {
// ignore it, not a valid MCBBS modpack.
}
try {
return McbbsModpackManifest.readManifest(file, charset);
} catch (Exception ignored) {
// ignore it, not a valid MCBBS modpack.
}
try {
return CurseManifest.readCurseForgeModpackManifest(zfs, charset);
} catch (Exception e) {
// ignore it, not a valid CurseForge modpack.
}
try {
return CurseManifest.readCurseForgeModpackManifest(file, charset);
} catch (Exception e) {
// ignore it, not a valid CurseForge modpack.
}
try {
return HMCLModpackManager.readHMCLModpackManifest(zfs, charset);
} catch (Exception e) {
// ignore it, not a valid HMCL modpack.
}
try {
return HMCLModpackManager.readHMCLModpackManifest(file, charset);
} catch (Exception e) {
// ignore it, not a valid HMCL modpack.
}
try {
return MultiMCInstanceConfiguration.readMultiMCModpackManifest(zfs, file, charset);
} catch (Exception e) {
// ignore it, not a valid MultiMC modpack.
}
try {
return MultiMCInstanceConfiguration.readMultiMCModpackManifest(file, charset);
} catch (Exception e) {
// ignore it, not a valid MultiMC modpack.
}
try {
return ServerModpackManifest.readManifest(zfs, charset);
} catch (Exception e) {
// ignore it, not a valid Server modpack.
}
} catch (IOException ignored) {
try {
return ServerModpackManifest.readManifest(file, charset);
} catch (Exception e) {
// ignore it, not a valid Server modpack.
}
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(file, charset)) {
@@ -163,6 +163,9 @@ public final class Config implements Cloneable, Observable {
@SerializedName("authlibInjectorServers")
private ObservableList<AuthlibInjectorServer> authlibInjectorServers = FXCollections.observableArrayList(server -> new Observable[] { server });
@SerializedName("promptedVersion")
private StringProperty promptedVersion = new SimpleStringProperty();
@SerializedName("_version")
private IntegerProperty configVersion = new SimpleIntegerProperty(0);
@@ -579,4 +582,16 @@ public final class Config implements Cloneable, Observable {
public void setTitleTransparent(boolean titleTransparent) {
this.titleTransparent.set(titleTransparent);
}
public String getPromptedVersion() {
return promptedVersion.get();
}
public StringProperty promptedVersionProperty() {
return promptedVersion;
}
public void setPromptedVersion(String promptedVersion) {
this.promptedVersion.set(promptedVersion);
}
}
@@ -0,0 +1,43 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.setting;
public enum VersionIconType {
DEFAULT("/assets/img/grass.png"),
GRASS("/assets/img/grass.png"),
CHEST("/assets/img/chest.png"),
CHICKEN("/assets/img/chicken.png"),
COMMAND("/assets/img/command.png"),
CRAFT_TABLE("/assets/img/craft_table.png"),
FABRIC("/assets/img/fabric.png"),
FORGE("/assets/img/forge.png"),
FURNACE("/assets/img/furnace.png");
// Please append new items at last
private final String resourceUrl;
VersionIconType(String resourceUrl) {
this.resourceUrl = resourceUrl;
}
public String getResourceUrl() {
return resourceUrl;
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -568,6 +568,20 @@ public final class VersionSetting implements Cloneable {
this.useNativeOpenAL.set(useNativeOpenAL);
}
private final ObjectProperty<VersionIconType> versionIcon = new SimpleObjectProperty<>(this, "versionIcon", VersionIconType.DEFAULT);
public VersionIconType getVersionIcon() {
return versionIcon.get();
}
public ObjectProperty<VersionIconType> versionIconProperty() {
return versionIcon;
}
public void setVersionIcon(VersionIconType versionIcon) {
this.versionIcon.set(versionIcon);
}
// launcher settings
/**
@@ -670,6 +684,7 @@ public final class VersionSetting implements Cloneable {
defaultJavaPathProperty.addListener(listener);
nativesDirProperty.addListener(listener);
nativesDirTypeProperty.addListener(listener);
versionIcon.addListener(listener);
}
@Override
@@ -703,6 +718,7 @@ public final class VersionSetting implements Cloneable {
versionSetting.setUseNativeOpenAL(isUseNativeOpenAL());
versionSetting.setLauncherVisibility(getLauncherVisibility());
versionSetting.setNativesDir(getNativesDir());
versionSetting.setVersionIcon(getVersionIcon());
return versionSetting;
}
@@ -741,6 +757,7 @@ public final class VersionSetting implements Cloneable {
obj.addProperty("defaultJavaPath", src.getDefaultJavaPath());
obj.addProperty("nativesDir", src.getNativesDir());
obj.addProperty("nativesDirType", src.getNativesDirType().ordinal());
obj.addProperty("versionIcon", src.getVersionIcon().ordinal());
return obj;
}
@@ -784,6 +801,7 @@ public final class VersionSetting implements Cloneable {
vs.setGameDirType(GameDirectoryType.values()[Optional.ofNullable(obj.get("gameDirType")).map(JsonElement::getAsInt).orElse(GameDirectoryType.ROOT_FOLDER.ordinal())]);
vs.setDefaultJavaPath(Optional.ofNullable(obj.get("defaultJavaPath")).map(JsonElement::getAsString).orElse(null));
vs.setNativesDirType(NativesDirectoryType.values()[Optional.ofNullable(obj.get("nativesDirType")).map(JsonElement::getAsInt).orElse(0)]);
vs.setVersionIcon(VersionIconType.values()[Optional.ofNullable(obj.get("versionIcon")).map(JsonElement::getAsInt).orElse(0)]);
return vs;
}
@@ -52,6 +52,7 @@ import org.jackhuang.hmcl.ui.wizard.Navigation;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jetbrains.annotations.Nullable;
import java.nio.file.Path;
@@ -166,30 +167,40 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
if (version == null) version = profile.getSelectedVersion();
Path runDirectory = profile.getRepository().hasVersion(version) ? profile.getRepository().getRunDirectory(version).toPath() : profile.getRepository().getBaseDirectory().toPath();
Path dest = runDirectory.resolve(subdirectoryName).resolve(file.getFile().getFilename());
TaskExecutorDialogPane downloadingPane = new TaskExecutorDialogPane(it -> {
});
TaskExecutor executor = Task.composeAsync(() -> {
FileDownloadTask task = new FileDownloadTask(NetworkUtils.toURL(file.getFile().getUrl()), dest.toFile());
task.setName(file.getName());
return task;
}).whenComplete(Schedulers.javafx(), exception -> {
if (exception != null) {
if (exception instanceof CancellationException) {
Controllers.showToast(i18n("message.cancelled"));
} else {
Controllers.dialog(DownloadProviders.localizeErrorMessage(exception), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR);
}
} else {
Controllers.showToast(i18n("install.success"));
Controllers.prompt(i18n("archive.name"), (result, resolve, reject) -> {
if (!OperatingSystem.isNameValid(result)) {
reject.accept(i18n("install.new_game.malformed"));
return;
}
}).executor(false);
Path dest = runDirectory.resolve(subdirectoryName).resolve(result);
TaskExecutorDialogPane downloadingPane = new TaskExecutorDialogPane(it -> {
});
TaskExecutor executor = Task.composeAsync(() -> {
FileDownloadTask task = new FileDownloadTask(NetworkUtils.toURL(file.getFile().getUrl()), dest.toFile());
task.setName(file.getName());
return task;
}).whenComplete(Schedulers.javafx(), exception -> {
if (exception != null) {
if (exception instanceof CancellationException) {
Controllers.showToast(i18n("message.cancelled"));
} else {
Controllers.dialog(DownloadProviders.localizeErrorMessage(exception), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR);
}
} else {
Controllers.showToast(i18n("install.success"));
}
}).executor(false);
downloadingPane.setExecutor(executor, true);
Controllers.dialog(downloadingPane);
executor.start();
resolve.run();
}, file.getFile().getFilename());
downloadingPane.setExecutor(executor, true);
Controllers.dialog(downloadingPane);
executor.start();
}
private void loadVersions(Profile profile) {
@@ -42,9 +42,11 @@ import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.AnnouncementCard;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.PopupMenu;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
@@ -53,11 +55,14 @@ import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.upgrade.RemoteVersion;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.upgrade.UpdateHandler;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import java.util.List;
import java.util.Objects;
import java.util.stream.IntStream;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.SINE;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -69,7 +74,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
private final StringProperty currentGame = new SimpleStringProperty(this, "currentGame");
private final BooleanProperty showUpdate = new SimpleBooleanProperty(this, "showUpdate");
private final StringProperty latestVersion = new SimpleStringProperty(this, "latestVersion");
private final ObjectProperty<RemoteVersion> latestVersion = new SimpleObjectProperty<>(this, "latestVersion");
private final ObservableList<Version> versions = FXCollections.observableArrayList();
private final ObservableList<Node> versionNodes;
private Profile profile;
@@ -103,7 +108,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
FXUtils.setLimitHeight(updatePane, 55);
StackPane.setAlignment(updatePane, Pos.TOP_RIGHT);
updatePane.setOnMouseClicked(e -> onUpgrade());
FXUtils.onChange(showUpdateProperty(), this::doAnimation);
FXUtils.onChange(showUpdateProperty(), this::showUpdate);
{
HBox hBox = new HBox();
@@ -118,7 +123,8 @@ public final class MainPage extends StackPane implements DecoratorPage {
TwoLineListItem prompt = new TwoLineListItem();
prompt.setSubtitle(i18n("update.bubble.subtitle"));
prompt.setPickOnBounds(false);
prompt.titleProperty().bind(latestVersionProperty());
prompt.titleProperty().bind(BindingMapping.of(latestVersionProperty()).map(latestVersion ->
latestVersion == null ? "" : i18n("update.bubble.title", latestVersion.getVersion())));
hBox.getChildren().setAll(lblIcon, prompt);
}
@@ -224,6 +230,17 @@ public final class MainPage extends StackPane implements DecoratorPage {
}
}
private void showUpdate(boolean show) {
doAnimation(show);
if (show && getLatestVersion() != null && !Objects.equals(config().getPromptedVersion(), getLatestVersion().getVersion())) {
Controllers.dialog("", i18n("update.bubble.title", getLatestVersion().getVersion()), MessageDialogPane.MessageType.INFO, () -> {
config().setPromptedVersion(getLatestVersion().getVersion());
onUpgrade();
});
}
}
private void doAnimation(boolean show) {
Duration duration = Duration.millis(320);
Timeline nowAnimation = new Timeline();
@@ -289,15 +306,15 @@ public final class MainPage extends StackPane implements DecoratorPage {
this.showUpdate.set(showUpdate);
}
public String getLatestVersion() {
public RemoteVersion getLatestVersion() {
return latestVersion.get();
}
public StringProperty latestVersionProperty() {
public ObjectProperty<RemoteVersion> latestVersionProperty() {
return latestVersion;
}
public void setLatestVersion(String latestVersion) {
public void setLatestVersion(RemoteVersion latestVersion) {
this.latestVersion.set(latestVersion);
}
@@ -42,7 +42,6 @@ import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.io.File;
@@ -92,8 +91,7 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
FXUtils.onChangeAndOperate(Profiles.selectedVersionProperty(), mainPage::setCurrentGame);
mainPage.showUpdateProperty().bind(UpdateChecker.outdatedProperty());
mainPage.latestVersionProperty().bind(BindingMapping.of(UpdateChecker.latestVersionProperty())
.map(version -> version == null ? "" : i18n("update.bubble.title", version.getVersion())));
mainPage.latestVersionProperty().bind(UpdateChecker.latestVersionProperty());
Profiles.registerVersionsListener(profile -> {
HMCLGameRepository repository = profile.getRepository();
@@ -87,6 +87,7 @@ public class MultiplayerClient extends Thread {
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8))) {
MultiplayerServer.Endpoint endpoint = new MultiplayerServer.Endpoint(socket, writer);
socket.setSoTimeout(30000);
LOG.info("Connected to 127.0.0.1:" + port);
endpoint.write(new HandshakeRequest());
@@ -107,7 +108,7 @@ public class MultiplayerClient extends Thread {
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to close socket", e);
}
}, 15 * 1000);
}, 25 * 1000);
String line;
while ((line = reader.readLine()) != null) {
@@ -44,7 +44,6 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission;
import java.util.*;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import java.util.logging.Level;
@@ -60,7 +59,7 @@ import static org.jackhuang.hmcl.util.Pair.pair;
* Cato Management.
*/
public final class MultiplayerManager {
static final String CATO_VERSION = "1.1.1-202110231414";
static final String CATO_VERSION = "1.1.1";
// private static final String CATO_DOWNLOAD_URL = "https://files.huangyuhui.net/maven/cato/cato/" + MultiplayerManager.CATO_VERSION;
private static final String CATO_DOWNLOAD_URL = "https://codechina.csdn.net/to/ioi_bin/-/raw/acb0524bcad82a31fa5a09bf4c79248ebd674de1/client/";
private static final String CATO_PATH = getCatoPath();
@@ -198,7 +197,7 @@ public final class MultiplayerManager {
task.cancel();
});
client.onKicked().register(kickedEvent -> {
future.completeExceptionally(new CancellationException());
future.completeExceptionally(new KickedException(kickedEvent.getReason()));
session.stop();
task.cancel();
});
@@ -229,6 +228,7 @@ public final class MultiplayerManager {
MultiplayerServer server = new MultiplayerServer(sessionName, gamePort, allowAllJoinRequests);
server.startServer();
session.setName(sessionName);
session.allowForwardingAddress(REMOTE_ADDRESS, server.getPort());
session.allowForwardingAddress(REMOTE_ADDRESS, gamePort);
session.showAllowedAddress();
@@ -264,7 +264,7 @@ public final class MultiplayerManager {
}));
}
public static final Pattern INVITATION_CODE_PATTERN = Pattern.compile("^(?<id>(idx|mix)(.*?))#(?<port>\\d{2,5})$");
public static final Pattern INVITATION_CODE_PATTERN = Pattern.compile("^(?<id>.*?)#(?<port>\\d{2,5})$");
public static Invitation parseInvitationCode(String invitationCode) throws JsonParseException {
Matcher matcher = INVITATION_CODE_PATTERN.matcher(invitationCode);
@@ -595,6 +595,18 @@ public final class MultiplayerManager {
public static class ConnectionErrorException extends RuntimeException {
}
public static class KickedException extends RuntimeException {
private final String reason;
public KickedException(String reason) {
this.reason = reason;
}
public String getReason() {
return reason;
}
}
public static class CatoNotExistsException extends RuntimeException {
private final Path file;
@@ -38,7 +38,9 @@ import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.HMCLService;
import org.jackhuang.hmcl.util.Result;
import org.jackhuang.hmcl.util.StringUtils;
import org.jetbrains.annotations.Nullable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -58,7 +60,7 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP
private final ObjectProperty<MultiplayerManager.State> multiplayerState = new SimpleObjectProperty<>(MultiplayerManager.State.DISCONNECTED);
private final ReadOnlyStringWrapper token = new ReadOnlyStringWrapper();
private final ReadOnlyObjectWrapper<DiscoveryInfo> natState = new ReadOnlyObjectWrapper<>();
private final ReadOnlyObjectWrapper<@Nullable Result<DiscoveryInfo>> natState = new ReadOnlyObjectWrapper<>();
private final ReadOnlyIntegerWrapper gamePort = new ReadOnlyIntegerWrapper(-1);
private final ReadOnlyObjectWrapper<MultiplayerManager.CatoSession> session = new ReadOnlyObjectWrapper<>();
private final ObservableList<MultiplayerChannel.CatoClient> clients = FXCollections.observableArrayList();
@@ -97,11 +99,11 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP
this.multiplayerState.set(multiplayerState);
}
public DiscoveryInfo getNatState() {
public Result<DiscoveryInfo> getNatState() {
return natState.get();
}
public ReadOnlyObjectProperty<DiscoveryInfo> natStateProperty() {
public ReadOnlyObjectProperty<Result<DiscoveryInfo>> natStateProperty() {
return natState.getReadOnlyProperty();
}
@@ -134,12 +136,12 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP
DiscoveryTest tester = new DiscoveryTest(null, 0, "stun.stunprotocol.org", 3478);
return tester.test();
}).whenComplete(Schedulers.javafx(), (info, exception) -> {
LOG.log(Level.INFO, "Nat test result " + MultiplayerPageSkin.getNATType(info), exception);
if (exception == null) {
natState.set(info);
natState.set(Result.ok(info));
} else {
natState.set(null);
natState.set(Result.error());
}
LOG.log(Level.INFO, "Nat test result " + MultiplayerPageSkin.getNATType(natState.get()), exception);
}).start();
}
@@ -345,7 +347,10 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP
Throwable e = resolveException(t);
if (e instanceof CancellationException) {
LOG.info("Connection rejected by the server");
return i18n("multiplayer.session.join.rejected");
return i18n("message.cancelled");
} else if (e instanceof MultiplayerManager.KickedException) {
LOG.info("Kicked by server");
return i18n("multiplayer.session.join.kicked", localizeKickMessage(((MultiplayerManager.KickedException) e).getReason()));
} else if (e instanceof MultiplayerManager.CatoAlreadyStartedException) {
LOG.info("Cato already started");
return i18n("multiplayer.session.error.already_started");
@@ -503,4 +508,5 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP
public ReadOnlyObjectProperty<State> stateProperty() {
return state;
}
}
@@ -41,8 +41,10 @@ import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.util.HMCLService;
import org.jackhuang.hmcl.util.Result;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import org.jetbrains.annotations.Nullable;
import static org.jackhuang.hmcl.setting.ConfigHolder.globalConfig;
import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap;
@@ -295,22 +297,24 @@ public class MultiplayerPageSkin extends DecoratorAnimatedPage.DecoratorAnimated
}
}
public static String getNATType(DiscoveryInfo info) {
public static String getNATType(@Nullable Result<DiscoveryInfo> info) {
if (info == null) {
return i18n("multiplayer.nat.testing");
} else if (info.isBlockedUDP()) {
} else if (info.isError()) {
return i18n("multiplayer.nat.failed");
} else if (info.get().isBlockedUDP()) {
return i18n("multiplayer.nat.type.blocked_udp");
} else if (info.isFullCone()) {
} else if (info.get().isFullCone()) {
return i18n("multiplayer.nat.type.full_cone");
} else if (info.isOpenAccess()) {
} else if (info.get().isOpenAccess()) {
return i18n("multiplayer.nat.type.open_access");
} else if (info.isPortRestrictedCone()) {
} else if (info.get().isPortRestrictedCone()) {
return i18n("multiplayer.nat.type.port_restricted_cone");
} else if (info.isRestrictedCone()) {
} else if (info.get().isRestrictedCone()) {
return i18n("multiplayer.nat.type.restricted_cone");
} else if (info.isSymmetric()) {
} else if (info.get().isSymmetric()) {
return i18n("multiplayer.nat.type.symmetric");
} else if (info.isSymmetricUDPFirewall()) {
} else if (info.get().isSymmetricUDPFirewall()) {
return i18n("multiplayer.nat.type.symmetric_udp_firewall");
} else {
return i18n("multiplayer.nat.type.unknown");
@@ -20,17 +20,25 @@ package org.jackhuang.hmcl.ui.versions;
import javafx.scene.Node;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import org.jackhuang.hmcl.util.Pair;
import java.util.function.Consumer;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class GameAdvancedListItem extends AdvancedListItem {
private final Tooltip tooltip;
private final ImageView imageView;
private final WeakListenerHolder holder = new WeakListenerHolder();
private Profile profile;
private Consumer<Event> onVersionIconChangedListener;
public GameAdvancedListItem() {
tooltip = new Tooltip();
@@ -39,23 +47,31 @@ public class GameAdvancedListItem extends AdvancedListItem {
setLeftGraphic(view.getKey());
imageView = view.getValue();
FXUtils.onChangeAndOperate(Profiles.selectedVersionProperty(), version -> {
if (version != null && Profiles.getSelectedProfile() != null &&
Profiles.getSelectedProfile().getRepository().hasVersion(version)) {
FXUtils.installFastTooltip(this, tooltip);
setTitle(version);
setSubtitle(null);
imageView.setImage(Profiles.getSelectedProfile().getRepository().getVersionIconImage(version));
tooltip.setText(version);
} else {
Tooltip.uninstall(this,tooltip);
setTitle(i18n("version.empty"));
setSubtitle(i18n("version.empty.add"));
imageView.setImage(newImage("/assets/img/grass.png"));
tooltip.setText("");
}
});
holder.add(FXUtils.onWeakChangeAndOperate(Profiles.selectedVersionProperty(), this::loadVersion));
setActionButtonVisible(false);
}
private void loadVersion(String version) {
if (Profiles.getSelectedProfile() != profile) {
profile = Profiles.getSelectedProfile();
onVersionIconChangedListener = profile.getRepository().onVersionIconChanged.registerWeak(event -> {
this.loadVersion(Profiles.getSelectedVersion());
});
}
if (version != null && Profiles.getSelectedProfile() != null &&
Profiles.getSelectedProfile().getRepository().hasVersion(version)) {
FXUtils.installFastTooltip(this, tooltip);
setTitle(version);
setSubtitle(null);
imageView.setImage(Profiles.getSelectedProfile().getRepository().getVersionIconImage(version));
tooltip.setText(version);
} else {
Tooltip.uninstall(this,tooltip);
setTitle(i18n("version.empty"));
setSubtitle(i18n("version.empty.add"));
imageView.setImage(newImage("/assets/img/grass.png"));
tooltip.setText("");
}
}
}
@@ -0,0 +1,126 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.versions;
import javafx.scene.Node;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.FlowPane;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.setting.VersionIconType;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.DialogPane;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class VersionIconDialog extends DialogPane {
private final Profile profile;
private final String versionId;
private final Runnable onFinish;
private final VersionSetting vs;
public VersionIconDialog(Profile profile, String versionId, Runnable onFinish) {
this.profile = profile;
this.versionId = versionId;
this.onFinish = onFinish;
this.vs = profile.getRepository().getLocalVersionSettingOrCreate(versionId);
setTitle(i18n("settings.icon"));
FlowPane pane = new FlowPane();
setBody(pane);
pane.getChildren().setAll(
createCustomIcon(),
createIcon(VersionIconType.GRASS),
createIcon(VersionIconType.CHEST),
createIcon(VersionIconType.CHICKEN),
createIcon(VersionIconType.COMMAND),
createIcon(VersionIconType.CRAFT_TABLE),
createIcon(VersionIconType.FABRIC),
createIcon(VersionIconType.FORGE),
createIcon(VersionIconType.FURNACE)
);
}
private void exploreIcon() {
FileChooser chooser = new FileChooser();
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.png"), "*.png"));
File selectedFile = chooser.showOpenDialog(Controllers.getStage());
if (selectedFile != null) {
File iconFile = profile.getRepository().getVersionIconFile(versionId);
try {
FileUtils.copyFile(selectedFile, iconFile);
if (vs != null) {
vs.setVersionIcon(VersionIconType.DEFAULT);
}
onAccept();
} catch (IOException e) {
Logging.LOG.log(Level.SEVERE, "Failed to copy icon file from " + selectedFile + " to " + iconFile, e);
}
}
}
private Node createCustomIcon() {
Node shape = SVG.plusCircleOutline(Theme.blackFillBinding(), 32, 32);
shape.setMouseTransparent(true);
RipplerContainer container = new RipplerContainer(shape);
FXUtils.setLimitWidth(container, 36);
FXUtils.setLimitHeight(container, 36);
container.setOnMouseClicked(e -> {
exploreIcon();
});
return container;
}
private Node createIcon(VersionIconType type) {
ImageView imageView = new ImageView(new Image(type.getResourceUrl(), 32, 32, true, true));
imageView.setMouseTransparent(true);
RipplerContainer container = new RipplerContainer(imageView);
FXUtils.setLimitWidth(container, 36);
FXUtils.setLimitHeight(container, 36);
container.setOnMouseClicked(e -> {
if (vs != null) {
vs.setVersionIcon(type);
onAccept();
}
});
return container;
}
@Override
protected void onAccept() {
profile.getRepository().onVersionIconChanged.fireEvent(new Event(this));
onFinish.run();
super.onAccept();
}
}
@@ -32,10 +32,7 @@ import javafx.scene.layout.*;
import javafx.scene.text.Text;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.setting.LauncherVisibility;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
@@ -44,9 +41,7 @@ import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
import org.jackhuang.hmcl.util.platform.Architecture;
@@ -55,17 +50,14 @@ import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -775,18 +767,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
if (versionId == null)
return;
FileChooser chooser = new FileChooser();
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.png"), "*.png"));
File selectedFile = chooser.showOpenDialog(Controllers.getStage());
if (selectedFile != null) {
File iconFile = profile.getRepository().getVersionIconFile(versionId);
try {
FileUtils.copyFile(selectedFile, iconFile);
loadIcon();
} catch (IOException e) {
Logging.LOG.log(Level.SEVERE, "Failed to copy icon file from " + selectedFile + " to " + iconFile, e);
}
}
Controllers.dialog(new VersionIconDialog(profile, versionId, this::loadIcon));
}
private void onDeleteIcon() {
@@ -796,6 +777,10 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
File iconFile = profile.getRepository().getVersionIconFile(versionId);
if (iconFile.exists())
iconFile.delete();
VersionSetting localVersionSetting = profile.getRepository().getLocalVersionSettingOrCreate(versionId);
if (localVersionSetting != null) {
localVersionSetting.setVersionIcon(VersionIconType.DEFAULT);
}
loadIcon();
}
@@ -804,11 +789,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
return;
}
File iconFile = profile.getRepository().getVersionIconFile(versionId);
if (iconFile.exists())
iconPickerItem.setImage(new Image("file:" + iconFile.getAbsolutePath()));
else
iconPickerItem.setImage(newImage("/assets/img/grass.png"));
iconPickerItem.setImage(profile.getRepository().getVersionIconImage(versionId));
FXUtils.limitSize(iconPickerItem.getImageView(), 32, 32);
}
@@ -19,7 +19,6 @@ package org.jackhuang.hmcl.upgrade;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.util.Pack200Utils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.tukaani.xz.XZInputStream;
@@ -50,7 +49,7 @@ class HMCLDownloadTask extends FileDownloadTask {
break;
case PACK_XZ:
byte[] raw = FileUtils.readAllBytes(target);
byte[] raw = Files.readAllBytes(target);
try (InputStream in = new XZInputStream(new ByteArrayInputStream(raw));
JarOutputStream out = new JarOutputStream(Files.newOutputStream(target))) {
Pack200Utils.unpack(in, out);
@@ -21,6 +21,7 @@ import javafx.application.Platform;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.countly.CrashReport;
import org.jackhuang.hmcl.ui.CrashWindow;
import org.jackhuang.hmcl.upgrade.IntegrityChecker;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
@@ -65,8 +66,6 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler {
pair("com.sun.javafx.css.StyleManager.findMatchingStyles", i18n("launcher.update_java")),
pair("NoSuchAlgorithmException", "Has your operating system been installed completely or is a ghost system?")
};
static final Set<String> CAUGHT_EXCEPTIONS = newSetFromMap(new ConcurrentHashMap<>());
}
private boolean checkThrowable(Throwable e) {
@@ -101,29 +100,11 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler {
LOG.log(Level.SEVERE, "Uncaught exception in thread " + t.getName(), e);
try {
String stackTrace = StringUtils.getStackTrace(e);
if (!stackTrace.contains("org.jackhuang"))
CrashReport report = new CrashReport(t, e);
if (!report.shouldBeReport())
return;
if (Hole.CAUGHT_EXCEPTIONS.contains(stackTrace))
return;
Hole.CAUGHT_EXCEPTIONS.add(stackTrace);
String text = "---- Hello Minecraft! Crash Report ----\n" +
" Version: " + Metadata.VERSION + "\n" +
" Time: " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + "\n" +
" Thread: " + t + "\n" +
"\n Content: \n " +
stackTrace + "\n\n" +
"-- System Details --\n" +
" Operating System: " + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION + "\n" +
" System Architecture: " + Architecture.SYSTEM_ARCH_NAME + "\n" +
" Java Architecture: " + Architecture.CURRENT_ARCH_NAME + "\n" +
" Java Version: " + System.getProperty("java.version") + ", " + System.getProperty("java.vendor") + "\n" +
" Java VM Version: " + System.getProperty("java.vm.name") + " (" + System.getProperty("java.vm.info") + "), " + System.getProperty("java.vm.vendor") + "\n" +
" JVM Max Memory: " + Runtime.getRuntime().maxMemory() + "\n" +
" JVM Total Memory: " + Runtime.getRuntime().totalMemory() + "\n" +
" JVM Free Memory: " + Runtime.getRuntime().freeMemory() + "\n";
String text = report.getDisplayText();
LOG.log(Level.SEVERE, text);
Platform.runLater(() -> {
@@ -132,7 +113,7 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler {
new CrashWindow(text).show();
}
if (!UpdateChecker.isOutdated() && IntegrityChecker.isSelfVerified()) {
reportToServer(text);
reportToServer(report);
}
}
});
@@ -141,10 +122,10 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler {
}
}
private void reportToServer(final String text) {
private void reportToServer(CrashReport crashReport) {
Thread t = new Thread(() -> {
HashMap<String, String> map = new HashMap<>();
map.put("crash_report", text);
map.put("crash_report", crashReport.getDisplayText());
map.put("version", Metadata.VERSION);
map.put("log", Logging.getLogs());
try {
@@ -18,7 +18,7 @@
<VBox fx:id="borderPane" alignment="CENTER" FXUtils.limitWidth="500">
<ComponentList>
<BorderPane><left><Label text="%modpack.task.install.will" /></left><right><Label fx:id="lblModpackLocation" /></right></BorderPane>
<BorderPane><left><Label text="%modpack.enter_name" /></left><right><JFXTextField fx:id="txtModpackName" StackPane.margin="$insets" /></right></BorderPane>
<BorderPane><left><Label BorderPane.alignment="CENTER_LEFT" text="%archive.name" /></left><right><JFXTextField fx:id="txtModpackName" StackPane.margin="$insets" /></right></BorderPane>
<BorderPane><left><Label text="%modpack.name"/></left><right><Label fx:id="lblName" /></right></BorderPane>
<BorderPane><left><Label text="%archive.version"/></left><right><Label fx:id="lblVersion" /></right></BorderPane>
@@ -654,6 +654,7 @@ multiplayer.exit.timeout.static_token=Failed to connect to multiplayer server. P
multiplayer.exit.timeout.dynamic_token=Failed to connect to multiplayer server.
multiplayer.hint=Multiplayer functionality is experimental. Please give feedback.
multiplayer.nat=Network Type Detection
multiplayer.nat.failed=Failed to detect network type.
multiplayer.nat.hint=Network type detection will make it clear whether your network fulfills our requirement for multiplayer mode.
multiplayer.nat.latency=Latency
multiplayer.nat.not_yet_tested=Not yet testsed
File diff suppressed because it is too large. Load diff
@@ -654,6 +654,7 @@ multiplayer.exit.timeout.static_token=無法連接多人聯機服務,請你切
multiplayer.exit.timeout.dynamic_token=無法連接多人聯機服務,你可以在多人聯機頁面的回饋中回饋問題。
multiplayer.hint=多人聯機功能處於實驗階段,如果有問題請回饋。
multiplayer.nat=網路檢測
multiplayer.nat.failed=檢測失敗,但你仍然可以繼續使用聯機功能。
multiplayer.nat.hint=執行網路檢測可以讓你更清楚你的網路狀況是否符合聯機功能的需求。不符合聯機功能運行條件的網路狀況將可能導致聯機失敗。
multiplayer.nat.latency=延遲
multiplayer.nat.not_yet_tested=尚未檢測
@@ -654,6 +654,7 @@ multiplayer.exit.timeout.static_token=无法连接多人联机服务,请你切
multiplayer.exit.timeout.dynamic_token=无法连接多人联机服务,你可以在多人联机页面的反馈中反馈问题。
multiplayer.hint=多人联机功能处于实验阶段,如果有问题请反馈。
multiplayer.nat=网络检测
multiplayer.nat.failed=检测失败,但你仍然可以继续使用联机功能。
multiplayer.nat.hint=执行网络检测可以让你更清楚你的网络状况是否符合联机功能的需求。检测结果为差的网络可能导致联机失败。
multiplayer.nat.latency=延迟
multiplayer.nat.not_yet_tested=尚未检测
@@ -25,6 +25,7 @@ import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorArtifactInfo;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorArtifactProvider;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloadException;
import org.jackhuang.hmcl.auth.yggdrasil.Texture;
import org.jackhuang.hmcl.auth.yggdrasil.TextureModel;
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
import org.jackhuang.hmcl.game.Arguments;
import org.jackhuang.hmcl.util.StringUtils;
@@ -89,11 +90,22 @@ public class OfflineAccount extends Account {
invalidate();
}
private boolean loadAuthlibInjector(Skin skin) {
if (skin == null) return false;
if (skin.getType() == Skin.Type.DEFAULT) return false;
TextureModel defaultModel = TextureModel.detectUUID(getUUID());
if (skin.getType() == Skin.Type.ALEX && defaultModel == TextureModel.ALEX ||
skin.getType() == Skin.Type.STEVE && defaultModel == TextureModel.STEVE) {
return false;
}
return true;
}
@Override
public AuthInfo logIn() throws AuthenticationException {
AuthInfo authInfo = new AuthInfo(username, uuid, UUIDTypeAdapter.fromUUID(UUID.randomUUID()), "{}");
if (skin != null) {
if (loadAuthlibInjector(skin)) {
CompletableFuture<AuthlibInjectorArtifactInfo> artifactTask = CompletableFuture.supplyAsync(() -> {
try {
return downloader.getArtifactInfo();
@@ -94,9 +94,16 @@ public class YggdrasilServer extends HttpServer {
if (!request.getQuery().containsKey("username")) {
return badRequest();
}
return findCharacterByName(request.getQuery().get("username"))
.map(character -> ok(character.toCompleteResponse(getRootUrl())))
.orElseGet(HttpServer::noContent);
Optional<Character> character = findCharacterByName(request.getQuery().get("username"));
//Workaround for JDK-8138667
//noinspection OptionalIsPresent
if (character.isPresent()) {
return ok(character.get().toCompleteResponse(getRootUrl()));
} else {
return HttpServer.noContent();
}
}
private Response joinServer(Request request) {
@@ -106,9 +113,15 @@ public class YggdrasilServer extends HttpServer {
private Response profile(Request request) {
String uuid = request.getPathVariables().group("uuid");
return findCharacterByUuid(UUIDTypeAdapter.fromString(uuid))
.map(character -> ok(character.toCompleteResponse(getRootUrl())))
.orElseGet(HttpServer::noContent);
Optional<Character> character = findCharacterByUuid(UUIDTypeAdapter.fromString(uuid));
//Workaround for JDK-8138667
//noinspection OptionalIsPresent
if (character.isPresent()) {
return ok(character.get().toCompleteResponse(getRootUrl()));
} else {
return HttpServer.noContent();
}
}
private Response texture(Request request) {
@@ -103,7 +103,7 @@ public class LibraryDownloadTask extends Task<Void> {
else
throw new LibraryDownloadException(library, t);
} else {
if (xz) unpackLibrary(jar, FileUtils.readAllBytes(xzFile.toPath()));
if (xz) unpackLibrary(jar, Files.readAllBytes(xzFile.toPath()));
}
}
@@ -180,7 +180,7 @@ public class LibraryDownloadTask extends Task<Void> {
if (checksums == null || checksums.isEmpty()) {
return true;
}
byte[] fileData = FileUtils.readAllBytes(libPath.toPath());
byte[] fileData = Files.readAllBytes(libPath.toPath());
boolean valid = checksums.contains(encodeHex(digest("SHA-1", fileData)));
if (!valid && libPath.getName().endsWith(".jar")) {
valid = validateJar(fileData, checksums);
@@ -230,7 +230,7 @@ public final class OptiFineInstallTask extends Task<Version> {
Path configClass = fs.getPath("Config.class");
if (!Files.exists(configClass)) configClass = fs.getPath("net/optifine/Config.class");
if (!Files.exists(configClass)) throw new IOException("Unrecognized installer");
ConstantPool pool = ConstantPoolScanner.parse(FileUtils.readAllBytes(configClass), ConstantType.UTF8);
ConstantPool pool = ConstantPoolScanner.parse(Files.readAllBytes(configClass), ConstantType.UTF8);
List<String> constants = new ArrayList<>();
pool.list(Utf8Constant.class).forEach(utf8 -> constants.add(utf8.get()));
String mcVersion = getOrDefault(constants, constants.indexOf("MC_VERSION") + 1, null);
@@ -104,13 +104,13 @@ public final class GameVersion {
Path minecraft = gameJar.getPath("net/minecraft/client/Minecraft.class");
if (Files.exists(minecraft)) {
Optional<String> result = getVersionOfClassMinecraft(FileUtils.readAllBytes(minecraft));
Optional<String> result = getVersionOfClassMinecraft(Files.readAllBytes(minecraft));
if (result.isPresent())
return result;
}
Path minecraftServer = gameJar.getPath("net/minecraft/server/MinecraftServer.class");
if (Files.exists(minecraftServer))
return getVersionFromClassMinecraftServer(FileUtils.readAllBytes(minecraftServer));
return getVersionFromClassMinecraftServer(Files.readAllBytes(minecraftServer));
return Optional.empty();
} catch (IOException e) {
return Optional.empty();
@@ -45,6 +45,7 @@ import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.function.Supplier;
@@ -68,7 +69,7 @@ public class DefaultLauncher extends Launcher {
super(repository, version, authInfo, options, listener, daemon);
}
private CommandBuilder generateCommandLine(File nativeFolder) throws IOException {
private Command generateCommandLine(File nativeFolder) throws IOException {
CommandBuilder res = new CommandBuilder();
switch (options.getProcessPriority()) {
@@ -208,10 +209,20 @@ public class DefaultLauncher extends Launcher {
Path gameAssets = repository.getActualAssetDirectory(version.getId(), version.getAssetIndex().getId());
Map<String, String> configuration = getConfigurations();
configuration.put("${classpath}", String.join(OperatingSystem.PATH_SEPARATOR, classpath));
configuration.put("${natives_directory}", nativeFolder.getAbsolutePath());
configuration.put("${game_assets}", gameAssets.toAbsolutePath().toString());
configuration.put("${assets_root}", gameAssets.toAbsolutePath().toString());
// lwjgl assumes path to native libraries encoded by ASCII.
// Here is a workaround for this issue: https://github.com/huanghongxun/HMCL/issues/1141.
String nativeFolderPath = nativeFolder.getAbsolutePath();
Path tempNativeFolder = null;
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX
&& !StringUtils.isASCII(nativeFolderPath)) {
tempNativeFolder = Paths.get("/", "tmp", "hmcl-natives-" + UUID.randomUUID());
nativeFolderPath = tempNativeFolder + File.pathSeparator + nativeFolderPath;
}
configuration.put("${natives_directory}", nativeFolderPath);
res.addAll(Arguments.parseArguments(version.getArguments().map(Arguments::getJvm).orElseGet(this::getDefaultJVMArguments), configuration));
if (authInfo.getArguments() != null && authInfo.getArguments().getJvm() != null && !authInfo.getArguments().getJvm().isEmpty())
res.addAll(Arguments.parseArguments(authInfo.getArguments().getJvm(), configuration));
@@ -258,7 +269,7 @@ public class DefaultLauncher extends Launcher {
res.addAllWithoutParsing(Arguments.parseStringArguments(options.getGameArguments(), configuration));
res.removeIf(it -> getForbiddens().containsKey(it) && getForbiddens().get(it).get());
return res;
return new Command(res, tempNativeFolder);
}
public Map<String, Boolean> getFeatures() {
@@ -363,8 +374,10 @@ public class DefaultLauncher extends Launcher {
nativeFolder = new File(options.getNativesDir());
}
final Command command = generateCommandLine(nativeFolder);
// To guarantee that when failed to generate launch command line, we will not call pre-launch command
List<String> rawCommandLine = generateCommandLine(nativeFolder).asMutableList();
List<String> rawCommandLine = command.commandLine.asMutableList();
// Pass classpath using the environment variable, to reduce the command length
String classpath = null;
@@ -374,6 +387,11 @@ public class DefaultLauncher extends Launcher {
classpath = rawCommandLine.remove(cpIndex);
}
if (command.tempNativeFolder != null) {
Files.deleteIfExists(command.tempNativeFolder);
Files.createSymbolicLink(command.tempNativeFolder, nativeFolder.toPath().toAbsolutePath());
}
if (rawCommandLine.stream().anyMatch(StringUtils::isBlank)) {
throw new IllegalStateException("Illegal command line " + rawCommandLine);
}
@@ -464,8 +482,8 @@ public class DefaultLauncher extends Launcher {
if (!FileUtils.makeFile(scriptFile))
throw new IOException("Script file: " + scriptFile + " cannot be created.");
final CommandBuilder commandLine = generateCommandLine(nativeFolder);
final String command = usePowerShell ? null : commandLine.toString();
final Command commandLine = generateCommandLine(nativeFolder);
final String command = usePowerShell ? null : commandLine.commandLine.toString();
if (!usePowerShell && isWindows) {
if (command.length() > 8192) { // maximum length of the command in cmd
@@ -509,7 +527,7 @@ public class DefaultLauncher extends Launcher {
writer.newLine();
writer.write('&');
for (String rawCommand : commandLine.asList()) {
for (String rawCommand : commandLine.commandLine.asList()) {
writer.write(' ');
writer.write(CommandBuilder.pwshString(rawCommand));
}
@@ -533,6 +551,10 @@ public class DefaultLauncher extends Launcher {
writer.write("export " + entry.getKey() + "=" + entry.getValue());
writer.newLine();
}
if (commandLine.tempNativeFolder != null) {
writer.write(new CommandBuilder().add("ln", "-s", nativeFolder.getAbsolutePath(), commandLine.tempNativeFolder.toString()).toString());
writer.newLine();
}
writer.write(new CommandBuilder().add("cd", repository.getRunDirectory(version.getId()).getAbsolutePath()).toString());
}
writer.newLine();
@@ -540,7 +562,7 @@ public class DefaultLauncher extends Launcher {
writer.write(options.getPreLaunchCommand());
writer.newLine();
}
writer.write(generateCommandLine(nativeFolder).toString());
writer.write(command);
writer.newLine();
if (StringUtils.isNotBlank(options.getPostExitCommand())) {
writer.write(options.getPostExitCommand());
@@ -551,6 +573,10 @@ public class DefaultLauncher extends Launcher {
writer.write("pause");
writer.newLine();
}
if (commandLine.tempNativeFolder != null) {
writer.write(new CommandBuilder().add("rm", commandLine.tempNativeFolder.toString()).toString());
writer.newLine();
}
}
}
if (!scriptFile.setExecutable(true))
@@ -574,4 +600,14 @@ public class DefaultLauncher extends Launcher {
managedProcess.addRelatedThread(stderr);
managedProcess.addRelatedThread(Lang.thread(new ExitWaiter(managedProcess, Arrays.asList(stdout, stderr), processListener::onExit), "exit-waiter", isDaemon));
}
private static final class Command {
final CommandBuilder commandLine;
final Path tempNativeFolder;
Command(CommandBuilder commandBuilder, Path tempNativeFolder) {
this.commandLine = commandBuilder;
this.tempNativeFolder = tempNativeFolder;
}
}
}
@@ -23,14 +23,13 @@ import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.ZipFileSystem;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
@@ -122,11 +121,11 @@ public final class CurseManifest {
* @throws JsonParseException if the manifest.json is missing or malformed.
* @return the manifest.
*/
public static Modpack readCurseForgeModpackManifest(ZipFileSystem zip, Charset encoding) throws IOException, JsonParseException {
String json = FileUtils.readText(zip.getPath("manifest.json"));
public static Modpack readCurseForgeModpackManifest(Path zip, Charset encoding) throws IOException, JsonParseException {
String json = CompressingUtils.readTextZipEntry(zip, "manifest.json", encoding);
CurseManifest manifest = JsonUtils.fromNonNullJson(json, CurseManifest.class);
return new Modpack(manifest.getName(), manifest.getAuthor(), manifest.getVersion(), manifest.getMinecraft().getGameVersion(),
Lang.ignoringException(() -> FileUtils.readText(zip.getPath("modlist.html")), "No description"), encoding, manifest) {
CompressingUtils.readTextZipEntryQuietly(zip, "modlist.html", encoding).orElse( "No description"), encoding, manifest) {
@Override
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, File zipFile, String name) {
return new CurseInstallTask(dependencyManager, zipFile, this, manifest, name);
@@ -25,6 +25,7 @@ import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.*;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jetbrains.annotations.Nullable;
@@ -32,6 +33,7 @@ import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -433,26 +435,29 @@ public class McbbsModpackManifest implements Validation {
}
private static Modpack fromManifestFile(Path manifestFile, Charset encoding) throws IOException, JsonParseException {
McbbsModpackManifest manifest = JsonUtils.fromNonNullJson(FileUtils.readText(manifestFile), McbbsModpackManifest.class);
String json = FileUtils.readText(manifestFile, StandardCharsets.UTF_8);
McbbsModpackManifest manifest = JsonUtils.fromNonNullJson(json, McbbsModpackManifest.class);
return manifest.toModpack(encoding);
}
/**
* @param fs the MCBBS modpack file.
* @param zip the MCBBS modpack file.
* @param encoding the modpack zip file encoding.
* @return the manifest.
* @throws IOException if the file is not a valid zip file.
* @throws IOException if the file is not a valid zip file.
* @throws JsonParseException if the server-manifest.json is missing or malformed.
* @return the manifest.
*/
public static Modpack readManifest(FileSystem fs, Charset encoding) throws IOException, JsonParseException {
Path mcbbsPackMeta = fs.getPath("mcbbs.packmeta");
if (Files.exists(mcbbsPackMeta)) {
return fromManifestFile(mcbbsPackMeta, encoding);
public static Modpack readManifest(Path zip, Charset encoding) throws IOException, JsonParseException {
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(zip, encoding)) {
Path mcbbsPackMeta = fs.getPath("mcbbs.packmeta");
if (Files.exists(mcbbsPackMeta)) {
return fromManifestFile(mcbbsPackMeta, encoding);
}
Path manifestJson = fs.getPath("manifest.json");
if (Files.exists(manifestJson)) {
return fromManifestFile(manifestJson, encoding);
}
throw new IOException("`mcbbs.packmeta` or `manifest.json` cannot be found");
}
Path manifestJson = fs.getPath("manifest.json");
if (Files.exists(manifestJson)) {
return fromManifestFile(manifestJson, encoding);
}
throw new IOException("`mcbbs.packmeta` or `manifest.json` cannot be found");
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -21,8 +21,8 @@ import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.ZipFileSystem;
import java.io.File;
import java.io.IOException;
@@ -30,6 +30,7 @@ import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
@@ -347,22 +348,24 @@ public final class MultiMCInstanceConfiguration {
}
}
public static Modpack readMultiMCModpackManifest(ZipFileSystem zipFile, Path filePath, Charset encoding) throws IOException {
Path root = getRootPath(zipFile.getPath("/"));
MultiMCManifest manifest = MultiMCManifest.readMultiMCModpackManifest(root);
String name = FileUtils.getNameWithoutExtension(filePath);
public static Modpack readMultiMCModpackManifest(Path modpackFile, Charset encoding) throws IOException {
try (FileSystem fs = CompressingUtils.readonly(modpackFile).setEncoding(encoding).build()) {
Path root = getRootPath(fs.getPath("/"));
MultiMCManifest manifest = MultiMCManifest.readMultiMCModpackManifest(root);
String name = FileUtils.getName(root, FileUtils.getNameWithoutExtension(modpackFile));
Path instancePath = root.resolve("instance.cfg");
if (Files.notExists(instancePath))
throw new IOException("`instance.cfg` not found, " + filePath + " is not a valid MultiMC modpack.");
try (InputStream instanceStream = Files.newInputStream(instancePath)) {
MultiMCInstanceConfiguration cfg = new MultiMCInstanceConfiguration(name, instanceStream, manifest);
return new Modpack(cfg.getName(), "", "", cfg.getGameVersion(), cfg.getNotes(), encoding, cfg) {
@Override
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, File zipFile, String name) {
return new MultiMCModpackInstallTask(dependencyManager, zipFile, this, cfg, name);
}
};
Path instancePath = root.resolve("instance.cfg");
if (Files.notExists(instancePath))
throw new IOException("`instance.cfg` not found, " + modpackFile + " is not a valid MultiMC modpack.");
try (InputStream instanceStream = Files.newInputStream(instancePath)) {
MultiMCInstanceConfiguration cfg = new MultiMCInstanceConfiguration(name, instanceStream, manifest);
return new Modpack(cfg.getName(), "", "", cfg.getGameVersion(), cfg.getNotes(), encoding, cfg) {
@Override
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, File zipFile, String name) {
return new MultiMCModpackInstallTask(dependencyManager, zipFile, this, cfg, name);
}
};
}
}
}
}
@@ -32,7 +32,6 @@ import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.ZipFileSystem;
import java.io.File;
import java.io.IOException;
@@ -145,11 +144,11 @@ public final class MultiMCModpackInstallTask extends Task<Void> {
public void execute() throws Exception {
Version version = repository.readVersionJson(name);
try (ZipFileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(zipFile.toPath())) {
try (FileSystem fs = CompressingUtils.readonly(zipFile.toPath()).setAutoDetectEncoding(true).build()) {
Path root = MultiMCInstanceConfiguration.getRootPath(fs.getPath("/"));
Path patches = root.resolve("patches");
if (Files.isDirectory(patches)) {
if (Files.exists(patches)) {
try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(patches)) {
for (Path patchJson : directoryStream) {
if (patchJson.toString().endsWith(".json")) {
@@ -25,12 +25,12 @@ import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
import org.jackhuang.hmcl.util.gson.Validation;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.FileSystem;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
@@ -129,13 +129,13 @@ public class ServerModpackManifest implements Validation {
}
/**
* @param fs the CurseForge modpack file.
* @return the manifest.
* @throws IOException if the file is not a valid zip file.
* @param zip the CurseForge modpack file.
* @throws IOException if the file is not a valid zip file.
* @throws JsonParseException if the server-manifest.json is missing or malformed.
* @return the manifest.
*/
public static Modpack readManifest(FileSystem fs, Charset encoding) throws IOException, JsonParseException {
String json = FileUtils.readText(fs.getPath("server-manifest.json"));
public static Modpack readManifest(Path zip, Charset encoding) throws IOException, JsonParseException {
String json = CompressingUtils.readTextZipEntry(zip, "server-manifest.json", encoding);
ServerModpackManifest manifest = JsonUtils.fromNonNullJson(json, ServerModpackManifest.class);
return manifest.toModpack(encoding);
}
@@ -0,0 +1,67 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util;
import java.util.Objects;
public abstract class Result<T> {
public T get() {
throw new IllegalStateException("TriState not ok");
}
public boolean isOK() {
return false;
}
public boolean isError() {
return false;
}
public static <T> Result<T> ok(T result) {
return new OK<>(Objects.requireNonNull(result));
}
@SuppressWarnings("unchecked")
public static <T> Result<T> error() {
return (Result<T>) Error.INSTANCE;
}
private static class OK<T> extends Result<T> {
private final T result;
public OK(T result) {
this.result = result;
}
@Override
public T get() {
return result;
}
}
private static class Error<T> extends Result<T> {
public static final Error<Void> INSTANCE = new Error<>();
@Override
public boolean isError() {
return true;
}
}
}
@@ -22,6 +22,8 @@ import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.regex.Pattern;
@@ -258,6 +260,10 @@ public final class StringUtils {
return Optional.of(str.substring(0, halfLength) + " ... " + str.substring(str.length() - halfLength));
}
public static boolean isASCII(CharSequence cs) {
return US_ASCII_ENCODER.canEncode(cs);
}
/**
* Class for computing the longest common subsequence between strings.
*/
@@ -295,4 +301,5 @@ public final class StringUtils {
public static final Pattern CHINESE_PATTERN = Pattern.compile("[\\u4e00-\\u9fa5]");
public static final CharsetEncoder US_ASCII_ENCODER = StandardCharsets.US_ASCII.newEncoder();
}
@@ -42,7 +42,6 @@ import java.util.zip.ZipException;
* @author huangyuhui
*/
public final class CompressingUtils {
private static final ZipFileSystemProvider MY_ZIPFS_PROVIDER = new ZipFileSystemProvider();
private static final FileSystemProvider ZIPFS_PROVIDER = FileSystemProvider.installedProviders().stream()
.filter(it -> "jar".equalsIgnoreCase(it.getScheme()))
@@ -154,12 +153,12 @@ public final class CompressingUtils {
return new Builder(zipFile, true).setUseTempFile(true);
}
public static ZipFileSystem createReadOnlyZipFileSystem(Path zipFile) throws IOException {
return new ZipFileSystem(MY_ZIPFS_PROVIDER, new ZipFile(zipFile.toFile()), true);
public static FileSystem createReadOnlyZipFileSystem(Path zipFile) throws IOException {
return createReadOnlyZipFileSystem(zipFile, null);
}
public static ZipFileSystem createReadOnlyZipFileSystem(Path zipFile, Charset charset) throws IOException {
return new ZipFileSystem(MY_ZIPFS_PROVIDER, new ZipFile(zipFile.toFile(), charset.name()), true);
public static FileSystem createReadOnlyZipFileSystem(Path zipFile, Charset charset) throws IOException {
return createZipFileSystem(zipFile, false, false, charset);
}
public static FileSystem createWritableZipFileSystem(Path zipFile) throws IOException {
@@ -200,7 +199,7 @@ public final class CompressingUtils {
*/
public static String readTextZipEntry(File zipFile, String name) throws IOException {
try (ZipFile s = new ZipFile(zipFile)) {
return readTextZipEntry(s, name);
return IOUtils.readFullyAsString(s.getInputStream(s.getEntry(name)), StandardCharsets.UTF_8);
}
}
@@ -209,20 +208,15 @@ public final class CompressingUtils {
*
* @param zipFile the zip file
* @param name the location of the text in zip file, something like A/B/C/D.txt
* @param encoding encoding of zip file.
* @throws IOException if the file is not a valid zip file.
* @return the plain text content of given file.
*/
public static String readTextZipEntry(Path zipFile, String name, Charset encoding) throws IOException {
try (ZipFile s = new ZipFile(zipFile.toFile(), encoding.name())) {
return readTextZipEntry(s, name);
return IOUtils.readFullyAsString(s.getInputStream(s.getEntry(name)), StandardCharsets.UTF_8);
}
}
public static String readTextZipEntry(ZipFile s, String name) throws IOException {
return IOUtils.readFullyAsString(s.getInputStream(s.getEntry(name)), StandardCharsets.UTF_8);
}
/**
* Read the text content of a file in zip.
*
@@ -230,7 +224,7 @@ public final class CompressingUtils {
* @param name the location of the text in zip file, something like A/B/C/D.txt
* @return the plain text content of given file.
*/
public static Optional<String> readTextZipEntryQuietly(ZipFile file, String name) {
public static Optional<String> readTextZipEntryQuietly(File file, String name) {
try {
return Optional.of(readTextZipEntry(file, name));
} catch (IOException e) {
@@ -1,159 +0,0 @@
package org.jackhuang.hmcl.util.io;
import java.util.regex.PatternSyntaxException;
public final class FileSystemUtils {
private FileSystemUtils() {
}
private static char EOL = 0;
private static boolean isRegexMeta(char var0) {
return ".^$+{[]|()".indexOf(var0) != -1;
}
private static boolean isGlobMeta(char var0) {
return "\\*?[{".indexOf(var0) != -1;
}
private static char next(String var0, int var1) {
return var1 < var0.length() ? var0.charAt(var1) : EOL;
}
public static String toRegexPattern(String var0) {
boolean var1 = false;
StringBuilder var2 = new StringBuilder("^");
int var3 = 0;
while(true) {
while(var3 < var0.length()) {
char var4 = var0.charAt(var3++);
switch(var4) {
case '*':
if (next(var0, var3) == '*') {
var2.append(".*");
++var3;
} else {
var2.append("[^/]*");
}
break;
case ',':
if (var1) {
var2.append(")|(?:");
} else {
var2.append(',');
}
break;
case '/':
var2.append(var4);
break;
case '?':
var2.append("[^/]");
break;
case '[':
var2.append("[[^/]&&[");
if (next(var0, var3) == '^') {
var2.append("\\^");
++var3;
} else {
if (next(var0, var3) == '!') {
var2.append('^');
++var3;
}
if (next(var0, var3) == '-') {
var2.append('-');
++var3;
}
}
boolean var6 = false;
char var7 = 0;
while(var3 < var0.length()) {
var4 = var0.charAt(var3++);
if (var4 == ']') {
break;
}
if (var4 == '/') {
throw new PatternSyntaxException("Explicit 'name separator' in class", var0, var3 - 1);
}
if (var4 == '\\' || var4 == '[' || var4 == '&' && next(var0, var3) == '&') {
var2.append('\\');
}
var2.append(var4);
if (var4 == '-') {
if (!var6) {
throw new PatternSyntaxException("Invalid range", var0, var3 - 1);
}
if ((var4 = next(var0, var3++)) == EOL || var4 == ']') {
break;
}
if (var4 < var7) {
throw new PatternSyntaxException("Invalid range", var0, var3 - 3);
}
var2.append(var4);
var6 = false;
} else {
var6 = true;
var7 = var4;
}
}
if (var4 != ']') {
throw new PatternSyntaxException("Missing ']", var0, var3 - 1);
}
var2.append("]]");
break;
case '\\':
if (var3 == var0.length()) {
throw new PatternSyntaxException("No character to escape", var0, var3 - 1);
}
char var5 = var0.charAt(var3++);
if (isGlobMeta(var5) || isRegexMeta(var5)) {
var2.append('\\');
}
var2.append(var5);
break;
case '{':
if (var1) {
throw new PatternSyntaxException("Cannot nest groups", var0, var3 - 1);
}
var2.append("(?:(?:");
var1 = true;
break;
case '}':
if (var1) {
var2.append("))");
var1 = false;
} else {
var2.append('}');
}
break;
default:
if (isRegexMeta(var4)) {
var2.append('\\');
}
var2.append(var4);
}
}
if (var1) {
throw new PatternSyntaxException("Missing '}", var0, var3 - 1);
}
return var2.append('$').toString();
}
}
}
@@ -113,11 +113,11 @@ public final class FileUtils {
}
public static String readText(File file) throws IOException {
return readText(file.toPath());
return readText(file, UTF_8);
}
public static String readText(File file, Charset charset) throws IOException {
return readText(file.toPath(), charset);
return new String(Files.readAllBytes(file.toPath()), charset);
}
public static String readText(Path file) throws IOException {
@@ -125,11 +125,7 @@ public final class FileUtils {
}
public static String readText(Path file, Charset charset) throws IOException {
return IOUtils.readFullyAsString(Files.newInputStream(file), charset);
}
public static byte[] readAllBytes(Path file) throws IOException {
return IOUtils.readFullyAsByteArray(Files.newInputStream(file));
return new String(Files.readAllBytes(file), charset);
}
/**
@@ -101,7 +101,7 @@ public class Unzipper {
*/
public void unzip() throws IOException {
Files.createDirectories(dest);
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(zipFile, encoding)) {
try (FileSystem fs = CompressingUtils.readonly(zipFile).setEncoding(encoding).setAutoDetectEncoding(true).build()) {
Path root = fs.getPath(subDirectory);
if (!root.isAbsolute() || (subDirectory.length() > 1 && subDirectory.endsWith("/")))
throw new IllegalArgumentException("Subdirectory for unzipper must be absolute");
@@ -1,81 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util.io;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
public class ZipFileAttributes implements BasicFileAttributes {
private final long size;
private final boolean symbolicLink;
private final boolean regularFile;
private final boolean directory;
public ZipFileAttributes(long size, boolean symbolicLink, boolean regularFile, boolean directory) {
this.size = size;
this.symbolicLink = symbolicLink;
this.regularFile = regularFile;
this.directory = directory;
}
@Override
public FileTime lastModifiedTime() {
return null;
}
@Override
public FileTime lastAccessTime() {
return null;
}
@Override
public FileTime creationTime() {
return null;
}
@Override
public boolean isRegularFile() {
return regularFile;
}
@Override
public boolean isDirectory() {
return directory;
}
@Override
public boolean isSymbolicLink() {
return symbolicLink;
}
@Override
public boolean isOther() {
return false;
}
@Override
public long size() {
return size;
}
@Override
public Object fileKey() {
return null;
}
}
@@ -1,310 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util.io;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.*;
import java.nio.file.attribute.UserPrincipalLookupService;
import java.nio.file.spi.FileSystemProvider;
import java.util.*;
import java.util.regex.Pattern;
import static org.jackhuang.hmcl.util.Lang.toIterable;
import static org.jackhuang.hmcl.util.io.ZipPath.getPathComponents;
public class ZipFileSystem extends FileSystem {
private final ZipFileSystemProvider provider;
private final ZipFile zipFile;
private final boolean readOnly;
private final IndexNode root;
private final Map<String, IndexNode> entries = new HashMap<>();
final ZipPath rootDir;
private volatile boolean isOpen = true;
public ZipFileSystem(ZipFileSystemProvider provider, ZipFile zipFile, boolean readOnly) {
this.provider = provider;
this.zipFile = zipFile;
this.readOnly = readOnly;
this.root = new IndexNode(null, true, "");
this.rootDir = new ZipPath(this, "/");
buildTree();
}
@Override
public FileSystemProvider provider() {
return provider;
}
@Override
public void close() throws IOException {
isOpen = false;
zipFile.close();
}
@Override
public boolean isOpen() {
return isOpen;
}
@Override
public boolean isReadOnly() {
return readOnly;
}
@Override
public String getSeparator() {
return "/";
}
@Override
public Iterable<Path> getRootDirectories() {
return Collections.singleton(rootDir);
}
@Override
public Iterable<FileStore> getFileStores() {
return null;
}
@Override
public Set<String> supportedFileAttributeViews() {
return null;
}
@NotNull
@Override
public Path getPath(@NotNull String first, @NotNull String @NotNull ... more) {
StringBuilder sb = new StringBuilder(first);
for (String segment : more) {
if (segment.length() > 0) {
if (sb.length() > 0) {
sb.append('/');
}
sb.append(segment);
}
}
return new ZipPath(this, sb.toString());
}
ZipFileAttributes readAttributes(ZipPath path) {
ensureOpen();
Optional<IndexNode> inode = getInode(path);
if (!inode.isPresent()) return null;
return inode.get().getAttributes();
}
InputStream newInputStream(ZipPath path, OpenOption... options) throws IOException {
ensureOpen();
ZipPath realPath = path.toRealPath();
ZipArchiveEntry entry = zipFile.getEntry(realPath.getEntryName());
return zipFile.getInputStream(entry);
}
DirectoryStream<Path> newDirectoryStream(ZipPath dir, DirectoryStream.Filter<? super Path> filter) throws IOException {
Optional<IndexNode> inode = getInode(dir);
if (!inode.isPresent() || !inode.get().isDirectory()) throw new NotDirectoryException(dir.toString());
List<ZipPath> list = new ArrayList<>();
for (IndexNode child = inode.get().child; child != null; child = child.sibling) {
list.add(new ZipPath(this, child.name));
}
return new DirectoryStream<Path>() {
private volatile boolean isClosed = false;
private volatile Iterator<ZipPath> itr;
@Override
public synchronized Iterator<Path> iterator() {
if (isClosed)
throw new ClosedDirectoryStreamException();
if (itr != null)
throw new IllegalStateException("Iterator has already been returned");
itr = list.iterator();
return new Iterator<Path>() {
@Override
public boolean hasNext() {
if (isClosed) return false;
return itr.hasNext();
}
@Override
public Path next() {
if (isClosed) throw new NoSuchElementException();
return itr.next();
}
};
}
@Override
public synchronized void close() {
isClosed = true;
}
};
}
void checkAccess(ZipPath path) throws IOException {
ensureOpen();
if (!getInode(path.getEntryName()).isPresent()) {
throw new NoSuchFileException(path.toString());
}
}
private static final String GLOB_SYNTAX = "glob";
private static final String REGEX_SYNTAX = "regex";
@Override
public PathMatcher getPathMatcher(String syntaxAndInput) {
int pos = syntaxAndInput.indexOf(':');
if (pos <= 0 || pos == syntaxAndInput.length()) {
throw new IllegalArgumentException();
}
String syntax = syntaxAndInput.substring(0, pos);
String input = syntaxAndInput.substring(pos + 1);
String expr;
if (syntax.equalsIgnoreCase(GLOB_SYNTAX)) {
expr = FileSystemUtils.toRegexPattern(input);
} else {
if (syntax.equalsIgnoreCase(REGEX_SYNTAX)) {
expr = input;
} else {
throw new UnsupportedOperationException("Syntax '" + syntax +
"' not recognized");
}
}
// return matcher
final Pattern pattern = Pattern.compile(expr);
return path -> pattern.matcher(path.toString()).matches();
}
@Override
public UserPrincipalLookupService getUserPrincipalLookupService() {
throw new UnsupportedOperationException();
}
@Override
public WatchService newWatchService() {
throw new UnsupportedOperationException();
}
private void ensureOpen() {
if (!isOpen) {
throw new ClosedFileSystemException();
}
}
private Optional<IndexNode> getInode(String entryName) {
return Optional.ofNullable(entries.get(entryName));
}
private Optional<IndexNode> getInode(ZipPath path) {
return getInode(path.toAbsolutePath().normalize().getEntryName());
}
private void buildTree() {
entries.put("", root);
for (ZipArchiveEntry entry : toIterable(zipFile.getEntriesInPhysicalOrder())) {
List<String> components = getPathComponents(entry.getName());
IndexNode node = new IndexNode(entry, entry.isDirectory(), String.join("/", components));
entries.put(node.name, node);
while (true) {
if (components.size() == 0) break;
if (components.size() == 1) {
node.sibling = root.child;
root.child = node;
break;
}
String parentName = String.join("/", components.subList(0, components.size() - 1));
if (entries.containsKey(parentName)) {
IndexNode parent = entries.get(parentName);
node.sibling = parent.child;
parent.child = node;
break;
}
// Add new pseudo directory entry
IndexNode parent = new IndexNode(null, true, parentName);
entries.put(parentName, parent);
node.sibling = parent.child;
parent.child = node;
node = parent;
}
}
}
private class IndexNode {
private final ZipArchiveEntry entry;
private final boolean isDirectory;
private final String name;
private ZipFileAttributes attributes;
public IndexNode(ZipArchiveEntry entry, boolean isDirectory, String name) {
this.entry = entry;
this.isDirectory = isDirectory;
this.name = name;
}
public boolean isDirectory() {
return isDirectory;
}
public String getName() {
return name;
}
public InputStream getInputStream() throws IOException {
if (entry == null) throw new IOException("Entry " + name + " cannot open");
return zipFile.getInputStream(entry);
}
public ZipFileAttributes getAttributes() {
if (attributes == null) {
if (entry == null) {
attributes = new ZipFileAttributes(0, false, false, true);
} else {
attributes = new ZipFileAttributes(
entry.getSize(),
entry.isUnixSymlink(),
!entry.isDirectory(),
entry.isDirectory()
);
}
}
return attributes;
}
IndexNode sibling;
IndexNode child;
}
}
@@ -1,139 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util.io;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileAttribute;
import java.nio.file.attribute.FileAttributeView;
import java.nio.file.spi.FileSystemProvider;
import java.util.Map;
import java.util.Set;
import static org.jackhuang.hmcl.util.io.ZipPath.ensurePath;
public class ZipFileSystemProvider extends FileSystemProvider {
@Override
public String getScheme() {
return "zip";
}
@Override
public ZipFileSystem newFileSystem(URI uri, Map<String, ?> env) throws IOException {
throw new UnsupportedOperationException();
}
@Override
public FileSystem getFileSystem(URI uri) {
throw new UnsupportedOperationException();
}
@NotNull
@Override
public Path getPath(@NotNull URI uri) {
throw new UnsupportedOperationException();
}
@Override
public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException {
throw new UnsupportedOperationException();
}
@Override
public InputStream newInputStream(Path path, OpenOption... options) throws IOException {
ZipPath zipPath = ensurePath(path);
return zipPath.getFileSystem().newInputStream(zipPath, options);
}
@Override
public DirectoryStream<Path> newDirectoryStream(Path dir, DirectoryStream.Filter<? super Path> filter) throws IOException {
ZipPath zipPath = ensurePath(dir);
return zipPath.getFileSystem().newDirectoryStream(zipPath, filter);
}
@Override
public void createDirectory(Path dir, FileAttribute<?>... attrs) throws IOException {
throw new ReadOnlyFileSystemException();
}
@Override
public void delete(Path path) throws IOException {
throw new ReadOnlyFileSystemException();
}
@Override
public void copy(Path source, Path target, CopyOption... options) throws IOException {
throw new ReadOnlyFileSystemException();
}
@Override
public void move(Path source, Path target, CopyOption... options) throws IOException {
throw new ReadOnlyFileSystemException();
}
@Override
public boolean isSameFile(Path path, Path path2) throws IOException {
return path.toRealPath().equals(path2.toRealPath());
}
@Override
public boolean isHidden(Path path) throws IOException {
return false;
}
@Override
public FileStore getFileStore(Path path) throws IOException {
return null;
}
@Override
public void checkAccess(Path path, AccessMode... modes) throws IOException {
ZipPath zipPath = ensurePath(path);
zipPath.checkAccess(modes);
}
@Override
public <V extends FileAttributeView> V getFileAttributeView(Path path, Class<V> type, LinkOption... options) {
return null;
}
@Override
public <A extends BasicFileAttributes> A readAttributes(Path path, Class<A> type, LinkOption... options) throws IOException {
if (type == BasicFileAttributes.class || type == ZipFileAttributes.class) {
//noinspection unchecked
return (A) ensurePath(path).getAttributes();
}
return null;
}
@Override
public Map<String, Object> readAttributes(Path path, String attributes, LinkOption... options) throws IOException {
throw new UnsupportedOperationException();
}
@Override
public void setAttribute(Path path, String attribute, Object value, LinkOption... options) throws IOException {
throw new ReadOnlyFileSystemException();
}
}
@@ -1,379 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util.io;
import org.jackhuang.hmcl.util.Lang;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.nio.file.*;
import java.util.*;
public class ZipPath implements Path {
private final ZipFileSystem zfs;
private final List<String> path;
private List<String> normalized;
private final boolean absolute;
ZipPath(ZipFileSystem zfs, String path) {
this(zfs, getPathComponents(path), path.startsWith("/"));
}
ZipPath(ZipFileSystem zfs, List<String> path, boolean absolute) {
this.zfs = zfs;
this.path = path;
this.absolute = absolute;
}
@NotNull
@Override
public ZipFileSystem getFileSystem() {
return zfs;
}
@Override
public boolean isAbsolute() {
return absolute;
}
@Override
public ZipPath getRoot() {
if (this.isAbsolute())
return zfs.rootDir;
else
return null;
}
@Override
public ZipPath getFileName() {
if (path.size() <= 1) return this;
else return new ZipPath(zfs, Collections.singletonList(path.get(path.size() - 1)), false);
}
@Override
public ZipPath getParent() {
if (path.isEmpty()) return null;
else if (path.size() == 1) return getRoot();
else return new ZipPath(zfs, path.subList(0, path.size() - 1), absolute);
}
@Override
public int getNameCount() {
return path.size();
}
@NotNull
@Override
public ZipPath getName(int index) {
if (index < 0 || index >= path.size()) throw new IllegalArgumentException();
return new ZipPath(zfs, Collections.singletonList(path.get(index)), false);
}
@NotNull
@Override
public ZipPath subpath(int beginIndex, int endIndex) {
if (beginIndex < 0 || beginIndex >= path.size() || endIndex > path.size() || beginIndex >= endIndex) {
throw new IllegalArgumentException();
}
return new ZipPath(zfs, path.subList(beginIndex, endIndex), absolute);
}
@Override
public boolean startsWith(@NotNull Path other) {
ZipPath p1 = this;
ZipPath p2 = ensurePath(other);
if (p1.isAbsolute() != p2.isAbsolute() || p1.path.size() < p2.path.size()) {
return false;
}
int length = p2.path.size();
for (int i = 0; i < length; i++) {
if (!Objects.equals(p1.path.get(i), p2.path.get(i))) {
return false;
}
}
return true;
}
@Override
public boolean startsWith(@NotNull String other) {
return startsWith(getFileSystem().getPath(other));
}
@Override
public boolean endsWith(@NotNull Path other) {
ZipPath p1 = this;
ZipPath p2 = ensurePath(other);
if (p2.isAbsolute() && !p1.isAbsolute() ||
p2.isAbsolute() && p1.isAbsolute() && p1.path.size() != p2.path.size() ||
p1.path.size() < p2.path.size()
) {
return false;
}
int length = p2.path.size();
for (int i = 0; i < length; i++) {
if (!Objects.equals(p1.path.get(p1.path.size() - i - 1), p2.path.get(p2.path.size() - i - 1))) {
return false;
}
}
return true;
}
@Override
public boolean endsWith(@NotNull String other) {
return endsWith(getFileSystem().getPath(other));
}
@NotNull
@Override
public ZipPath normalize() {
if (isNormalizable()) {
doNormalize();
return new ZipPath(zfs, normalized, absolute);
}
return this;
}
private boolean isNormalizable() {
for (String component : path) {
if (".".equals(component) || "..".equals(component)) {
return true;
}
}
return false;
}
private void doNormalize() {
if (normalized != null) return;
Stack<String> stack = new Stack<>();
for (String component : path) {
if (".".equals(component)) {
continue;
} else if ("..".equals(component)) {
if (!stack.isEmpty()) stack.pop();
} else {
stack.push(component);
}
}
normalized = new ArrayList<>(stack);
}
@NotNull
@Override
public ZipPath resolve(@NotNull Path other) {
ZipPath p1 = this;
ZipPath p2 = ensurePath(other);
if (p2.isAbsolute()) return p2;
return new ZipPath(zfs, Lang.merge(p1.path, p2.path), absolute);
}
@NotNull
@Override
public ZipPath resolve(@NotNull String other) {
return resolve(getFileSystem().getPath(other));
}
@NotNull
@Override
public ZipPath resolveSibling(@NotNull Path other) {
ZipPath parent = getParent();
return parent == null ? ensurePath(other) : parent.resolve(other);
}
@NotNull
@Override
public ZipPath resolveSibling(@NotNull String other) {
return resolveSibling(zfs.getPath(other));
}
@NotNull
@Override
public Path relativize(@NotNull Path other) {
ZipPath p1 = this;
ZipPath p2 = ensurePath(other);
if (p2.equals(p1)) return new ZipPath(zfs, Collections.emptyList(), false);
if (p1.isAbsolute() != p2.isAbsolute()) throw new IllegalArgumentException();
int l = Math.min(p1.path.size(), p2.path.size());
int common = 0;
while (common < l && Objects.equals(p1.path.get(common), p2.path.get(common))) common++;
int up = p1.path.size() - common;
List<String> result = new ArrayList<>();
for (int i = 0; i < up; i++) result.add("..");
result.addAll(p2.path);
return new ZipPath(zfs, result, false);
}
@NotNull
@Override
public URI toUri() {
throw new UnsupportedOperationException();
}
@NotNull
@Override
public ZipPath toAbsolutePath() {
if (isAbsolute()) {
return this;
}
return new ZipPath(zfs, path, true);
}
@NotNull
@Override
public ZipPath toRealPath(@NotNull LinkOption... options) throws IOException {
ZipPath absolute = toAbsolutePath().normalize();
absolute.checkAccess();
return absolute;
}
void checkAccess(AccessMode... modes) throws IOException {
boolean w = false;
boolean x = false;
for (AccessMode mode : modes) {
switch (mode) {
case READ:
break;
case WRITE:
w = true;
break;
case EXECUTE:
x = true;
break;
default:
throw new UnsupportedOperationException();
}
}
zfs.checkAccess(toAbsolutePath().normalize());
if ((w && zfs.isReadOnly()) || x) {
throw new AccessDeniedException(toString());
}
}
@NotNull
@Override
public File toFile() {
throw new UnsupportedOperationException();
}
@NotNull
@Override
public WatchKey register(@NotNull WatchService watcher, @NotNull WatchEvent.Kind<?> @NotNull [] events, WatchEvent.Modifier... modifiers) throws IOException {
throw new UnsupportedOperationException();
}
@NotNull
@Override
public WatchKey register(@NotNull WatchService watcher, @NotNull WatchEvent.Kind<?> @NotNull ... events) throws IOException {
throw new UnsupportedOperationException();
}
@NotNull
@Override
public Iterator<Path> iterator() {
return new Iterator<Path>() {
private int index = 0;
public boolean hasNext() {
return index < getNameCount();
}
public Path next() {
if (index < getNameCount()) {
return getName(index++);
}
throw new NoSuchElementException();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ZipPath paths = (ZipPath) o;
return absolute == paths.absolute && path.equals(paths.path);
}
@Override
public int hashCode() {
return Objects.hash(path, absolute);
}
@Override
public int compareTo(@NotNull Path other) {
ZipPath p1 = this;
ZipPath p2 = ensurePath(other);
return p1.toString().compareTo(p2.toString());
}
ZipFileAttributes getAttributes() throws IOException {
ZipFileAttributes attributes = zfs.readAttributes(this);
if (attributes == null) throw new NoSuchFileException(toString());
else return attributes;
}
static List<String> getPathComponents(String path) {
List<String> components = new ArrayList<>();
int lastSlash = 0;
for (int i = 0; i <= path.length(); i++) {
if (i == path.length() || path.charAt(i) == '/' || path.charAt(i) == '\\') {
if (i != lastSlash) {
String component = path.substring(lastSlash, i);
components.add(component);
}
lastSlash = i + 1;
}
}
return components;
}
private static String normalizePath(String path) {
return String.join("/", getPathComponents(path));
}
static ZipPath ensurePath(Path path) {
if (path == null) throw new NullPointerException();
if (!(path instanceof ZipPath)) throw new ProviderMismatchException();
return (ZipPath) path;
}
String getEntryName() {
if (!isAbsolute()) throw new IllegalStateException();
return String.join("/", path);
}
@Override
public String toString() {
String str = String.join("/", path);
if (absolute) return "/" + str;
else return str;
}
}
@@ -1,21 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util.io;
public class ZipFileSystemTest {
}
@@ -1,77 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util.io;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.apache.commons.compress.utils.SeekableInMemoryByteChannel;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Path;
import java.util.function.BiConsumer;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
public class ZipPathTest {
ZipFileSystemProvider provider = new ZipFileSystemProvider();
ZipFileSystem zfs = new ZipFileSystem(provider, new ZipFile(new SeekableInMemoryByteChannel(IOUtils.readFullyAsByteArray(ZipPathTest.class.getResourceAsStream("/test.zip")))), true);
public ZipPathTest() throws IOException {
}
private Path p(String path) {
return zfs.getPath(path);
}
@Test
public void testNormalizePath() throws IOException {
BiConsumer<String, String> equals = (expected, actual) -> {
assertEquals(zfs.getPath(expected), zfs.getPath(actual).toAbsolutePath().normalize());
};
BiConsumer<String, String> notEquals = (expected, actual) -> {
assertNotEquals(zfs.getPath(expected), zfs.getPath(actual).toAbsolutePath().normalize());
};
equals.accept("/a/b/c/d", "/a\\b/c/d");
equals.accept("/a/b/c/d", "/a\\b/c/d/");
equals.accept("/a/b/c/d", "/a\\b/c//d");
equals.accept("/a/b/c/d", "/a\\\\b/c/d");
equals.accept("/a/b/c/d", "a/b/c/d");
equals.accept("/a/b/c/d", "a/b/.c/../c/d");
notEquals.accept("/a/b/c/d", "/a\\b/c");
}
@Test
public void testRelativizePath() throws IOException {
assertEquals(p("../../a/b/c"), p("/a/b/c/a/b/c").relativize(p("/a/b/c/d/e")));
assertEquals(p("../.."), p("/a/b/c").relativize(p("/a/b/c/d/e")));
assertEquals(p("../../"), p("/a/b/c").relativize(p("/a/b/c/d/e")));
assertEquals(p("../../"), p("/a/b/c/").relativize(p("/a/b/c/d/e")));
assertEquals(p("../.."), p("/a/b/c/").relativize(p("/a/b/c/d/e")));
assertEquals(p(""), p("/a/b/c/").relativize(p("/a/b/c")));
assertEquals(p(""), p("/a/b/c").relativize(p("/a/b/c")));
assertEquals(p(""), p("/a/b/c").relativize(p("/a/b/c/")));
assertEquals(p(""), p("/a/b/c/").relativize(p("/a/b/c/")));
}
}