feat: MCBBS modpack

This commit is contained in:
huanghongxun
2021-01-16 16:17:46 +08:00
parent 3f2be63d08
commit f2a86857bc
30 changed files with 1948 additions and 375 deletions

View File

@@ -19,13 +19,24 @@ package org.jackhuang.hmcl.game;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.mod.Modpack; 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.Profile;
import org.jackhuang.hmcl.setting.ProxyManager;
import org.jackhuang.hmcl.setting.VersionSetting; import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Logging; import org.jackhuang.hmcl.util.Logging;
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.FileUtils;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@@ -33,6 +44,7 @@ import java.nio.file.Files;
import java.util.*; import java.util.*;
import java.util.logging.Level; import java.util.logging.Level;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.newImage; import static org.jackhuang.hmcl.ui.FXUtils.newImage;
public class HMCLGameRepository extends DefaultGameRepository { public class HMCLGameRepository extends DefaultGameRepository {
@@ -273,6 +285,54 @@ public class HMCLGameRepository extends DefaultGameRepository {
return FORBIDDEN.contains(id); return FORBIDDEN.contains(id);
} }
public LaunchOptions getLaunchOptions(String version, File gameDir, boolean checkJava) throws InterruptedException {
VersionSetting vs = getVersionSetting(version);
JavaVersion javaVersion = Optional.ofNullable(vs.getJavaVersion(checkJava)).orElse(JavaVersion.fromCurrentEnvironment());
LaunchOptions.Builder builder = new LaunchOptions.Builder()
.setGameDir(gameDir)
.setJava(javaVersion)
.setVersionName(Metadata.TITLE)
.setVersionType(Metadata.TITLE)
.setProfileName(Metadata.TITLE)
.setGameArguments(StringUtils.tokenize(vs.getMinecraftArgs()))
.setJavaArguments(StringUtils.tokenize(vs.getJavaArgs()))
.setMaxMemory(vs.getMaxMemory())
.setMinMemory(vs.getMinMemory())
.setMetaspace(Lang.toIntOrNull(vs.getPermSize()))
.setWidth(vs.getWidth())
.setHeight(vs.getHeight())
.setFullscreen(vs.isFullscreen())
.setServerIp(vs.getServerIp())
.setWrapper(vs.getWrapper())
.setPrecalledCommand(vs.getPreLaunchCommand())
.setNoGeneratedJVMArgs(vs.isNoJVMArgs());
if (config().hasProxy()) {
builder.setProxy(ProxyManager.getProxy());
if (config().hasProxyAuth()) {
builder.setProxyUser(config().getProxyUser());
builder.setProxyPass(config().getProxyPass());
}
}
File json = getModpackConfiguration(version);
if (json.exists()) {
try {
String jsonText = FileUtils.readText(json);
ModpackConfiguration<?> modpackConfiguration = JsonUtils.GSON.fromJson(jsonText, ModpackConfiguration.class);
if (McbbsModpackLocalInstallTask.MODPACK_TYPE.equals(modpackConfiguration.getType())) {
ModpackConfiguration<McbbsModpackManifest> config = JsonUtils.GSON.fromJson(FileUtils.readText(json), new TypeToken<ModpackConfiguration<McbbsModpackManifest>>() {
}.getType());
config.getManifest().injectLaunchOptions(builder);
}
} catch (IOException | JsonParseException e) {
e.printStackTrace();
}
}
return builder.create();
}
@Override @Override
public File getModpackConfiguration(String version) { public File getModpackConfiguration(String version) {
return new File(getVersionRoot(version), "modpack.cfg"); return new File(getVersionRoot(version), "modpack.cfg");

View File

@@ -1,74 +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.game;
import org.jackhuang.hmcl.mod.ModAdviser;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.Zipper;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* Export the game to a mod pack file.
*/
public class HMCLModpackExportTask extends Task<Void> {
private final DefaultGameRepository repository;
private final String version;
private final List<String> whitelist;
private final Modpack modpack;
private final File output;
/**
* @param output mod pack file.
* @param version to locate version.json
*/
public HMCLModpackExportTask(DefaultGameRepository repository, String version, List<String> whitelist, Modpack modpack, File output) {
this.repository = repository;
this.version = version;
this.whitelist = whitelist;
this.modpack = modpack;
this.output = output;
onDone().register(event -> {
if (event.isFailed()) output.delete();
});
}
@Override
public void execute() throws Exception {
ArrayList<String> blackList = new ArrayList<>(ModAdviser.MODPACK_BLACK_LIST);
blackList.add(version + ".jar");
blackList.add(version + ".json");
Logging.LOG.info("Compressing game files without some files in blacklist, including files or directories: usernamecache.json, asm, logs, backups, versions, assets, usercache.json, libraries, crash-reports, launcher_profiles.json, NVIDIA, TCNodeTracker");
try (Zipper zip = new Zipper(output.toPath())) {
zip.putDirectory(repository.getRunDirectory(version).toPath(), "minecraft", path -> Modpack.acceptFile(path, blackList, whitelist));
Version mv = repository.getResolvedPreservingPatchesVersion(version);
String gameVersion = GameVersion.minecraftVersion(repository.getVersionJar(version))
.orElseThrow(() -> new IOException("Cannot parse the version of " + version));
zip.putTextFile(JsonUtils.GSON.toJson(mv.setJar(gameVersion)), "minecraft/pack.json"); // Making "jar" to gameVersion is to be compatible with old HMCL.
zip.putTextFile(JsonUtils.GSON.toJson(modpack.setGameVersion(gameVersion)), "modpack.json"); // Newer HMCL only reads 'gameVersion' field.
}
}
}

View File

@@ -169,7 +169,7 @@ public final class LauncherHelper {
repository, repository,
version.getPatches().isEmpty() ? repository.getResolvedVersion(selectedVersion) : version, version.getPatches().isEmpty() ? repository.getResolvedVersion(selectedVersion) : version,
authInfo, authInfo,
setting.toLaunchOptions(profile.getGameDir(), !setting.isNotCheckJVM()), repository.getLaunchOptions(selectedVersion, profile.getGameDir(), !setting.isNotCheckJVM()),
launcherVisibility == LauncherVisibility.CLOSE launcherVisibility == LauncherVisibility.CLOSE
? null // Unnecessary to start listening to game process output when close launcher immediately after game launched. ? null // Unnecessary to start listening to game process output when close launcher immediately after game launched.
: new HMCLProcessListener(repository, selectedVersion, authInfo, launchingLatch, gameVersion.isPresent()) : new HMCLProcessListener(repository, selectedVersion, authInfo, launchingLatch, gameVersion.isPresent())

View File

@@ -23,6 +23,8 @@ import org.jackhuang.hmcl.mod.*;
import org.jackhuang.hmcl.mod.curse.CurseCompletionException; import org.jackhuang.hmcl.mod.curse.CurseCompletionException;
import org.jackhuang.hmcl.mod.curse.CurseInstallTask; import org.jackhuang.hmcl.mod.curse.CurseInstallTask;
import org.jackhuang.hmcl.mod.curse.CurseManifest; import org.jackhuang.hmcl.mod.curse.CurseManifest;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackLocalInstallTask;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackManifest;
import org.jackhuang.hmcl.mod.multimc.MultiMCInstanceConfiguration; import org.jackhuang.hmcl.mod.multimc.MultiMCInstanceConfiguration;
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackInstallTask; import org.jackhuang.hmcl.mod.multimc.MultiMCModpackInstallTask;
import org.jackhuang.hmcl.mod.server.ServerModpackLocalInstallTask; import org.jackhuang.hmcl.mod.server.ServerModpackLocalInstallTask;
@@ -43,12 +45,20 @@ import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
public final class ModpackHelper { public final class ModpackHelper {
private ModpackHelper() {} private ModpackHelper() {}
public static Modpack readModpackManifest(Path file, Charset charset) throws UnsupportedModpackException { public static Modpack readModpackManifest(Path file, Charset charset) throws UnsupportedModpackException {
try {
return McbbsModpackManifest.readManifest(file, charset);
} catch (Exception ignored) {
ignored.printStackTrace();
// ignore it, not a valid MCBBS modpack.
}
try { try {
return CurseManifest.readCurseForgeModpackManifest(file, charset); return CurseManifest.readCurseForgeModpackManifest(file, charset);
} catch (Exception e) { } catch (Exception e) {
@@ -148,12 +158,16 @@ public final class ModpackHelper {
return new HMCLModpackInstallTask(profile, zipFile, modpack, name) return new HMCLModpackInstallTask(profile, zipFile, modpack, name)
.whenComplete(Schedulers.defaultScheduler(), success, failure); .whenComplete(Schedulers.defaultScheduler(), success, failure);
else if (modpack.getManifest() instanceof MultiMCInstanceConfiguration) else if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
return new MultiMCModpackInstallTask(profile.getDependency(), zipFile, modpack, ((MultiMCInstanceConfiguration) modpack.getManifest()), name) return new MultiMCModpackInstallTask(profile.getDependency(), zipFile, modpack, (MultiMCInstanceConfiguration) modpack.getManifest(), name)
.whenComplete(Schedulers.defaultScheduler(), success, failure) .whenComplete(Schedulers.defaultScheduler(), success, failure)
.thenComposeAsync(new MultiMCInstallVersionSettingTask(profile, ((MultiMCInstanceConfiguration) modpack.getManifest()), name)); .thenComposeAsync(createMultiMCPostInstallTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name));
else if (modpack.getManifest() instanceof ServerModpackManifest) else if (modpack.getManifest() instanceof ServerModpackManifest)
return new ServerModpackLocalInstallTask(profile.getDependency(), zipFile, modpack, ((ServerModpackManifest) modpack.getManifest()), name) return new ServerModpackLocalInstallTask(profile.getDependency(), zipFile, modpack, (ServerModpackManifest) modpack.getManifest(), name)
.whenComplete(Schedulers.defaultScheduler(), success, failure); .whenComplete(Schedulers.defaultScheduler(), success, failure);
else if (modpack.getManifest() instanceof McbbsModpackManifest)
return new McbbsModpackLocalInstallTask(profile.getDependency(), zipFile, modpack, (McbbsModpackManifest) modpack.getManifest(), name)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.thenComposeAsync(createMcbbsPostInstallTask(profile, (McbbsModpackManifest) modpack.getManifest(), name));
else throw new IllegalArgumentException("Unrecognized modpack: " + modpack.getManifest()); else throw new IllegalArgumentException("Unrecognized modpack: " + modpack.getManifest());
} }
@@ -232,5 +246,18 @@ public final class ModpackHelper {
} }
} }
private static Task<Void> createMultiMCPostInstallTask(Profile profile, MultiMCInstanceConfiguration manifest, String version) {
return Task.runAsync(Schedulers.javafx(), () -> {
VersionSetting vs = Objects.requireNonNull(profile.getRepository().specializeVersionSetting(version));
ModpackHelper.toVersionSetting(manifest, vs);
});
}
private static Task<Void> createMcbbsPostInstallTask(Profile profile, McbbsModpackManifest manifest, String version) {
return Task.runAsync(Schedulers.javafx(), () -> {
VersionSetting vs = Objects.requireNonNull(profile.getRepository().specializeVersionSetting(version));
if (manifest.getLaunchInfo().getMinMemory() > vs.getMaxMemory())
vs.setMaxMemory(manifest.getLaunchInfo().getMinMemory());
});
}
} }

View File

@@ -1,46 +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.game;
import org.jackhuang.hmcl.mod.multimc.MultiMCInstanceConfiguration;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import java.util.Objects;
public final class MultiMCInstallVersionSettingTask extends Task<Void> {
private final Profile profile;
private final MultiMCInstanceConfiguration manifest;
private final String version;
public MultiMCInstallVersionSettingTask(Profile profile, MultiMCInstanceConfiguration manifest, String version) {
this.profile = profile;
this.manifest = manifest;
this.version = version;
setExecutor(Schedulers.javafx());
}
@Override
public void execute() {
VersionSetting vs = Objects.requireNonNull(profile.getRepository().specializeVersionSetting(version));
ModpackHelper.toVersionSetting(manifest, vs);
}
}

View File

@@ -21,16 +21,13 @@ import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter; import com.google.gson.annotations.JsonAdapter;
import javafx.beans.InvalidationListener; import javafx.beans.InvalidationListener;
import javafx.beans.property.*; import javafx.beans.property.*;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.game.GameDirectoryType; import org.jackhuang.hmcl.game.GameDirectoryType;
import org.jackhuang.hmcl.game.LaunchOptions;
import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.platform.JavaVersion; import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.util.platform.OperatingSystem; import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform; import org.jackhuang.hmcl.util.platform.Platform;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.nio.file.InvalidPathException; import java.nio.file.InvalidPathException;
@@ -39,8 +36,6 @@ import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
/** /**
* *
* @author huangyuhui * @author huangyuhui
@@ -533,36 +528,6 @@ public final class VersionSetting implements Cloneable {
defaultJavaPathProperty.addListener(listener); defaultJavaPathProperty.addListener(listener);
} }
public LaunchOptions toLaunchOptions(File gameDir, boolean checkJava) throws InterruptedException {
JavaVersion javaVersion = Optional.ofNullable(getJavaVersion(checkJava)).orElse(JavaVersion.fromCurrentEnvironment());
LaunchOptions.Builder builder = new LaunchOptions.Builder()
.setGameDir(gameDir)
.setJava(javaVersion)
.setVersionName(Metadata.TITLE)
.setVersionType(Metadata.TITLE)
.setProfileName(Metadata.TITLE)
.setMinecraftArgs(getMinecraftArgs())
.setJavaArgs(getJavaArgs())
.setMaxMemory(getMaxMemory())
.setMinMemory(getMinMemory())
.setMetaspace(Lang.toIntOrNull(getPermSize()))
.setWidth(getWidth())
.setHeight(getHeight())
.setFullscreen(isFullscreen())
.setServerIp(getServerIp())
.setWrapper(getWrapper())
.setPrecalledCommand(getPreLaunchCommand())
.setNoGeneratedJVMArgs(isNoJVMArgs());
if (config().hasProxy()) {
builder.setProxy(ProxyManager.getProxy());
if (config().hasProxyAuth()) {
builder.setProxyUser(config().getProxyUser());
builder.setProxyPass(config().getProxyPass());
}
}
return builder.create();
}
@Override @Override
public VersionSetting clone() { public VersionSetting clone() {
VersionSetting versionSetting = new VersionSetting(); VersionSetting versionSetting = new VersionSetting();

View File

@@ -17,9 +17,6 @@
*/ */
package org.jackhuang.hmcl.ui; package org.jackhuang.hmcl.ui;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Worker;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.scene.web.WebEngine; import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView; import javafx.scene.web.WebView;
@@ -41,7 +38,6 @@ public class WebStage extends Stage {
getScene().getStylesheets().addAll(config().getTheme().getStylesheets()); getScene().getStylesheets().addAll(config().getTheme().getStylesheets());
getIcons().add(newImage("/assets/img/icon.png")); getIcons().add(newImage("/assets/img/icon.png"));
webView.setContextMenuEnabled(false); webView.setContextMenuEnabled(false);
titleProperty().bind(webEngine.titleProperty());
} }
public WebView getWebView() { public WebView getWebView() {

View File

@@ -35,6 +35,8 @@ public class MicrosoftAccountLoginStage extends WebStage implements MicrosoftSer
super(600, 600); super(600, 600);
initModality(Modality.APPLICATION_MODAL); initModality(Modality.APPLICATION_MODAL);
titleProperty().bind(webEngine.titleProperty());
webEngine.locationProperty().addListener((observable, oldValue, newValue) -> { webEngine.locationProperty().addListener((observable, oldValue, newValue) -> {
if (urlTester != null && urlTester.test(newValue)) { if (urlTester != null && urlTester.test(newValue)) {
future.complete(newValue); future.complete(newValue);

View File

@@ -34,14 +34,16 @@ import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
import org.jackhuang.hmcl.download.game.GameInstallTask; import org.jackhuang.hmcl.download.game.GameInstallTask;
import org.jackhuang.hmcl.download.liteloader.LiteLoaderInstallTask; import org.jackhuang.hmcl.download.liteloader.LiteLoaderInstallTask;
import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask; import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
import org.jackhuang.hmcl.game.HMCLModpackExportTask;
import org.jackhuang.hmcl.game.HMCLModpackInstallTask; import org.jackhuang.hmcl.game.HMCLModpackInstallTask;
import org.jackhuang.hmcl.mod.MinecraftInstanceTask; import org.jackhuang.hmcl.mod.MinecraftInstanceTask;
import org.jackhuang.hmcl.mod.ModpackInstallTask; import org.jackhuang.hmcl.mod.ModpackInstallTask;
import org.jackhuang.hmcl.mod.ModpackUpdateTask; import org.jackhuang.hmcl.mod.ModpackUpdateTask;
import org.jackhuang.hmcl.mod.curse.CurseCompletionTask; import org.jackhuang.hmcl.mod.curse.CurseCompletionTask;
import org.jackhuang.hmcl.mod.curse.CurseInstallTask; import org.jackhuang.hmcl.mod.curse.CurseInstallTask;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackExportTask;
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackExportTask;
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackInstallTask; import org.jackhuang.hmcl.mod.multimc.MultiMCModpackInstallTask;
import org.jackhuang.hmcl.mod.server.ServerModpackExportTask;
import org.jackhuang.hmcl.setting.Theme; import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor; import org.jackhuang.hmcl.task.TaskExecutor;
@@ -128,7 +130,7 @@ public final class TaskListPane extends StackPane {
task.setName(i18n("modpack.install", i18n("modpack.type.multimc"))); task.setName(i18n("modpack.install", i18n("modpack.type.multimc")));
} else if (task instanceof HMCLModpackInstallTask) { } else if (task instanceof HMCLModpackInstallTask) {
task.setName(i18n("modpack.install", i18n("modpack.type.hmcl"))); task.setName(i18n("modpack.install", i18n("modpack.type.hmcl")));
} else if (task instanceof HMCLModpackExportTask) { } else if (task instanceof McbbsModpackExportTask || task instanceof MultiMCModpackExportTask || task instanceof ServerModpackExportTask) {
task.setName(i18n("modpack.export")); task.setName(i18n("modpack.export"));
} else if (task instanceof MinecraftInstanceTask) { } else if (task instanceof MinecraftInstanceTask) {
task.setName(i18n("modpack.scan")); task.setName(i18n("modpack.scan"));

View File

@@ -19,32 +19,25 @@ package org.jackhuang.hmcl.ui.export;
import javafx.scene.Node; import javafx.scene.Node;
import org.jackhuang.hmcl.Launcher; import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.game.HMCLModpackExportTask;
import org.jackhuang.hmcl.mod.ModAdviser; import org.jackhuang.hmcl.mod.ModAdviser;
import org.jackhuang.hmcl.mod.Modpack; import org.jackhuang.hmcl.mod.ModpackExportInfo;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackExportTask;
import org.jackhuang.hmcl.mod.multimc.MultiMCInstanceConfiguration; import org.jackhuang.hmcl.mod.multimc.MultiMCInstanceConfiguration;
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackExportTask; import org.jackhuang.hmcl.mod.multimc.MultiMCModpackExportTask;
import org.jackhuang.hmcl.mod.server.ServerModpackExportTask; import org.jackhuang.hmcl.mod.server.ServerModpackExportTask;
import org.jackhuang.hmcl.setting.Config;
import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.VersionSetting; import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.wizard.WizardController; import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardProvider; import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.io.Zipper;
import java.io.File; import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
public final class ExportWizardProvider implements WizardProvider { public final class ExportWizardProvider implements WizardProvider {
private final Profile profile; private final Profile profile;
private final String version; private final String version;
@@ -62,73 +55,31 @@ public final class ExportWizardProvider implements WizardProvider {
public Object finish(Map<String, Object> settings) { public Object finish(Map<String, Object> settings) {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
List<String> whitelist = (List<String>) settings.get(ModpackFileSelectionPage.MODPACK_FILE_SELECTION); List<String> whitelist = (List<String>) settings.get(ModpackFileSelectionPage.MODPACK_FILE_SELECTION);
File modpackFile = (File) settings.get(ModpackInfoPage.MODPACK_FILE); ModpackExportInfo exportInfo = (ModpackExportInfo) settings.get(ModpackInfoPage.MODPACK_INFO);
String modpackName = (String) settings.get(ModpackInfoPage.MODPACK_NAME); exportInfo.setWhitelist(whitelist);
String modpackAuthor = (String) settings.get(ModpackInfoPage.MODPACK_AUTHOR);
String modpackFileApi = (String) settings.get(ModpackInfoPage.MODPACK_FILE_API);
String modpackVersion = (String) settings.get(ModpackInfoPage.MODPACK_VERSION);
String modpackDescription = (String) settings.get(ModpackInfoPage.MODPACK_DESCRIPTION);
String modpackType = (String) settings.get(ModpackTypeSelectionPage.MODPACK_TYPE); String modpackType = (String) settings.get(ModpackTypeSelectionPage.MODPACK_TYPE);
boolean includeLauncher = (Boolean) settings.get(ModpackInfoPage.MODPACK_INCLUDE_LAUNCHER);
switch (modpackType) { switch (modpackType) {
case ModpackTypeSelectionPage.MODPACK_TYPE_HMCL: case ModpackTypeSelectionPage.MODPACK_TYPE_MCBBS:
return exportAsHMCL(whitelist, modpackFile, modpackName, modpackAuthor, modpackVersion, modpackDescription, includeLauncher); return exportAsMcbbs(exportInfo);
case ModpackTypeSelectionPage.MODPACK_TYPE_MULTIMC: case ModpackTypeSelectionPage.MODPACK_TYPE_MULTIMC:
return exportAsMultiMC(whitelist, modpackFile, modpackName, modpackAuthor, modpackVersion, modpackDescription); return exportAsMultiMC(exportInfo);
case ModpackTypeSelectionPage.MODPACK_TYPE_SERVER: case ModpackTypeSelectionPage.MODPACK_TYPE_SERVER:
return exportAsServer(whitelist, modpackFile, modpackName, modpackAuthor, modpackVersion, modpackDescription, modpackFileApi); return exportAsServer(exportInfo);
default: default:
throw new IllegalStateException("Unrecognized modpack type " + modpackType); throw new IllegalStateException("Unrecognized modpack type " + modpackType);
} }
} }
private Task<?> exportAsHMCL(List<String> whitelist, File modpackFile, String modpackName, String modpackAuthor, String modpackVersion, String modpackDescription, boolean includeLauncherRaw) { private Task<?> exportAsMcbbs(ModpackExportInfo exportInfo) {
List<File> launcherJar = Launcher.getCurrentJarFiles(); List<File> launcherJar = Launcher.getCurrentJarFiles();
boolean includeLauncher = includeLauncherRaw && launcherJar != null;
return new Task<Void>() { return new Task<Void>() {
Task<?> dependency = null; Task<?> dependency = null;
@Override @Override
public void execute() throws Exception { public void execute() {
File tempModpack = includeLauncher ? Files.createTempFile("hmcl", ".zip").toFile() : modpackFile; dependency = new McbbsModpackExportTask(profile.getRepository(), version, exportInfo);
dependency = new HMCLModpackExportTask(profile.getRepository(), version, whitelist,
new Modpack(modpackName, modpackAuthor, modpackVersion, null, modpackDescription, StandardCharsets.UTF_8, null), tempModpack);
if (includeLauncher) {
dependency = dependency.thenRunAsync(() -> {
try (Zipper zip = new Zipper(modpackFile.toPath())) {
Config exported = new Config();
exported.setBackgroundImageType(config().getBackgroundImageType());
exported.setBackgroundImage(config().getBackgroundImage());
exported.setTheme(config().getTheme());
exported.setDownloadType(config().getDownloadType());
exported.setPreferredLoginType(config().getPreferredLoginType());
exported.getAuthlibInjectorServers().setAll(config().getAuthlibInjectorServers());
zip.putTextFile(exported.toJson(), ConfigHolder.CONFIG_FILENAME);
zip.putFile(tempModpack, "modpack.zip");
File bg = new File("bg").getAbsoluteFile();
if (bg.isDirectory())
zip.putDirectory(bg.toPath(), "bg");
File background_png = new File("background.png").getAbsoluteFile();
if (background_png.isFile())
zip.putFile(background_png, "background.png");
File background_jpg = new File("background.jpg").getAbsoluteFile();
if (background_jpg.isFile())
zip.putFile(background_jpg, "background.jpg");
for (File jar : launcherJar)
zip.putFile(jar, jar.getName());
}
});
}
} }
@Override @Override
@@ -138,30 +89,30 @@ public final class ExportWizardProvider implements WizardProvider {
}; };
} }
private Task<?> exportAsMultiMC(List<String> whitelist, File modpackFile, String modpackName, String modpackAuthor, String modpackVersion, String modpackDescription) { private Task<?> exportAsMultiMC(ModpackExportInfo exportInfo) {
return new Task<Void>() { return new Task<Void>() {
Task<?> dependency; Task<?> dependency;
@Override @Override
public void execute() { public void execute() {
VersionSetting vs = profile.getVersionSetting(version); VersionSetting vs = profile.getVersionSetting(version);
dependency = new MultiMCModpackExportTask(profile.getRepository(), version, whitelist, dependency = new MultiMCModpackExportTask(profile.getRepository(), version, exportInfo.getWhitelist(),
new MultiMCInstanceConfiguration( new MultiMCInstanceConfiguration(
"OneSix", "OneSix",
modpackName + "-" + modpackVersion, exportInfo.getName() + "-" + exportInfo.getVersion(),
null, null,
Lang.toIntOrNull(vs.getPermSize()), Lang.toIntOrNull(vs.getPermSize()),
vs.getWrapper(), vs.getWrapper(),
vs.getPreLaunchCommand(), vs.getPreLaunchCommand(),
null, null,
modpackDescription, exportInfo.getDescription(),
null, null,
vs.getJavaArgs(), exportInfo.getJavaArguments(),
vs.isFullscreen(), vs.isFullscreen(),
vs.getWidth(), vs.getWidth(),
vs.getHeight(), vs.getHeight(),
vs.getMaxMemory(), vs.getMaxMemory(),
vs.getMinMemory(), exportInfo.getMinMemory(),
vs.isShowLogs(), vs.isShowLogs(),
/* showConsoleOnError */ true, /* showConsoleOnError */ true,
/* autoCloseConsole */ false, /* autoCloseConsole */ false,
@@ -171,7 +122,7 @@ public final class ExportWizardProvider implements WizardProvider {
/* overrideConsole */ true, /* overrideConsole */ true,
/* overrideCommands */ true, /* overrideCommands */ true,
/* overrideWindow */ true /* overrideWindow */ true
), modpackFile); ), exportInfo.getOutput().toFile());
} }
@Override @Override
@@ -181,13 +132,13 @@ public final class ExportWizardProvider implements WizardProvider {
}; };
} }
private Task<?> exportAsServer(List<String> whitelist, File modpackFile, String modpackName, String modpackAuthor, String modpackVersion, String modpackDescription, String modpackFileApi) { private Task<?> exportAsServer(ModpackExportInfo exportInfo) {
return new Task<Void>() { return new Task<Void>() {
Task<?> dependency; Task<?> dependency;
@Override @Override
public void execute() { public void execute() {
dependency = new ServerModpackExportTask(profile.getRepository(), version, whitelist, modpackName, modpackAuthor, modpackVersion, modpackDescription, modpackFileApi, modpackFile); dependency = new ServerModpackExportTask(profile.getRepository(), version, exportInfo);
} }
@Override @Override
@@ -203,7 +154,7 @@ public final class ExportWizardProvider implements WizardProvider {
case 0: case 0:
return new ModpackTypeSelectionPage(controller); return new ModpackTypeSelectionPage(controller);
case 1: case 1:
return new ModpackInfoPage(controller, version); return new ModpackInfoPage(controller, profile.getRepository(), version);
case 2: case 2:
return new ModpackFileSelectionPage(controller, profile, version, ModAdviser::suggestMod); return new ModpackFileSelectionPage(controller, profile, version, ModAdviser::suggestMod);
default: default:

View File

@@ -17,76 +17,100 @@
*/ */
package org.jackhuang.hmcl.ui.export; package org.jackhuang.hmcl.ui.export;
import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.*;
import com.jfoenix.controls.JFXTextArea;
import com.jfoenix.controls.JFXTextField;
import com.jfoenix.controls.JFXToggleButton;
import com.jfoenix.validation.RequiredFieldValidator; import com.jfoenix.validation.RequiredFieldValidator;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty; import javafx.beans.property.*;
import javafx.beans.property.SimpleBooleanProperty; import javafx.collections.ObservableList;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.event.EventHandler; import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.input.MouseEvent; import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.TextAlignment; import javafx.scene.text.TextAlignment;
import javafx.stage.FileChooser; import javafx.stage.FileChooser;
import org.jackhuang.hmcl.Launcher; import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.mod.ModpackExportInfo;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackManifest;
import org.jackhuang.hmcl.setting.Accounts; import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.ComponentList; import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.NumberValidator;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.ui.construct.Validator; import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.wizard.WizardController; import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage; import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.StringUtils;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.net.URL; import java.net.URL;
import java.util.List; import java.util.*;
import java.util.Map;
import java.util.Optional;
import static org.jackhuang.hmcl.ui.export.ModpackTypeSelectionPage.*; import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.jfxListCellFactory;
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
import static org.jackhuang.hmcl.ui.export.ModpackTypeSelectionPage.MODPACK_TYPE;
import static org.jackhuang.hmcl.ui.export.ModpackTypeSelectionPage.MODPACK_TYPE_SERVER;
import static org.jackhuang.hmcl.util.Lang.tryCast;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class ModpackInfoPage extends Control implements WizardPage { public final class ModpackInfoPage extends Control implements WizardPage {
private final WizardController controller; private final WizardController controller;
private final HMCLGameRepository gameRepository;
private final ModpackExportInfo.Options options;
private final String versionName;
private final boolean canIncludeLauncher; private final boolean canIncludeLauncher;
private final boolean showFileApi;
private SimpleStringProperty versionName = new SimpleStringProperty(); private final ModpackExportInfo exportInfo = new ModpackExportInfo();
private SimpleStringProperty modpackName = new SimpleStringProperty();
private SimpleStringProperty modpackFileApi = new SimpleStringProperty();
private SimpleStringProperty modpackAuthor = new SimpleStringProperty();
private SimpleStringProperty modpackVersion = new SimpleStringProperty("1.0");
private SimpleStringProperty modpackDescription = new SimpleStringProperty();
private SimpleBooleanProperty includingLauncher = new SimpleBooleanProperty();
private ObjectProperty<EventHandler<? super MouseEvent>> next = new SimpleObjectProperty<>();
public ModpackInfoPage(WizardController controller, String version) { private final SimpleStringProperty name = new SimpleStringProperty("");
private final SimpleStringProperty author = new SimpleStringProperty("");
private final SimpleStringProperty version = new SimpleStringProperty("1.0");
private final SimpleStringProperty description = new SimpleStringProperty("");
private final SimpleStringProperty url = new SimpleStringProperty("");
private final SimpleBooleanProperty forceUpdate = new SimpleBooleanProperty();
private final SimpleBooleanProperty packWithLauncher = new SimpleBooleanProperty();
private final SimpleStringProperty fileApi = new SimpleStringProperty();
private final SimpleIntegerProperty minMemory = new SimpleIntegerProperty(0);
private final SimpleStringProperty authlibInjectorServer = new SimpleStringProperty();
private final SimpleStringProperty launchArguments = new SimpleStringProperty("");
private final SimpleStringProperty javaArguments = new SimpleStringProperty("");
private final ObjectProperty<EventHandler<? super MouseEvent>> next = new SimpleObjectProperty<>();
private final SimpleStringProperty mcbbsThreadId = new SimpleStringProperty();
public ModpackInfoPage(WizardController controller, HMCLGameRepository gameRepository, String version) {
this.controller = controller; this.controller = controller;
modpackName.set(version); this.gameRepository = gameRepository;
modpackAuthor.set(Optional.ofNullable(Accounts.getSelectedAccount()).map(Account::getUsername).orElse("")); this.options = tryCast(controller.getSettings().get(MODPACK_INFO_OPTION), ModpackExportInfo.Options.class)
versionName.set(version); .orElseThrow(() -> new IllegalArgumentException("Settings.MODPACK_INFO_OPTION is required"));
this.versionName = version;
name.set(version);
author.set(Optional.ofNullable(Accounts.getSelectedAccount()).map(Account::getUsername).orElse(""));
VersionSetting versionSetting = gameRepository.getVersionSetting(versionName);
minMemory.set(Optional.ofNullable(versionSetting.getMinMemory()).orElse(0));
launchArguments.set(versionSetting.getMinecraftArgs());
javaArguments.set(versionSetting.getJavaArgs());
List<File> launcherJar = Launcher.getCurrentJarFiles(); List<File> launcherJar = Launcher.getCurrentJarFiles();
canIncludeLauncher = launcherJar != null; canIncludeLauncher = launcherJar != null;
showFileApi = controller.getSettings().get(MODPACK_TYPE) == MODPACK_TYPE_SERVER;
next.set(e -> onNext()); next.set(e -> onNext());
} }
@FXML
private void onNext() { private void onNext() {
FileChooser fileChooser = new FileChooser(); FileChooser fileChooser = new FileChooser();
fileChooser.setTitle(i18n("modpack.wizard.step.initialization.save")); fileChooser.setTitle(i18n("modpack.wizard.step.initialization.save"));
@@ -96,24 +120,35 @@ public final class ModpackInfoPage extends Control implements WizardPage {
controller.onEnd(); controller.onEnd();
return; return;
} }
controller.getSettings().put(MODPACK_NAME, modpackName.get());
controller.getSettings().put(MODPACK_FILE_API, modpackFileApi.get()); exportInfo.setName(name.get());
controller.getSettings().put(MODPACK_VERSION, modpackVersion.get()); exportInfo.setFileApi(fileApi.get());
controller.getSettings().put(MODPACK_AUTHOR, modpackAuthor.get()); exportInfo.setVersion(version.get());
controller.getSettings().put(MODPACK_FILE, file); exportInfo.setAuthor(author.get());
controller.getSettings().put(MODPACK_DESCRIPTION, modpackDescription.get()); exportInfo.setOutput(file.toPath());
controller.getSettings().put(MODPACK_INCLUDE_LAUNCHER, includingLauncher.get()); exportInfo.setDescription(description.get());
exportInfo.setPackWithLauncher(packWithLauncher.get());
exportInfo.setUrl(url.get());
exportInfo.setForceUpdate(forceUpdate.get());
exportInfo.setPackWithLauncher(packWithLauncher.get());
exportInfo.setMinMemory(minMemory.get());
exportInfo.setLaunchArguments(launchArguments.get());
exportInfo.setJavaArguments(javaArguments.get());
exportInfo.setAuthlibInjectorServer(authlibInjectorServer.get());
if (StringUtils.isNotBlank(mcbbsThreadId.get())) {
exportInfo.setOrigins(Collections.singletonList(new McbbsModpackManifest.Origin(
"mcbbs", Integer.parseInt(mcbbsThreadId.get())
)));
}
controller.getSettings().put(MODPACK_INFO, exportInfo);
controller.onNext(); controller.onNext();
} }
@Override @Override
public void cleanup(Map<String, Object> settings) { public void cleanup(Map<String, Object> settings) {
controller.getSettings().remove(MODPACK_NAME); controller.getSettings().remove(MODPACK_INFO);
controller.getSettings().remove(MODPACK_VERSION);
controller.getSettings().remove(MODPACK_AUTHOR);
controller.getSettings().remove(MODPACK_DESCRIPTION);
controller.getSettings().remove(MODPACK_INCLUDE_LAUNCHER);
controller.getSettings().remove(MODPACK_FILE);
} }
@Override @Override
@@ -126,19 +161,11 @@ public final class ModpackInfoPage extends Control implements WizardPage {
return new ModpackInfoPageSkin(this); return new ModpackInfoPageSkin(this);
} }
public static final String MODPACK_NAME = "modpack.name"; public static final String MODPACK_INFO = "modpack.info";
public static final String MODPACK_FILE_API = "modpack.file_api"; public static final String MODPACK_INFO_OPTION = "modpack.info.option";
public static final String MODPACK_VERSION = "modpack.version";
public static final String MODPACK_AUTHOR = "archive.author";
public static final String MODPACK_DESCRIPTION = "modpack.description";
public static final String MODPACK_INCLUDE_LAUNCHER = "modpack.include_launcher";
public static final String MODPACK_FILE = "modpack.file";
public static class ModpackInfoPageSkin extends SkinBase<ModpackInfoPage> { public static class ModpackInfoPageSkin extends SkinBase<ModpackInfoPage> {
private final JFXTextField txtModpackName; private ObservableList<Node> originList;
private final JFXTextField txtModpackFileApi;
private final JFXTextField txtModpackAuthor;
private final JFXTextField txtModpackVersion;
public ModpackInfoPageSkin(ModpackInfoPage skinnable) { public ModpackInfoPageSkin(ModpackInfoPage skinnable) {
super(skinnable); super(skinnable);
@@ -151,6 +178,8 @@ public final class ModpackInfoPage extends Control implements WizardPage {
scroll.setFitToHeight(true); scroll.setFitToHeight(true);
getChildren().setAll(scroll); getChildren().setAll(scroll);
List<JFXTextField> validatingFields = new ArrayList<>();
{ {
BorderPane borderPane = new BorderPane(); BorderPane borderPane = new BorderPane();
borderPane.setStyle("-fx-padding: 16;"); borderPane.setStyle("-fx-padding: 16;");
@@ -179,14 +208,14 @@ public final class ModpackInfoPage extends Control implements WizardPage {
borderPane1.setLeft(new Label(i18n("modpack.wizard.step.initialization.exported_version"))); borderPane1.setLeft(new Label(i18n("modpack.wizard.step.initialization.exported_version")));
Label versionNameLabel = new Label(); Label versionNameLabel = new Label();
versionNameLabel.textProperty().bind(skinnable.versionName); versionNameLabel.setText(skinnable.versionName);
borderPane1.setRight(versionNameLabel); borderPane1.setRight(versionNameLabel);
list.getContent().add(borderPane1); list.getContent().add(borderPane1);
} }
{ {
txtModpackName = new JFXTextField(); JFXTextField txtModpackName = new JFXTextField();
txtModpackName.textProperty().bindBidirectional(skinnable.modpackName); txtModpackName.textProperty().bindBidirectional(skinnable.name);
txtModpackName.setLabelFloat(true); txtModpackName.setLabelFloat(true);
txtModpackName.setPromptText(i18n("modpack.name")); txtModpackName.setPromptText(i18n("modpack.name"));
RequiredFieldValidator validator = new RequiredFieldValidator(); RequiredFieldValidator validator = new RequiredFieldValidator();
@@ -194,16 +223,25 @@ public final class ModpackInfoPage extends Control implements WizardPage {
txtModpackName.getValidators().add(validator); txtModpackName.getValidators().add(validator);
StackPane.setMargin(txtModpackName, insets); StackPane.setMargin(txtModpackName, insets);
list.getContent().add(txtModpackName); list.getContent().add(txtModpackName);
validatingFields.add(txtModpackName);
} }
if (skinnable.showFileApi) { if (skinnable.options.isRequireFileApi()) {
txtModpackFileApi = new JFXTextField(); JFXTextField txtModpackFileApi = new JFXTextField();
txtModpackFileApi.textProperty().bindBidirectional(skinnable.modpackFileApi); txtModpackFileApi.textProperty().bindBidirectional(skinnable.fileApi);
txtModpackFileApi.setLabelFloat(true); txtModpackFileApi.setLabelFloat(true);
txtModpackFileApi.setPromptText(i18n("modpack.file_api")); txtModpackFileApi.setPromptText(i18n("modpack.file_api"));
if (skinnable.options.isValidateFileApi()) {
RequiredFieldValidator validator = new RequiredFieldValidator(); RequiredFieldValidator validator = new RequiredFieldValidator();
txtModpackFileApi.getValidators().add(validator); txtModpackFileApi.getValidators().add(validator);
}
txtModpackFileApi.getValidators().add(new Validator(s -> { txtModpackFileApi.getValidators().add(new Validator(s -> {
if (s.isEmpty()) {
return true;
}
try { try {
new URL(s).toURI(); new URL(s).toURI();
return true; return true;
@@ -213,35 +251,39 @@ public final class ModpackInfoPage extends Control implements WizardPage {
})); }));
StackPane.setMargin(txtModpackFileApi, insets); StackPane.setMargin(txtModpackFileApi, insets);
list.getContent().add(txtModpackFileApi); list.getContent().add(txtModpackFileApi);
} else {
txtModpackFileApi = null; validatingFields.add(txtModpackFileApi);
} }
{ {
txtModpackAuthor = new JFXTextField(); JFXTextField txtModpackAuthor = new JFXTextField();
txtModpackAuthor.textProperty().bindBidirectional(skinnable.modpackAuthor); txtModpackAuthor.textProperty().bindBidirectional(skinnable.author);
txtModpackAuthor.setLabelFloat(true); txtModpackAuthor.setLabelFloat(true);
txtModpackAuthor.setPromptText(i18n("archive.author")); txtModpackAuthor.setPromptText(i18n("archive.author"));
RequiredFieldValidator validator = new RequiredFieldValidator(); RequiredFieldValidator validator = new RequiredFieldValidator();
txtModpackAuthor.getValidators().add(validator); txtModpackAuthor.getValidators().add(validator);
StackPane.setMargin(txtModpackAuthor, insets); StackPane.setMargin(txtModpackAuthor, insets);
list.getContent().add(txtModpackAuthor); list.getContent().add(txtModpackAuthor);
validatingFields.add(txtModpackAuthor);
} }
{ {
txtModpackVersion = new JFXTextField(); JFXTextField txtModpackVersion = new JFXTextField();
txtModpackVersion.textProperty().bindBidirectional(skinnable.modpackVersion); txtModpackVersion.textProperty().bindBidirectional(skinnable.version);
txtModpackVersion.setLabelFloat(true); txtModpackVersion.setLabelFloat(true);
txtModpackVersion.setPromptText(i18n("archive.version")); txtModpackVersion.setPromptText(i18n("archive.version"));
RequiredFieldValidator validator = new RequiredFieldValidator(); RequiredFieldValidator validator = new RequiredFieldValidator();
txtModpackVersion.getValidators().add(validator); txtModpackVersion.getValidators().add(validator);
StackPane.setMargin(txtModpackVersion, insets); StackPane.setMargin(txtModpackVersion, insets);
list.getContent().add(txtModpackVersion); list.getContent().add(txtModpackVersion);
validatingFields.add(txtModpackVersion);
} }
{ {
JFXTextArea area = new JFXTextArea(); JFXTextArea area = new JFXTextArea();
area.textProperty().bindBidirectional(skinnable.modpackDescription); area.textProperty().bindBidirectional(skinnable.description);
area.setLabelFloat(true); area.setLabelFloat(true);
area.setPromptText(i18n("modpack.desc")); area.setPromptText(i18n("modpack.desc"));
area.setMinHeight(400); area.setMinHeight(400);
@@ -249,18 +291,110 @@ public final class ModpackInfoPage extends Control implements WizardPage {
list.getContent().add(area); list.getContent().add(area);
} }
if (skinnable.controller.getSettings().get(MODPACK_TYPE) == MODPACK_TYPE_HMCL) { if (skinnable.options.isRequireForceUpdate()) {
BorderPane borderPane1 = new BorderPane(); BorderPane pane = new BorderPane();
borderPane1.setLeft(new Label(i18n("modpack.wizard.step.initialization.include_launcher"))); pane.setLeft(new Label(i18n("modpack.wizard.step.initialization.force_update")));
list.getContent().add(borderPane1); list.getContent().add(pane);
JFXToggleButton button = new JFXToggleButton(); JFXToggleButton button = new JFXToggleButton();
button.setDisable(!skinnable.canIncludeLauncher); button.setDisable(!skinnable.canIncludeLauncher);
button.selectedProperty().bindBidirectional(skinnable.includingLauncher); button.selectedProperty().bindBidirectional(skinnable.packWithLauncher);
button.setSize(8); button.setSize(8);
button.setMinHeight(16); button.setMinHeight(16);
button.setMaxHeight(16); button.setMaxHeight(16);
borderPane1.setRight(button); pane.setRight(button);
}
if (skinnable.options.isRequireAuthlibInjectorServer()) {
JFXComboBox<AuthlibInjectorServer> cboServers = new JFXComboBox<>();
cboServers.setCellFactory(jfxListCellFactory(server -> new TwoLineListItem(server.getName(), server.getUrl())));
cboServers.setConverter(stringConverter(AuthlibInjectorServer::getName));
Bindings.bindContent(cboServers.getItems(), config().getAuthlibInjectorServers());
skinnable.authlibInjectorServer.bind(Bindings.createStringBinding(() ->
Optional.ofNullable(cboServers.getSelectionModel().getSelectedItem())
.map(AuthlibInjectorServer::getUrl)
.orElse(null)));
BorderPane pane = new BorderPane();
Label left = new Label(i18n("account.injector.server"));
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
pane.setLeft(left);
pane.setRight(cboServers);
list.getContent().add(pane);
}
if (skinnable.options.isRequireMinMemory()) {
JFXTextField txtMinMemory = new JFXTextField();
FXUtils.bindInt(txtMinMemory, skinnable.minMemory);
txtMinMemory.getValidators().add(new NumberValidator(i18n("input.number"), false));
FXUtils.setLimitWidth(txtMinMemory, 300);
validatingFields.add(txtMinMemory);
BorderPane pane = new BorderPane();
Label label = new Label(i18n("settings.min_memory"));
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
pane.setLeft(label);
BorderPane.setAlignment(txtMinMemory, Pos.CENTER_RIGHT);
pane.setRight(txtMinMemory);
list.getContent().add(pane);
}
if (skinnable.options.isRequireLaunchArguments()) {
JFXTextField txtLaunchArguments = new JFXTextField();
txtLaunchArguments.textProperty().bindBidirectional(skinnable.launchArguments);
txtLaunchArguments.setLabelFloat(true);
txtLaunchArguments.setPromptText(i18n("settings.advanced.minecraft_arguments"));
StackPane.setMargin(txtLaunchArguments, insets);
list.getContent().add(txtLaunchArguments);
}
if (skinnable.options.isRequireJavaArguments()) {
JFXTextField txtJavaArguments = new JFXTextField();
txtJavaArguments.textProperty().bindBidirectional(skinnable.javaArguments);
txtJavaArguments.setLabelFloat(true);
txtJavaArguments.setPromptText(i18n("settings.advanced.jvm_args"));
StackPane.setMargin(txtJavaArguments, insets);
list.getContent().add(txtJavaArguments);
}
if (skinnable.options.isRequireOrigins() || skinnable.options.isRequireUrl()) {
BorderPane originPane = new BorderPane();
Label title = new Label(i18n("modpack.origin"));
originPane.setTop(title);
VBox container = new VBox();
BorderPane.setMargin(container, new Insets(0, 0, 0, 16));
originPane.setCenter(container);
list.getContent().add(originPane);
if (skinnable.options.isRequireUrl()) {
BorderPane pane = new BorderPane();
pane.setPadding(new Insets(8, 0, 8, 0));
Label left = new Label(i18n("modpack.origin.url"));
pane.setLeft(left);
JFXTextField txtModpackUrl = new JFXTextField();
txtModpackUrl.textProperty().bindBidirectional(skinnable.url);
pane.setRight(txtModpackUrl);
container.getChildren().add(pane);
}
if (skinnable.options.isRequireOrigins()) {
BorderPane pane = new BorderPane();
pane.setPadding(new Insets(8, 0, 8, 0));
Label left = new Label(i18n("modpack.origin.mcbbs"));
pane.setLeft(left);
JFXTextField txtMcbbs = new JFXTextField();
FXUtils.setValidateWhileTextChanged(txtMcbbs, true);
txtMcbbs.getValidators().add(new NumberValidator(i18n("input.number"), true));
txtMcbbs.textProperty().bindBidirectional(skinnable.mcbbsThreadId);
pane.setRight(txtMcbbs);
container.getChildren().add(pane);
validatingFields.add(txtMcbbs);
}
} }
} }
@@ -276,15 +410,13 @@ public final class ModpackInfoPage extends Control implements WizardPage {
nextButton.setButtonType(JFXButton.ButtonType.RAISED); nextButton.setButtonType(JFXButton.ButtonType.RAISED);
nextButton.setText(i18n("wizard.next")); nextButton.setText(i18n("wizard.next"));
nextButton.getStyleClass().add("jfx-button-raised"); nextButton.getStyleClass().add("jfx-button-raised");
if (skinnable.showFileApi) { nextButton.disableProperty().bind(
nextButton.disableProperty().bind(Bindings.createBooleanBinding(() -> // Disable nextButton if any text of JFXTextFields in validatingFields does not fulfill
!txtModpackName.validate() || !txtModpackVersion.validate() || !txtModpackAuthor.validate() || !txtModpackFileApi.validate(), // our requirement.
txtModpackName.textProperty(), txtModpackAuthor.textProperty(), txtModpackVersion.textProperty(), txtModpackFileApi.textProperty())); Bindings.createBooleanBinding(() -> validatingFields.stream()
} else { .map(field -> !field.validate())
nextButton.disableProperty().bind(Bindings.createBooleanBinding(() -> .reduce(true, (left, right) -> left || right),
!txtModpackName.validate() || !txtModpackVersion.validate() || !txtModpackAuthor.validate(), validatingFields.stream().map(JFXTextField::textProperty).toArray(StringProperty[]::new)));
txtModpackName.textProperty(), txtModpackAuthor.textProperty(), txtModpackVersion.textProperty()));
}
hbox.getChildren().add(nextButton); hbox.getChildren().add(nextButton);
} }
} }

View File

@@ -20,18 +20,23 @@ package org.jackhuang.hmcl.ui.export;
import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXButton;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.mod.ModpackExportInfo;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackExportTask;
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackExportTask;
import org.jackhuang.hmcl.mod.server.ServerModpackExportTask;
import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.wizard.WizardController; import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage; import org.jackhuang.hmcl.ui.wizard.WizardPage;
import java.util.Map; import java.util.Map;
import static org.jackhuang.hmcl.ui.export.ModpackInfoPage.MODPACK_INFO_OPTION;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class ModpackTypeSelectionPage extends StackPane implements WizardPage { public final class ModpackTypeSelectionPage extends StackPane implements WizardPage {
private final WizardController controller; private final WizardController controller;
@FXML @FXML
private JFXButton btnHMCL; private JFXButton btnMCBBS;
@FXML @FXML
private JFXButton btnMultiMC; private JFXButton btnMultiMC;
@FXML @FXML
@@ -41,12 +46,15 @@ public final class ModpackTypeSelectionPage extends StackPane implements WizardP
this.controller = controller; this.controller = controller;
FXUtils.loadFXML(this, "/assets/fxml/modpack/type.fxml"); FXUtils.loadFXML(this, "/assets/fxml/modpack/type.fxml");
JFXButton[] buttons = new JFXButton[]{btnHMCL, btnMultiMC, btnServer}; JFXButton[] buttons = new JFXButton[]{btnMCBBS, btnMultiMC, btnServer};
String[] types = new String[]{MODPACK_TYPE_HMCL, MODPACK_TYPE_MULTIMC, MODPACK_TYPE_SERVER}; String[] types = new String[]{MODPACK_TYPE_MCBBS, MODPACK_TYPE_MULTIMC, MODPACK_TYPE_SERVER};
ModpackExportInfo.Options[] options = new ModpackExportInfo.Options[]{McbbsModpackExportTask.OPTION, MultiMCModpackExportTask.OPTION, ServerModpackExportTask.OPTION};
for (int i = 0; i < types.length; ++i) { for (int i = 0; i < types.length; ++i) {
String type = types[i]; String type = types[i];
ModpackExportInfo.Options option = options[i];
buttons[i].setOnMouseClicked(e -> { buttons[i].setOnMouseClicked(e -> {
controller.getSettings().put(MODPACK_TYPE, type); controller.getSettings().put(MODPACK_TYPE, type);
controller.getSettings().put(MODPACK_INFO_OPTION, option);
controller.onNext(); controller.onNext();
}); });
} }
@@ -63,7 +71,7 @@ public final class ModpackTypeSelectionPage extends StackPane implements WizardP
public static final String MODPACK_TYPE = "modpack.type"; public static final String MODPACK_TYPE = "modpack.type";
public static final String MODPACK_TYPE_HMCL = "hmcl"; public static final String MODPACK_TYPE_MCBBS = "mcbbs";
public static final String MODPACK_TYPE_MULTIMC = "multimc"; public static final String MODPACK_TYPE_MULTIMC = "multimc";
public static final String MODPACK_TYPE_SERVER = "server"; public static final String MODPACK_TYPE_SERVER = "server";
} }

View File

@@ -13,11 +13,11 @@
</fx:define> </fx:define>
<VBox fx:id="list" styleClass="jfx-list-view" maxHeight="150" maxWidth="300"> <VBox fx:id="list" styleClass="jfx-list-view" maxHeight="150" maxWidth="300">
<Label padding="${insets}" text="%modpack.export.as" /> <Label padding="${insets}" text="%modpack.export.as" />
<JFXButton fx:id="btnHMCL" prefWidth="${list.width}"> <JFXButton fx:id="btnMCBBS" prefWidth="${list.width}">
<graphic> <graphic>
<BorderPane mouseTransparent="true"> <BorderPane mouseTransparent="true">
<left> <left>
<TwoLineListItem title="%modpack.type.hmcl" subtitle="%modpack.type.hmcl.export" /> <TwoLineListItem title="%modpack.type.mcbbs" subtitle="%modpack.type.mcbbs.export" />
</left> </left>
<right> <right>
<fx:include BorderPane.alignment="CENTER" source="/assets/svg/arrow-right.fxml"/> <fx:include BorderPane.alignment="CENTER" source="/assets/svg/arrow-right.fxml"/>

View File

@@ -264,6 +264,10 @@ modpack.invalid=Invalid modpack file.
modpack.mismatched_type=Inappropriate modpack type, your current game is a %s modpack, but your update file is a %s modpack. modpack.mismatched_type=Inappropriate modpack type, your current game is a %s modpack, but your update file is a %s modpack.
modpack.name=Modpack Name modpack.name=Modpack Name
modpack.not_a_valid_name=Invalid modpack name modpack.not_a_valid_name=Invalid modpack name
modpack.origin=Origin
modpack.origin.url=Official Website
modpack.origin.mcbbs=MCBBS
modpack.origin.mcbbs.prompt=Thread id
modpack.scan=Scanning this modpack modpack.scan=Scanning this modpack
modpack.task.install=Import Modpack modpack.task.install=Import Modpack
modpack.task.install.error=This modpack file cannot be recognized. Only Curse and MultiMC modpacks are supported. modpack.task.install.error=This modpack file cannot be recognized. Only Curse and MultiMC modpacks are supported.
@@ -273,8 +277,8 @@ modpack.type.curse.completion=Install files related to Curse modpack
modpack.type.curse.tolerable_error=We cannot complete the download of all files of this Curse modpack. You can retry the download when starting corresponding game version. You may retry for a couple of times due to network problems. modpack.type.curse.tolerable_error=We cannot complete the download of all files of this Curse modpack. You can retry the download when starting corresponding game version. You may retry for a couple of times due to network problems.
modpack.type.curse.error=Unable to install this Curse modpack. Please retry. modpack.type.curse.error=Unable to install this Curse modpack. Please retry.
modpack.type.curse.not_found=Some of required resources are missing and thus could not be downloaded. Please consider the latest version or other modpacks. modpack.type.curse.not_found=Some of required resources are missing and thus could not be downloaded. Please consider the latest version or other modpacks.
modpack.type.hmcl=Hello Minecraft! Launcher modpack.type.mcbbs=MCBBS Standard
modpack.type.hmcl.export=Can be imported by Hello Minecraft! Launcher modpack.type.mcbbs.export=Can be imported by Hello Minecraft! Launcher
modpack.type.multimc=MultiMC modpack.type.multimc=MultiMC
modpack.type.multimc.export=Can be imported by Hello Minecraft! Launcher and MultiMC modpack.type.multimc.export=Can be imported by Hello Minecraft! Launcher and MultiMC
modpack.type.server=Server Auto-Update Modpack modpack.type.server=Server Auto-Update Modpack
@@ -290,6 +294,7 @@ modpack.wizard.step.2.title=Add files to the modpack.
modpack.wizard.step.3=Modpack Type modpack.wizard.step.3=Modpack Type
modpack.wizard.step.3.title=Choose the format of the modpack. modpack.wizard.step.3.title=Choose the format of the modpack.
modpack.wizard.step.initialization.exported_version=Exported game version modpack.wizard.step.initialization.exported_version=Exported game version
modpack.wizard.step.initialization.force_update=Force updating modpack if possible
modpack.wizard.step.initialization.include_launcher=Include the launcher modpack.wizard.step.initialization.include_launcher=Include the launcher
modpack.wizard.step.initialization.save=Export to... modpack.wizard.step.initialization.save=Export to...
modpack.wizard.step.initialization.warning=Before creating a modpack, you should ensure that the game can launch successfully,\nand that your Minecraft is a release version.\nDo NOT add mods which cannot be redistributed. modpack.wizard.step.initialization.warning=Before creating a modpack, you should ensure that the game can launch successfully,\nand that your Minecraft is a release version.\nDo NOT add mods which cannot be redistributed.
@@ -405,6 +410,7 @@ settings.launcher.proxy.socks=Socks
settings.launcher.proxy.username=Account settings.launcher.proxy.username=Account
settings.launcher.theme=Theme settings.launcher.theme=Theme
settings.min_memory=Min Memory/MB
settings.max_memory=Max Memory/MB settings.max_memory=Max Memory/MB
settings.physical_memory=Physical Memory Size settings.physical_memory=Physical Memory Size
settings.show_log=Show Logs settings.show_log=Show Logs

View File

@@ -264,6 +264,10 @@ modpack.invalid=无效的整合包升级文件,可能是下载时出现问题
modpack.mismatched_type=整合包类型不匹配,当前游戏是 %s 整合包,但是提供的整合包更新文件是 %s 整合包。 modpack.mismatched_type=整合包类型不匹配,当前游戏是 %s 整合包,但是提供的整合包更新文件是 %s 整合包。
modpack.name=整合包名称 modpack.name=整合包名称
modpack.not_a_valid_name=不是一个有效的整合包名称 modpack.not_a_valid_name=不是一个有效的整合包名称
modpack.origin=来源
modpack.origin.url=官方网站
modpack.origin.mcbbs=MCBBS
modpack.origin.mcbbs.prompt=贴子 id
modpack.scan=解析整合包 modpack.scan=解析整合包
modpack.task.install=导入整合包 modpack.task.install=导入整合包
modpack.task.install.error=无法识别该整合包,目前仅支持导入 Curse、MultiMC、HMCL 整合包。 modpack.task.install.error=无法识别该整合包,目前仅支持导入 Curse、MultiMC、HMCL 整合包。
@@ -273,7 +277,7 @@ modpack.type.curse.completion=下载 Curse 整合包相关文件
modpack.type.curse.tolerable_error=但未能完成 Curse 整合包文件的下载,您可以在启动该游戏版本时继续 Curse 整合包文件的下载。由于网络问题,您可能需要重试多次。 modpack.type.curse.tolerable_error=但未能完成 Curse 整合包文件的下载,您可以在启动该游戏版本时继续 Curse 整合包文件的下载。由于网络问题,您可能需要重试多次。
modpack.type.curse.error=未能完成 Curse 整合包的下载,请多次重试或设置代理 modpack.type.curse.error=未能完成 Curse 整合包的下载,请多次重试或设置代理
modpack.type.curse.not_found=部分必需文件已经在网络中被删除并且再也无法下载,请尝试该整合包的最新版本或者安装其他整合包。 modpack.type.curse.not_found=部分必需文件已经在网络中被删除并且再也无法下载,请尝试该整合包的最新版本或者安装其他整合包。
modpack.type.hmcl=HMCL modpack.type.mcbbs=我的世界中文论坛整合包标准
modpack.type.hmcl.export=可以被 Hello Minecraft! Launcher (HMCL) 导入 modpack.type.hmcl.export=可以被 Hello Minecraft! Launcher (HMCL) 导入
modpack.type.multimc=MultiMC modpack.type.multimc=MultiMC
modpack.type.multimc.export=可以被 Hello Minecraft! Launcher (HMCL) 和 MultiMC 导入 modpack.type.multimc.export=可以被 Hello Minecraft! Launcher (HMCL) 和 MultiMC 导入
@@ -290,6 +294,7 @@ modpack.wizard.step.2.title=选中你想加到整合包中的文件或文件夹
modpack.wizard.step.3=整合包类型 modpack.wizard.step.3=整合包类型
modpack.wizard.step.3.title=选择整合包导出类型 modpack.wizard.step.3.title=选择整合包导出类型
modpack.wizard.step.initialization.exported_version=要导出的游戏版本 modpack.wizard.step.initialization.exported_version=要导出的游戏版本
modpack.wizard.step.initialization.force_update=强制升级整合包至最新版本(需要自建服务器)
modpack.wizard.step.initialization.include_launcher=包含启动器 modpack.wizard.step.initialization.include_launcher=包含启动器
modpack.wizard.step.initialization.save=选择要导出到的游戏整合包位置 modpack.wizard.step.initialization.save=选择要导出到的游戏整合包位置
modpack.wizard.step.initialization.warning=在制作整合包前,请您确认您选择的版本可以正常启动,\n并保证您的 Minecraft 是正式版而非快照版,\n而且不应当将不允许非官方途径传播的 Mod、材质包等纳入整合包。\n整合包会保存您目前的下载源设置 modpack.wizard.step.initialization.warning=在制作整合包前,请您确认您选择的版本可以正常启动,\n并保证您的 Minecraft 是正式版而非快照版,\n而且不应当将不允许非官方途径传播的 Mod、材质包等纳入整合包。\n整合包会保存您目前的下载源设置
@@ -405,6 +410,7 @@ settings.launcher.proxy.socks=Socks
settings.launcher.proxy.username=账户 settings.launcher.proxy.username=账户
settings.launcher.theme=主题 settings.launcher.theme=主题
settings.min_memory=最小内存MB
settings.max_memory=最大内存MB settings.max_memory=最大内存MB
settings.physical_memory=物理内存大小 settings.physical_memory=物理内存大小
settings.show_log=查看日志 settings.show_log=查看日志

View File

@@ -18,10 +18,14 @@
package org.jackhuang.hmcl.game; package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.util.platform.JavaVersion; import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jetbrains.annotations.NotNull;
import java.io.File; import java.io.File;
import java.io.Serializable; import java.io.Serializable;
import java.net.Proxy; import java.net.Proxy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/** /**
* *
@@ -34,8 +38,8 @@ public class LaunchOptions implements Serializable {
private String versionName; private String versionName;
private String versionType; private String versionType;
private String profileName; private String profileName;
private String minecraftArgs; private List<String> gameArguments = new ArrayList<>();
private String javaArgs; private List<String> javaArguments = new ArrayList<>();
private Integer minMemory; private Integer minMemory;
private Integer maxMemory; private Integer maxMemory;
private Integer metaspace; private Integer metaspace;
@@ -90,15 +94,17 @@ public class LaunchOptions implements Serializable {
/** /**
* User custom additional minecraft command line arguments. * User custom additional minecraft command line arguments.
*/ */
public String getMinecraftArgs() { @NotNull
return minecraftArgs; public List<String> getGameArguments() {
return Collections.unmodifiableList(gameArguments);
} }
/** /**
* User custom additional java virtual machine command line arguments. * User custom additional java virtual machine command line arguments.
*/ */
public String getJavaArgs() { @NotNull
return javaArgs; public List<String> getJavaArguments() {
return Collections.unmodifiableList(javaArguments);
} }
/** /**
@@ -202,6 +208,150 @@ public class LaunchOptions implements Serializable {
return options; return options;
} }
/**
* The game directory
*/
public File getGameDir() {
return options.gameDir;
}
/**
* The Java Environment that Minecraft runs on.
*/
public JavaVersion getJava() {
return options.java;
}
/**
* Will shown in the left bottom corner of the main menu of Minecraft.
* null if use the id of launch version.
*/
public String getVersionName() {
return options.versionName;
}
/**
* Will shown in the left bottom corner of the main menu of Minecraft.
* null if use Version.versionType.
*/
public String getVersionType() {
return options.versionType;
}
/**
* Don't know what the hell this is.
*/
public String getProfileName() {
return options.profileName;
}
/**
* User custom additional minecraft command line arguments.
*/
public List<String> getGameArguments() {
return options.gameArguments;
}
/**
* User custom additional java virtual machine command line arguments.
*/
public List<String> getJavaArguments() {
return options.javaArguments;
}
/**
* The minimum memory that the JVM can allocate.
*/
public Integer getMinMemory() {
return options.minMemory;
}
/**
* The maximum memory that the JVM can allocate.
*/
public Integer getMaxMemory() {
return options.maxMemory;
}
/**
* The maximum metaspace memory that the JVM can allocate.
* For Java 7 -XX:PermSize and Java 8 -XX:MetaspaceSize
* Containing class instances.
*/
public Integer getMetaspace() {
return options.metaspace;
}
/**
* The initial game window width
*/
public Integer getWidth() {
return options.width;
}
/**
* The initial game window height
*/
public Integer getHeight() {
return options.height;
}
/**
* Is inital game window fullscreen.
*/
public boolean isFullscreen() {
return options.fullscreen;
}
/**
* The server ip that will connect to when enter game main menu.
*/
public String getServerIp() {
return options.serverIp;
}
/**
* i.e. optirun
*/
public String getWrapper() {
return options.wrapper;
}
/**
* Proxy settings
*/
public Proxy getProxy() {
return options.proxy;
}
/**
* The user name of the proxy, optional.
*/
public String getProxyUser() {
return options.proxyUser;
}
/**
* The password of the proxy, optional
*/
public String getProxyPass() {
return options.proxyPass;
}
/**
* Prevent game launcher from generating default JVM arguments like max memory.
*/
public boolean isNoGeneratedJVMArgs() {
return options.noGeneratedJVMArgs;
}
/**
* Called command line before launching the game.
*/
public String getPreLaunchCommand() {
return options.preLaunchCommand;
}
public Builder setGameDir(File gameDir) { public Builder setGameDir(File gameDir) {
options.gameDir = gameDir; options.gameDir = gameDir;
return this; return this;
@@ -227,13 +377,15 @@ public class LaunchOptions implements Serializable {
return this; return this;
} }
public Builder setMinecraftArgs(String minecraftArgs) { public Builder setGameArguments(List<String> gameArguments) {
options.minecraftArgs = minecraftArgs; options.gameArguments.clear();
options.gameArguments.addAll(gameArguments);
return this; return this;
} }
public Builder setJavaArgs(String javaArgs) { public Builder setJavaArguments(List<String> javaArguments) {
options.javaArgs = javaArgs; options.javaArguments.clear();
options.javaArguments.addAll(javaArguments);
return this; return this;
} }

View File

@@ -82,8 +82,7 @@ public class DefaultLauncher extends Launcher {
res.add(options.getJava().getBinary().toString()); res.add(options.getJava().getBinary().toString());
if (StringUtils.isNotBlank(options.getJavaArgs())) res.addAllWithoutParsing(options.getJavaArguments());
res.addAllWithoutParsing(StringUtils.tokenize(options.getJavaArgs()));
// JVM Args // JVM Args
if (!options.isNoGeneratedJVMArgs()) { if (!options.isNoGeneratedJVMArgs()) {
@@ -218,8 +217,7 @@ public class DefaultLauncher extends Launcher {
} }
} }
if (StringUtils.isNotBlank(options.getMinecraftArgs())) res.addAllWithoutParsing(options.getGameArguments());
res.addAllWithoutParsing(StringUtils.tokenize(options.getMinecraftArgs()));
res.removeIf(it -> getForbiddens().containsKey(it) && getForbiddens().get(it).get()); res.removeIf(it -> getForbiddens().containsKey(it) && getForbiddens().get(it).get());
return res; return res;

View File

@@ -0,0 +1,298 @@
package org.jackhuang.hmcl.mod;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackManifest;
import org.jetbrains.annotations.Nullable;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class ModpackExportInfo {
private final List<String> whitelist = new ArrayList<>();
private String name;
private String author;
private String version;
private String description;
private String url;
private boolean forceUpdate;
private boolean packWithLauncher;
private String fileApi;
private int minMemory;
private List<Integer> supportedJavaVersions;
private String launchArguments;
private String javaArguments;
private String authlibInjectorServer;
private Path output;
private List<McbbsModpackManifest.Origin> origins = new ArrayList<>();
public ModpackExportInfo() {}
public List<String> getWhitelist() {
return whitelist;
}
public ModpackExportInfo setWhitelist(List<String> whitelist) {
this.whitelist.clear();
this.whitelist.addAll(whitelist);
return this;
}
/**
* Name of this modpack.
*/
public String getName() {
return name;
}
public ModpackExportInfo setName(String name) {
this.name = name;
return this;
}
/**
* Author of this modpack.
*/
public String getAuthor() {
return author;
}
public ModpackExportInfo setAuthor(String author) {
this.author = author;
return this;
}
/**
* Version of this modpack.
*/
public String getVersion() {
return version;
}
public ModpackExportInfo setVersion(String version) {
this.version = version;
return this;
}
/**
* Description of this modpack.
*
* Supports plain HTML text.
*/
public String getDescription() {
return description;
}
public ModpackExportInfo setDescription(String description) {
this.description = description;
return this;
}
public String getFileApi() {
return fileApi;
}
public ModpackExportInfo setFileApi(String fileApi) {
this.fileApi = fileApi;
return this;
}
/**
* Modpack official introduction webpage link.
*/
public String getUrl() {
return url;
}
public ModpackExportInfo setUrl(String url) {
this.url = url;
return this;
}
public boolean isForceUpdate() {
return forceUpdate;
}
public ModpackExportInfo setForceUpdate(boolean forceUpdate) {
this.forceUpdate = forceUpdate;
return this;
}
public boolean isPackWithLauncher() {
return packWithLauncher;
}
public ModpackExportInfo setPackWithLauncher(boolean packWithLauncher) {
this.packWithLauncher = packWithLauncher;
return this;
}
public int getMinMemory() {
return minMemory;
}
public ModpackExportInfo setMinMemory(int minMemory) {
this.minMemory = minMemory;
return this;
}
@Nullable
public List<Integer> getSupportedJavaVersions() {
return supportedJavaVersions;
}
public ModpackExportInfo setSupportedJavaVersions(List<Integer> supportedJavaVersions) {
this.supportedJavaVersions = supportedJavaVersions;
return this;
}
public String getLaunchArguments() {
return launchArguments;
}
public ModpackExportInfo setLaunchArguments(String launchArguments) {
this.launchArguments = launchArguments;
return this;
}
public String getJavaArguments() {
return javaArguments;
}
public ModpackExportInfo setJavaArguments(String javaArguments) {
this.javaArguments = javaArguments;
return this;
}
public String getAuthlibInjectorServer() {
return authlibInjectorServer;
}
public ModpackExportInfo setAuthlibInjectorServer(String authlibInjectorServer) {
this.authlibInjectorServer = authlibInjectorServer;
return this;
}
public Path getOutput() {
return output;
}
public ModpackExportInfo setOutput(Path output) {
this.output = output;
return this;
}
public List<McbbsModpackManifest.Origin> getOrigins() {
return Collections.unmodifiableList(origins);
}
public ModpackExportInfo setOrigins(List<McbbsModpackManifest.Origin> origins) {
this.origins.clear();
this.origins.addAll(origins);
return this;
}
public ModpackExportInfo validate() throws NullPointerException {
if (output == null)
throw new NullPointerException("ModpackExportInfo.output cannot be null");
return this;
}
public static class Options {
private boolean requireUrl;
private boolean requireForceUpdate;
private boolean requireFileApi;
private boolean validateFileApi;
private boolean requireMinMemory;
private boolean requireAuthlibInjectorServer;
private boolean requireLaunchArguments;
private boolean requireJavaArguments;
private boolean requireOrigins;
public Options() {
}
public boolean isRequireUrl() {
return requireUrl;
}
public boolean isRequireForceUpdate() {
return requireForceUpdate;
}
public boolean isRequireFileApi() {
return requireFileApi;
}
public boolean isValidateFileApi() {
return validateFileApi;
}
public boolean isRequireMinMemory() {
return requireMinMemory;
}
public boolean isRequireAuthlibInjectorServer() {
return requireAuthlibInjectorServer;
}
public boolean isRequireLaunchArguments() {
return requireLaunchArguments;
}
public boolean isRequireJavaArguments() {
return requireJavaArguments;
}
public boolean isRequireOrigins() {
return requireOrigins;
}
public Options requireUrl() {
requireUrl = true;
return this;
}
public Options requireForceUpdate() {
requireForceUpdate = true;
return this;
}
public Options requireFileApi(boolean optional) {
requireFileApi = true;
validateFileApi = !optional;
return this;
}
public Options requireMinMemory() {
requireMinMemory = true;
return this;
}
public Options requireAuthlibInjectorServer() {
requireAuthlibInjectorServer = true;
return this;
}
public Options requireLaunchArguments() {
requireLaunchArguments = true;
return this;
}
public Options requireJavaArguments() {
requireJavaArguments = true;
return this;
}
public Options requireOrigins() {
requireOrigins = true;
return this;
}
}
}

View File

@@ -0,0 +1,201 @@
/*
* 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.mod.mcbbs;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.game.DefaultGameRepository;
import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;
public class McbbsModpackCompletionTask extends Task<Void> {
private final DefaultGameRepository repository;
private final String version;
private ModpackConfiguration<McbbsModpackManifest> manifest;
private GetTask dependent;
private McbbsModpackManifest remoteManifest;
private final List<Task<?>> dependencies = new LinkedList<>();
public McbbsModpackCompletionTask(DefaultDependencyManager dependencyManager, String version) {
this(dependencyManager, version, null);
}
public McbbsModpackCompletionTask(DefaultDependencyManager dependencyManager, String version, ModpackConfiguration<McbbsModpackManifest> manifest) {
this.repository = dependencyManager.getGameRepository();
this.version = version;
if (manifest == null) {
try {
File manifestFile = repository.getModpackConfiguration(version);
if (manifestFile.exists()) {
this.manifest = JsonUtils.GSON.fromJson(FileUtils.readText(manifestFile), new TypeToken<ModpackConfiguration<McbbsModpackManifest>>() {
}.getType());
}
} catch (Exception e) {
Logging.LOG.log(Level.WARNING, "Unable to read mcbbs modpack manifest.json", e);
}
} else {
this.manifest = manifest;
}
}
@Override
public boolean doPreExecute() {
return true;
}
@Override
public void preExecute() throws Exception {
if (manifest == null || StringUtils.isBlank(manifest.getManifest().getFileApi())) return;
dependent = new GetTask(new URL(manifest.getManifest().getFileApi() + "/manifest.json"));
}
@Override
public Collection<Task<?>> getDependencies() {
return dependencies;
}
@Override
public Collection<Task<?>> getDependents() {
return dependent == null ? Collections.emptySet() : Collections.singleton(dependent);
}
@Override
public void execute() throws Exception {
if (manifest == null || StringUtils.isBlank(manifest.getManifest().getFileApi())) return;
try {
remoteManifest = JsonUtils.fromNonNullJson(dependent.getResult(), McbbsModpackManifest.class);
} catch (JsonParseException e) {
throw new IOException(e);
}
Path rootPath = repository.getVersionRoot(version).toPath();
// Because in China, Curse is too difficult to visit,
// if failed, ignore it and retry next time.
// CurseManifest newManifest = manifest.setFiles(
// manifest.getFiles().parallelStream()
// .map(file -> {
// updateProgress(finished.incrementAndGet(), manifest.getFiles().size());
// if (StringUtils.isBlank(file.getFileName())) {
// try {
// return file.withFileName(NetworkUtils.detectFileName(file.getUrl()));
// } catch (IOException e) {
// try {
// String result = NetworkUtils.doGet(NetworkUtils.toURL(String.format("https://cursemeta.dries007.net/%d/%d.json", file.getProjectID(), file.getFileID())));
// CurseMetaMod mod = JsonUtils.fromNonNullJson(result, CurseMetaMod.class);
// return file.withFileName(mod.getFileNameOnDisk()).withURL(mod.getDownloadURL());
// } catch (FileNotFoundException fof) {
// Logging.LOG.log(Level.WARNING, "Could not query cursemeta for deleted mods: " + file.getUrl(), fof);
// notFound.set(true);
// return file;
// } catch (IOException | JsonParseException e2) {
// try {
// String result = NetworkUtils.doGet(NetworkUtils.toURL(String.format("https://addons-ecs.forgesvc.net/api/v2/addon/%d/file/%d", file.getProjectID(), file.getFileID())));
// CurseMetaMod mod = JsonUtils.fromNonNullJson(result, CurseMetaMod.class);
// return file.withFileName(mod.getFileName()).withURL(mod.getDownloadURL());
// } catch (FileNotFoundException fof) {
// Logging.LOG.log(Level.WARNING, "Could not query forgesvc for deleted mods: " + file.getUrl(), fof);
// notFound.set(true);
// return file;
// } catch (IOException | JsonParseException e3) {
// Logging.LOG.log(Level.WARNING, "Unable to fetch the file name of URL: " + file.getUrl(), e);
// Logging.LOG.log(Level.WARNING, "Unable to fetch the file name of URL: " + file.getUrl(), e2);
// Logging.LOG.log(Level.WARNING, "Unable to fetch the file name of URL: " + file.getUrl(), e3);
// allNameKnown.set(false);
// return file;
// }
// }
// }
// } else {
// return file;
// }
// })
// .collect(Collectors.toList()));
//
// Map<String, ModpackConfiguration.FileInformation> files = manifest.getManifest().getFiles().stream()
// .collect(Collectors.toMap(ModpackConfiguration.FileInformation::getPath,
// Function.identity()));
//
// Set<String> remoteFiles = remoteManifest.getFiles().stream().map(ModpackConfiguration.FileInformation::getPath)
// .collect(Collectors.toSet());
//
// // for files in new modpack
// for (ModpackConfiguration.FileInformation file : remoteManifest.getFiles()) {
// Path actualPath = rootPath.resolve(file.getPath());
// boolean download;
// if (!files.containsKey(file.getPath())) {
// // If old modpack does not have this entry, download it
// download = true;
// } else if (!Files.exists(actualPath)) {
// // If both old and new modpacks have this entry, but the file is missing...
// // Re-download it since network problem may cause file missing
// download = true;
// } else {
// // If user modified this entry file, we will not replace this file since this modified file is that user expects.
// String fileHash = encodeHex(digest("SHA-1", actualPath));
// String oldHash = files.get(file.getPath()).getHash();
// download = !Objects.equals(oldHash, file.getHash()) && Objects.equals(oldHash, fileHash);
// }
//
// if (download) {
// dependencies.add(new FileDownloadTask(
// new URL(remoteManifest.getFileApi() + "/overrides/" + NetworkUtils.encodeLocation(file.getPath())),
// actualPath.toFile(),
// new FileDownloadTask.IntegrityCheck("SHA-1", file.getHash())));
// }
// }
//
// // If old modpack have this entry, and new modpack deleted it. Delete this file.
// for (ModpackConfiguration.FileInformation file : manifest.getManifest().getFiles()) {
// Path actualPath = rootPath.resolve(file.getPath());
// if (Files.exists(actualPath) && !remoteFiles.contains(file.getPath()))
// Files.deleteIfExists(actualPath);
// }
}
@Override
public boolean doPostExecute() {
return true;
}
@Override
public void postExecute() throws Exception {
// if (manifest == null || StringUtils.isBlank(manifest.getManifest().getFileApi())) return;
// File manifestFile = repository.getModpackConfiguration(version);
// FileUtils.writeText(manifestFile, JsonUtils.GSON.toJson(new ModpackConfiguration<>(remoteManifest, this.manifest.getType(), this.manifest.getName(), this.manifest.getVersion(), remoteManifest.getFiles())));
}
}

View File

@@ -0,0 +1,120 @@
/*
* 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.mod.mcbbs;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.game.DefaultGameRepository;
import org.jackhuang.hmcl.game.GameVersion;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.mod.ModAdviser;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.ModpackExportInfo;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.Zipper;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.*;
import static org.jackhuang.hmcl.util.DigestUtils.digest;
import static org.jackhuang.hmcl.util.Hex.encodeHex;
public class McbbsModpackExportTask extends Task<Void> {
private final DefaultGameRepository repository;
private final String version;
private final ModpackExportInfo info;
public McbbsModpackExportTask(DefaultGameRepository repository, String version, ModpackExportInfo info) {
this.repository = repository;
this.version = version;
this.info = info.validate();
onDone().register(event -> {
if (event.isFailed()) this.info.getOutput().toFile().delete();
});
}
@Override
public void execute() throws Exception {
ArrayList<String> blackList = new ArrayList<>(ModAdviser.MODPACK_BLACK_LIST);
blackList.add(version + ".jar");
blackList.add(version + ".json");
Logging.LOG.info("Compressing game files without some files in blacklist, including files or directories: usernamecache.json, asm, logs, backups, versions, assets, usercache.json, libraries, crash-reports, launcher_profiles.json, NVIDIA, TCNodeTracker");
try (Zipper zip = new Zipper(info.getOutput())) {
Path runDirectory = repository.getRunDirectory(version).toPath();
List<McbbsModpackManifest.File> files = new ArrayList<>();
zip.putDirectory(runDirectory, "overrides", path -> {
if (Modpack.acceptFile(path, blackList, info.getWhitelist())) {
Path file = runDirectory.resolve(path);
if (Files.isRegularFile(file)) {
String relativePath = runDirectory.relativize(file).normalize().toString().replace(File.separatorChar, '/');
files.add(new McbbsModpackManifest.AddonFile(true, relativePath, encodeHex(digest("SHA-1", file))));
}
return true;
} else {
return false;
}
});
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(repository.getResolvedPreservingPatchesVersion(version));
String gameVersion = GameVersion.minecraftVersion(repository.getVersionJar(version))
.orElseThrow(() -> new IOException("Cannot parse the version of " + version));
List<McbbsModpackManifest.Addon> addons = new ArrayList<>();
addons.add(new McbbsModpackManifest.Addon(MINECRAFT.getPatchId(), gameVersion));
analyzer.getVersion(FORGE).ifPresent(forgeVersion ->
addons.add(new McbbsModpackManifest.Addon(FORGE.getPatchId(), forgeVersion)));
analyzer.getVersion(LITELOADER).ifPresent(liteLoaderVersion ->
addons.add(new McbbsModpackManifest.Addon(LITELOADER.getPatchId(), liteLoaderVersion)));
analyzer.getVersion(OPTIFINE).ifPresent(optifineVersion ->
addons.add(new McbbsModpackManifest.Addon(OPTIFINE.getPatchId(), optifineVersion)));
analyzer.getVersion(FABRIC).ifPresent(fabricVersion ->
addons.add(new McbbsModpackManifest.Addon(FABRIC.getPatchId(), fabricVersion)));
List<Library> libraries = new ArrayList<>();
// TODO libraries
List<McbbsModpackManifest.Origin> origins = new ArrayList<>();
// TODO origins
McbbsModpackManifest.Settings settings = new McbbsModpackManifest.Settings();
McbbsModpackManifest.LaunchInfo launchInfo = new McbbsModpackManifest.LaunchInfo(info.getMinMemory(), info.getSupportedJavaVersions(), StringUtils.tokenize(info.getLaunchArguments()), StringUtils.tokenize(info.getJavaArguments()));
McbbsModpackManifest manifest = new McbbsModpackManifest(McbbsModpackManifest.MANIFEST_TYPE, 1, info.getName(), info.getVersion(), info.getAuthor(), info.getDescription(), info.getFileApi() == null ? null : StringUtils.removeSuffix(info.getFileApi(), "/"), info.getUrl(), info.isForceUpdate(), origins, addons, libraries, files, settings, launchInfo);
zip.putTextFile(JsonUtils.GSON.toJson(manifest), "manifest.json");
}
}
public static final ModpackExportInfo.Options OPTION = new ModpackExportInfo.Options()
.requireFileApi(true)
.requireUrl()
.requireForceUpdate()
.requireMinMemory()
.requireAuthlibInjectorServer()
.requireJavaArguments()
.requireLaunchArguments()
.requireOrigins();
}

View File

@@ -0,0 +1,130 @@
/*
* 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.mod.mcbbs;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.GameBuilder;
import org.jackhuang.hmcl.game.DefaultGameRepository;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.mod.MinecraftInstanceTask;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.mod.ModpackInstallTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class McbbsModpackLocalInstallTask extends Task<Void> {
private final File zipFile;
private final Modpack modpack;
private final McbbsModpackManifest manifest;
private final String name;
private final boolean update;
private final DefaultGameRepository repository;
private final List<Task<?>> dependencies = new LinkedList<>();
private final List<Task<?>> dependents = new LinkedList<>();
public McbbsModpackLocalInstallTask(DefaultDependencyManager dependencyManager, File zipFile, Modpack modpack, McbbsModpackManifest manifest, String name) {
this.zipFile = zipFile;
this.modpack = modpack;
this.manifest = manifest;
this.name = name;
this.repository = dependencyManager.getGameRepository();
File run = repository.getRunDirectory(name);
File json = repository.getModpackConfiguration(name);
if (repository.hasVersion(name) && !json.exists())
throw new IllegalArgumentException("Version " + name + " already exists.");
this.update = repository.hasVersion(name);
GameBuilder builder = dependencyManager.gameBuilder().name(name);
for (McbbsModpackManifest.Addon addon : manifest.getAddons()) {
builder.version(addon.getId(), addon.getVersion());
}
dependents.add(builder.buildAsync());
onDone().register(event -> {
if (event.isFailed())
repository.removeVersionFromDisk(name);
});
ModpackConfiguration<McbbsModpackManifest> config = null;
try {
if (json.exists()) {
config = JsonUtils.GSON.fromJson(FileUtils.readText(json), new TypeToken<ModpackConfiguration<McbbsModpackManifest>>() {
}.getType());
if (!MODPACK_TYPE.equals(config.getType()))
throw new IllegalArgumentException("Version " + name + " is not a Mcbbs modpack. Cannot update this version.");
}
} catch (JsonParseException | IOException ignore) {
}
dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), "/overrides", any -> true, config).withStage("hmcl.modpack"));
dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), "/overrides", manifest, MODPACK_TYPE, modpack.getName(), modpack.getVersion(), repository.getModpackConfiguration(name)).withStage("hmcl.modpack"));
}
@Override
public List<Task<?>> getDependents() {
return dependents;
}
@Override
public List<Task<?>> getDependencies() {
return dependencies;
}
@Override
public void execute() throws Exception {
Version version = repository.readVersionJson(name);
Optional<Version> mcbbsPatch = version.getPatches().stream().filter(patch -> PATCH_NAME.equals(patch.getId())).findFirst();
if (!update) {
Version patch = new Version(PATCH_NAME).setLibraries(manifest.getLibraries());
dependencies.add(repository.saveAsync(version.addPatch(patch)));
} else if (mcbbsPatch.isPresent()) {
// This mcbbs modpack was installed by HMCL.
Version patch = mcbbsPatch.get().setLibraries(manifest.getLibraries());
dependencies.add(repository.saveAsync(version.addPatch(patch)));
} else {
// This mcbbs modpack was installed by other launchers.
// TODO: maintain libraries.
}
}
@Override
public List<String> getStages() {
return Stream.concat(
dependents.stream().flatMap(task -> task.getStages().stream()),
Stream.of("hmcl.modpack")
).collect(Collectors.toList());
}
private static final String PATCH_NAME = "mcbbs";
public static final String MODPACK_TYPE = "Mcbbs";
}

View File

@@ -0,0 +1,409 @@
/*
* 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.mod.mcbbs;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.game.LaunchOptions;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.util.gson.*;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.MINECRAFT;
public class McbbsModpackManifest implements Validation {
public static final String MANIFEST_TYPE = "minecraftModpack";
private final String manifestType;
private final int manifestVersion;
private final String name;
private final String version;
private final String author;
private final String description;
@Nullable
private final String fileApi;
private final String url;
private final boolean forceUpdate;
@SerializedName("origin")
private final List<Origin> origins;
private final List<Addon> addons;
private final List<Library> libraries;
private final List<File> files;
private final Settings settings;
private final LaunchInfo launchInfo;
// sandbox and antiCheating are both not supported.
public McbbsModpackManifest() {
this(MANIFEST_TYPE, 1, "", "", "", "", null, "", false, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), new Settings(), new LaunchInfo());
}
public McbbsModpackManifest(String manifestType, int manifestVersion, String name, String version, String author, String description, @Nullable String fileApi, String url, boolean forceUpdate, List<Origin> origins, List<Addon> addons, List<Library> libraries, List<File> files, Settings settings, LaunchInfo launchInfo) {
this.manifestType = manifestType;
this.manifestVersion = manifestVersion;
this.name = name;
this.version = version;
this.author = author;
this.description = description;
this.fileApi = fileApi;
this.url = url;
this.forceUpdate = forceUpdate;
this.origins = origins;
this.addons = addons;
this.libraries = libraries;
this.files = files;
this.settings = settings;
this.launchInfo = launchInfo;
}
public String getManifestType() {
return manifestType;
}
public int getManifestVersion() {
return manifestVersion;
}
public String getName() {
return name;
}
public String getVersion() {
return version;
}
public String getAuthor() {
return author;
}
public String getDescription() {
return description;
}
public String getFileApi() {
return fileApi;
}
public String getUrl() {
return url;
}
public boolean isForceUpdate() {
return forceUpdate;
}
public List<Origin> getOrigins() {
return origins;
}
public List<Addon> getAddons() {
return addons;
}
public List<Library> getLibraries() {
return libraries;
}
public List<File> getFiles() {
return files;
}
public Settings getSettings() {
return settings;
}
public LaunchInfo getLaunchInfo() {
return launchInfo;
}
@Override
public void validate() throws JsonParseException, TolerableValidationException {
if (!MANIFEST_TYPE.equals(manifestType))
throw new JsonParseException("McbbsModpackManifest.manifestType must be 'minecraftModpack'");
// if (manifestVersion > 1)
// throw new JsonParseException("Only supports version 1 of McbbsModpackManifest");
if (files == null)
throw new JsonParseException("McbbsModpackManifest.files cannot be null");
}
public static final class Origin {
private final String type;
private final int id;
public Origin() {
this("", 0);
}
public Origin(String type, int id) {
this.type = type;
this.id = id;
}
public String getType() {
return type;
}
public int getId() {
return id;
}
}
public static final class Addon {
private final String id;
private final String version;
public Addon() {
this("", "");
}
public Addon(String id, String version) {
this.id = id;
this.version = version;
}
public String getId() {
return id;
}
public String getVersion() {
return version;
}
}
public static final class Settings {
@SerializedName("install_modes")
private final boolean installMods;
@SerializedName("install_resourcepack")
private final boolean installResourcepack;
public Settings() {
this(true, true);
}
public Settings(boolean installMods, boolean installResourcepack) {
this.installMods = installMods;
this.installResourcepack = installResourcepack;
}
public boolean isInstallMods() {
return installMods;
}
public boolean isInstallResourcepack() {
return installResourcepack;
}
}
@JsonType(
property = "type",
subtypes = {
@JsonSubtype(clazz = AddonFile.class, name = "addon"),
@JsonSubtype(clazz = CurseFile.class, name = "curse")
}
)
public static abstract class File implements Validation {
private final boolean force;
public File(boolean force) {
this.force = force;
}
@Override
public void validate() throws JsonParseException, TolerableValidationException {
}
public boolean isForce() {
return force;
}
}
public static final class AddonFile extends File {
private final String path;
private final String hash;
public AddonFile(boolean force, String path, String hash) {
super(force);
this.path = path;
this.hash = hash;
}
public String getPath() {
return path;
}
public String getHash() {
return hash;
}
@Override
public void validate() throws JsonParseException, TolerableValidationException {
super.validate();
Validation.requireNonNull(path, "AddonFile.path cannot be null");
Validation.requireNonNull(hash, "AddonFile.hash cannot be null");
}
}
public static final class CurseFile extends File {
private final int projectID;
private final int fileID;
private final String fileName;
private final String url;
public CurseFile() {
this(false, 0, 0, "", "");
}
public CurseFile(boolean force, int projectID, int fileID, String fileName, String url) {
super(force);
this.projectID = projectID;
this.fileID = fileID;
this.fileName = fileName;
this.url = url;
}
public int getProjectID() {
return projectID;
}
public int getFileID() {
return fileID;
}
public String getFileName() {
return fileName;
}
public URL getUrl() {
return url == null ? NetworkUtils.toURL("https://www.curseforge.com/minecraft/mc-mods/" + projectID + "/download/" + fileID + "/file")
: NetworkUtils.toURL(NetworkUtils.encodeLocation(url));
}
@Override
public void validate() throws JsonParseException, TolerableValidationException {
super.validate();
if (projectID == 0 || fileID == 0) {
throw new JsonParseException("CurseFile.{projectID|fileID} cannot be empty.");
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CurseFile curseFile = (CurseFile) o;
return projectID == curseFile.projectID && fileID == curseFile.fileID;
}
@Override
public int hashCode() {
return Objects.hash(projectID, fileID);
}
}
public static final class LaunchInfo {
private final int minMemory;
private final List<Integer> supportJava;
@SerializedName("launchArgument")
private final List<String> launchArguments;
@SerializedName("javaArgument")
private final List<String> javaArguments;
public LaunchInfo() {
this(0, Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
}
public LaunchInfo(int minMemory, List<Integer> supportJava, List<String> launchArguments, List<String> javaArguments) {
this.minMemory = minMemory;
this.supportJava = supportJava;
this.launchArguments = launchArguments;
this.javaArguments = javaArguments;
}
public int getMinMemory() {
return minMemory;
}
@Nullable
public List<Integer> getSupportJava() {
return supportJava;
}
public List<String> getLaunchArguments() {
return Optional.ofNullable(launchArguments).orElseGet(Collections::emptyList);
}
public List<String> getJavaArguments() {
return Optional.ofNullable(javaArguments).orElseGet(Collections::emptyList);
}
}
public static class ServerInfo {
private final String authlibInjectorServer;
public ServerInfo() {
this(null);
}
public ServerInfo(String authlibInjectorServer) {
this.authlibInjectorServer = authlibInjectorServer;
}
@Nullable
public String getAuthlibInjectorServer() {
return authlibInjectorServer;
}
}
public Modpack toModpack(Charset encoding) throws IOException {
String gameVersion = addons.stream().filter(x -> MINECRAFT.getPatchId().equals(x.id)).findAny()
.orElseThrow(() -> new IOException("Cannot find game version")).getVersion();
return new Modpack(name, author, version, gameVersion, description, encoding, this);
}
public void injectLaunchOptions(LaunchOptions.Builder launchOptions) {
launchOptions.getGameArguments().addAll(launchInfo.getLaunchArguments());
launchOptions.getJavaArguments().addAll(launchInfo.getJavaArguments());
}
/**
* @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(Path zip, Charset encoding) throws IOException, JsonParseException {
String json = CompressingUtils.readTextZipEntry(zip, "manifest.json", encoding);
McbbsModpackManifest manifest = JsonUtils.fromNonNullJson(json, McbbsModpackManifest.class);
return manifest.toModpack(encoding);
}
}

View File

@@ -0,0 +1,95 @@
/*
* 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.mod.mcbbs;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.GameBuilder;
import org.jackhuang.hmcl.game.DefaultGameRepository;
import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.mod.server.ServerModpackManifest;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
public class McbbsModpackRemoteInstallTask extends Task<Void> {
private final String name;
private final DefaultDependencyManager dependency;
private final DefaultGameRepository repository;
private final List<Task<?>> dependencies = new LinkedList<>();
private final List<Task<?>> dependents = new LinkedList<>();
private final ServerModpackManifest manifest;
public McbbsModpackRemoteInstallTask(DefaultDependencyManager dependencyManager, ServerModpackManifest manifest, String name) {
this.name = name;
this.dependency = dependencyManager;
this.repository = dependencyManager.getGameRepository();
this.manifest = manifest;
File json = repository.getModpackConfiguration(name);
if (repository.hasVersion(name) && !json.exists())
throw new IllegalArgumentException("Version " + name + " already exists.");
GameBuilder builder = dependencyManager.gameBuilder().name(name);
for (ServerModpackManifest.Addon addon : manifest.getAddons()) {
builder.version(addon.getId(), addon.getVersion());
}
dependents.add(builder.buildAsync());
onDone().register(event -> {
if (event.isFailed())
repository.removeVersionFromDisk(name);
});
ModpackConfiguration<ServerModpackManifest> config = null;
try {
if (json.exists()) {
config = JsonUtils.GSON.fromJson(FileUtils.readText(json), new TypeToken<ModpackConfiguration<ServerModpackManifest>>() {
}.getType());
if (!MODPACK_TYPE.equals(config.getType()))
throw new IllegalArgumentException("Version " + name + " is not a Server modpack. Cannot update this version.");
}
} catch (JsonParseException | IOException ignore) {
}
}
@Override
public List<Task<?>> getDependents() {
return dependents;
}
@Override
public List<Task<?>> getDependencies() {
return dependencies;
}
@Override
public void execute() throws Exception {
// dependencies.add(new McbbsModpackCompletionTask(dependency, name, new ModpackConfiguration<>(manifest, MODPACK_TYPE, manifest.getName(), manifest.getVersion(), Collections.emptyList())));
}
public static final String MODPACK_TYPE = "Server";
}

View File

@@ -22,6 +22,7 @@ import org.jackhuang.hmcl.game.DefaultGameRepository;
import org.jackhuang.hmcl.game.GameVersion; import org.jackhuang.hmcl.game.GameVersion;
import org.jackhuang.hmcl.mod.ModAdviser; import org.jackhuang.hmcl.mod.ModAdviser;
import org.jackhuang.hmcl.mod.Modpack; import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.ModpackExportInfo;
import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Logging; import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.gson.JsonUtils;
@@ -91,4 +92,6 @@ public class MultiMCModpackExportTask extends Task<Void> {
zip.putTextFile("", ".packignore"); zip.putTextFile("", ".packignore");
} }
} }
public static final ModpackExportInfo.Options OPTION = new ModpackExportInfo.Options().requireMinMemory();
} }

View File

@@ -69,7 +69,7 @@ public class ServerModpackCompletionTask extends Task<Void> {
}.getType()); }.getType());
} }
} catch (Exception e) { } catch (Exception e) {
Logging.LOG.log(Level.WARNING, "Unable to read CurseForge modpack manifest.json", e); Logging.LOG.log(Level.WARNING, "Unable to read Server modpack manifest.json", e);
} }
} else { } else {
this.manifest = manifest; this.manifest = manifest;

View File

@@ -23,6 +23,7 @@ import org.jackhuang.hmcl.game.GameVersion;
import org.jackhuang.hmcl.mod.ModAdviser; import org.jackhuang.hmcl.mod.ModAdviser;
import org.jackhuang.hmcl.mod.Modpack; import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.ModpackConfiguration; import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.mod.ModpackExportInfo;
import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Logging; import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.StringUtils;
@@ -43,27 +44,15 @@ import static org.jackhuang.hmcl.util.Hex.encodeHex;
public class ServerModpackExportTask extends Task<Void> { public class ServerModpackExportTask extends Task<Void> {
private final DefaultGameRepository repository; private final DefaultGameRepository repository;
private final String versionId; private final String versionId;
private final List<String> whitelist; private final ModpackExportInfo exportInfo;
private final File output;
private final String modpackName;
private final String modpackAuthor;
private final String modpackVersion;
private final String modpackDescription;
private final String modpackFileApi;
public ServerModpackExportTask(DefaultGameRepository repository, String versionId, List<String> whitelist, String modpackName, String modpackAuthor, String modpackVersion, String modpackDescription, String modpackFileApi, File output) { public ServerModpackExportTask(DefaultGameRepository repository, String version, ModpackExportInfo exportInfo) {
this.repository = repository; this.repository = repository;
this.versionId = versionId; this.versionId = version;
this.whitelist = whitelist; this.exportInfo = exportInfo.validate();
this.output = output;
this.modpackName = modpackName;
this.modpackAuthor = modpackAuthor;
this.modpackVersion = modpackVersion;
this.modpackDescription = modpackDescription;
this.modpackFileApi = modpackFileApi;
onDone().register(event -> { onDone().register(event -> {
if (event.isFailed()) output.delete(); if (event.isFailed()) exportInfo.getOutput().toFile().delete();
}); });
} }
@@ -73,11 +62,11 @@ public class ServerModpackExportTask extends Task<Void> {
blackList.add(versionId + ".jar"); blackList.add(versionId + ".jar");
blackList.add(versionId + ".json"); blackList.add(versionId + ".json");
Logging.LOG.info("Compressing game files without some files in blacklist, including files or directories: usernamecache.json, asm, logs, backups, versions, assets, usercache.json, libraries, crash-reports, launcher_profiles.json, NVIDIA, TCNodeTracker"); Logging.LOG.info("Compressing game files without some files in blacklist, including files or directories: usernamecache.json, asm, logs, backups, versions, assets, usercache.json, libraries, crash-reports, launcher_profiles.json, NVIDIA, TCNodeTracker");
try (Zipper zip = new Zipper(output.toPath())) { try (Zipper zip = new Zipper(exportInfo.getOutput())) {
Path runDirectory = repository.getRunDirectory(versionId).toPath(); Path runDirectory = repository.getRunDirectory(versionId).toPath();
List<ModpackConfiguration.FileInformation> files = new ArrayList<>(); List<ModpackConfiguration.FileInformation> files = new ArrayList<>();
zip.putDirectory(runDirectory, "overrides", path -> { zip.putDirectory(runDirectory, "overrides", path -> {
if (Modpack.acceptFile(path, blackList, whitelist)) { if (Modpack.acceptFile(path, blackList, exportInfo.getWhitelist())) {
Path file = runDirectory.resolve(path); Path file = runDirectory.resolve(path);
if (Files.isRegularFile(file)) { if (Files.isRegularFile(file)) {
String relativePath = runDirectory.relativize(file).normalize().toString().replace(File.separatorChar, '/'); String relativePath = runDirectory.relativize(file).normalize().toString().replace(File.separatorChar, '/');
@@ -102,8 +91,11 @@ public class ServerModpackExportTask extends Task<Void> {
addons.add(new ServerModpackManifest.Addon(OPTIFINE.getPatchId(), optifineVersion))); addons.add(new ServerModpackManifest.Addon(OPTIFINE.getPatchId(), optifineVersion)));
analyzer.getVersion(FABRIC).ifPresent(fabricVersion -> analyzer.getVersion(FABRIC).ifPresent(fabricVersion ->
addons.add(new ServerModpackManifest.Addon(FABRIC.getPatchId(), fabricVersion))); addons.add(new ServerModpackManifest.Addon(FABRIC.getPatchId(), fabricVersion)));
ServerModpackManifest manifest = new ServerModpackManifest(modpackName, modpackAuthor, modpackVersion, modpackDescription, StringUtils.removeSuffix(modpackFileApi, "/"), files, addons); ServerModpackManifest manifest = new ServerModpackManifest(exportInfo.getName(), exportInfo.getAuthor(), exportInfo.getVersion(), exportInfo.getDescription(), StringUtils.removeSuffix(exportInfo.getFileApi(), "/"), files, addons);
zip.putTextFile(JsonUtils.GSON.toJson(manifest), "server-manifest.json"); zip.putTextFile(JsonUtils.GSON.toJson(manifest), "server-manifest.json");
} }
} }
public static final ModpackExportInfo.Options OPTION = new ModpackExportInfo.Options()
.requireFileApi(false);
} }

View File

@@ -0,0 +1,7 @@
package org.jackhuang.hmcl.util.gson;
public @interface JsonSubtype {
Class<?> clazz();
String name();
}

View File

@@ -0,0 +1,14 @@
package org.jackhuang.hmcl.util.gson;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface JsonType {
String property();
JsonSubtype[] subtypes();
}

View File

@@ -0,0 +1,115 @@
package org.jackhuang.hmcl.util.gson;
import com.google.gson.*;
import com.google.gson.internal.Streams;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class JsonTypeAdapterFactory implements TypeAdapterFactory {
public static final JsonTypeAdapterFactory INSTANCE = new JsonTypeAdapterFactory();
private <T> TypeAdapter<T> createForJsonType(Gson gson, TypeToken<T> type) {
Class<? super T> rawType = type.getRawType();
JsonType jsonType = rawType.getDeclaredAnnotation(JsonType.class);
if (jsonType == null)
return null;
JsonSubtype[] subtypes = jsonType.subtypes();
Map<String, TypeAdapter<?>> labelTypeAdapterMap = new HashMap<>();
Map<Class<?>, TypeAdapter<?>> classTypeAdapterMap = new HashMap<>();
Map<Class<?>, JsonSubtype> classJsonSubtypeMap = new HashMap<>();
for (JsonSubtype subtype : subtypes) {
TypeAdapter<?> typeAdapter = gson.getDelegateAdapter(this, TypeToken.get(subtype.clazz()));
labelTypeAdapterMap.put(subtype.name(), typeAdapter);
classTypeAdapterMap.put(subtype.clazz(), typeAdapter);
classJsonSubtypeMap.put(subtype.clazz(), subtype);
}
return new TypeAdapter<T>() {
@Override
public void write(JsonWriter out, T value) throws IOException {
Class<?> type = value.getClass();
@SuppressWarnings("unchecked")
TypeAdapter<T> delegate = (TypeAdapter<T>) classTypeAdapterMap.get(type);
if (delegate == null) {
throw new JsonParseException("Cannot serialize " + type.getName() + ". Please check your @JsonType configuration");
}
JsonSubtype subtype = classJsonSubtypeMap.get(type);
JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject();
if (jsonObject.has(jsonType.property())) {
throw new JsonParseException("Cannot serialize " + type.getName() + ". Because it has already defined a field named '" + jsonType.property() + "'");
}
jsonObject.add(jsonType.property(), new JsonPrimitive(subtype.name()));
Streams.write(jsonObject, out);
}
@Override
public T read(JsonReader in) {
JsonElement jsonElement = Streams.parse(in);
JsonElement typeLabelElement = jsonElement.getAsJsonObject().get(jsonType.property());
if (typeLabelElement == null) {
throw new JsonParseException("Cannot deserialize " + type + ". Because it does not define a field named '" + jsonType.property() + "'");
}
String typeLabel = typeLabelElement.getAsString();
@SuppressWarnings("unchecked")
TypeAdapter<T> delegate = (TypeAdapter<T>) labelTypeAdapterMap.get(typeLabel);
if (delegate == null) {
throw new JsonParseException("Cannot deserialize " + type + " with subtype '" + typeLabel + "'");
}
return delegate.fromJsonTree(jsonElement);
}
};
}
private <T> TypeAdapter<T> createForJsonSubtype(Gson gson, TypeToken<T> type) {
Class<? super T> rawType = type.getRawType();
if (rawType.getSuperclass() == null) return null;
JsonType jsonType = rawType.getSuperclass().getDeclaredAnnotation(JsonType.class);
if (jsonType == null)
return null;
JsonSubtype jsonSubtype = null;
for (JsonSubtype subtype : jsonType.subtypes()) {
if (subtype.clazz() == rawType) {
jsonSubtype = subtype;
}
}
if (jsonSubtype == null)
return null;
final JsonSubtype subtype = jsonSubtype;
TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
return new TypeAdapter<T>() {
@Override
public void write(JsonWriter out, T value) throws IOException {
Class<?> type = value.getClass();
JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject();
if (jsonObject.has(jsonType.property())) {
throw new JsonParseException("Cannot serialize " + type.getName() + ". Because it has already defined a field named '" + jsonType.property() + "'");
}
jsonObject.add(jsonType.property(), new JsonPrimitive(subtype.name()));
Streams.write(jsonObject, out);
}
@Override
public T read(JsonReader in) throws IOException {
return delegate.read(in);
}
};
}
@Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
TypeAdapter<T> typeAdapter = createForJsonType(gson, type);
if (typeAdapter == null)
typeAdapter = createForJsonSubtype(gson, type);
return typeAdapter;
}
}

View File

@@ -17,29 +17,21 @@
*/ */
package org.jackhuang.hmcl.util.gson; package org.jackhuang.hmcl.util.gson;
import java.io.File;
import java.util.Date;
import java.util.UUID;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException; import com.google.gson.JsonParseException;
import com.google.gson.JsonSyntaxException; import com.google.gson.JsonSyntaxException;
import java.io.File;
import java.util.Date;
import java.util.UUID;
/** /**
* @author yushijinhun * @author yushijinhun
*/ */
public final class JsonUtils { public final class JsonUtils {
public static final Gson GSON = new GsonBuilder() public static final Gson GSON = defaultGsonBuilder().create();
.enableComplexMapKeySerialization()
.setPrettyPrinting()
.registerTypeAdapter(Date.class, DateTypeAdapter.INSTANCE)
.registerTypeAdapter(UUID.class, UUIDTypeAdapter.INSTANCE)
.registerTypeAdapter(File.class, FileTypeAdapter.INSTANCE)
.registerTypeAdapterFactory(ValidationTypeAdapterFactory.INSTANCE)
.registerTypeAdapterFactory(LowerCaseEnumTypeAdapterFactory.INSTANCE)
.create();
private JsonUtils() { private JsonUtils() {
} }
@@ -58,4 +50,16 @@ public final class JsonUtils {
return null; return null;
} }
} }
public static GsonBuilder defaultGsonBuilder() {
return new GsonBuilder()
.enableComplexMapKeySerialization()
.setPrettyPrinting()
.registerTypeAdapter(Date.class, DateTypeAdapter.INSTANCE)
.registerTypeAdapter(UUID.class, UUIDTypeAdapter.INSTANCE)
.registerTypeAdapter(File.class, FileTypeAdapter.INSTANCE)
.registerTypeAdapterFactory(ValidationTypeAdapterFactory.INSTANCE)
.registerTypeAdapterFactory(LowerCaseEnumTypeAdapterFactory.INSTANCE)
.registerTypeAdapterFactory(JsonTypeAdapterFactory.INSTANCE);
}
} }